hedra 2.0.2 → 2.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.
@@ -0,0 +1,151 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'uri'
4
+ require 'digest'
5
+
6
+ module Hedra
7
+ # Check for Subresource Integrity (SRI) on external resources
8
+ class SriChecker
9
+ EXTERNAL_RESOURCE_PATTERN = /<(script|link)\s+[^>]*>/i.freeze
10
+ SRC_PATTERN = /(?:src|href)=["']([^"']+)["']/i.freeze
11
+ INTEGRITY_PATTERN = /integrity=["']([^"']+)["']/i.freeze
12
+ CROSSORIGIN_PATTERN = /crossorigin(?:=["']([^"']+)["'])?/i.freeze
13
+
14
+ def initialize(http_client: nil)
15
+ @http_client = http_client
16
+ end
17
+
18
+ def check(url, html_content = nil)
19
+ findings = []
20
+
21
+ # Fetch HTML if not provided
22
+ html_content ||= fetch_html(url)
23
+ return findings unless html_content
24
+
25
+ base_uri = URI.parse(url)
26
+ external_resources = extract_external_resources(html_content, base_uri)
27
+
28
+ return findings if external_resources.empty?
29
+
30
+ external_resources.each do |resource|
31
+ next if resource[:has_integrity]
32
+
33
+ findings << {
34
+ header: 'subresource-integrity',
35
+ issue: "External #{resource[:type]} missing SRI: #{truncate_url(resource[:url])}",
36
+ severity: :warning,
37
+ recommended_fix: "Add integrity attribute with hash: integrity=\"sha384-...\" crossorigin=\"anonymous\""
38
+ }
39
+ end
40
+
41
+ # Check for crossorigin without integrity
42
+ external_resources.each do |resource|
43
+ if resource[:has_integrity] && !resource[:has_crossorigin]
44
+ findings << {
45
+ header: 'subresource-integrity',
46
+ issue: "SRI without crossorigin attribute: #{truncate_url(resource[:url])}",
47
+ severity: :info,
48
+ recommended_fix: 'Add crossorigin="anonymous" when using integrity attribute'
49
+ }
50
+ end
51
+ end
52
+
53
+ findings
54
+ rescue StandardError => e
55
+ warn "SRI check failed: #{e.message}" if ENV['DEBUG']
56
+ []
57
+ end
58
+
59
+ private
60
+
61
+ def fetch_html(url)
62
+ return nil unless @http_client
63
+
64
+ response = @http_client.get(url)
65
+ content_type = response.headers['Content-Type'].to_s
66
+
67
+ # Only process HTML content
68
+ return nil unless content_type.include?('text/html')
69
+
70
+ response.body.to_s
71
+ rescue StandardError => e
72
+ warn "Failed to fetch HTML for SRI check: #{e.message}" if ENV['DEBUG']
73
+ nil
74
+ end
75
+
76
+ def extract_external_resources(html, base_uri)
77
+ resources = []
78
+
79
+ html.scan(EXTERNAL_RESOURCE_PATTERN) do |match|
80
+ tag = match[0]
81
+ full_tag = ::Regexp.last_match(0)
82
+
83
+ # Extract src/href
84
+ src_match = full_tag.match(SRC_PATTERN)
85
+ next unless src_match
86
+
87
+ resource_url = src_match[1]
88
+ next if resource_url.nil? || resource_url.empty?
89
+
90
+ # Skip data URIs, inline scripts, and relative URLs to same origin
91
+ next if resource_url.start_with?('data:', 'blob:', '#', 'javascript:')
92
+
93
+ # Resolve relative URLs
94
+ absolute_url = resolve_url(resource_url, base_uri)
95
+ next unless absolute_url
96
+
97
+ # Check if external
98
+ resource_uri = URI.parse(absolute_url)
99
+ next if same_origin?(base_uri, resource_uri)
100
+
101
+ # Check for integrity and crossorigin attributes
102
+ has_integrity = full_tag.match?(INTEGRITY_PATTERN)
103
+ has_crossorigin = full_tag.match?(CROSSORIGIN_PATTERN)
104
+
105
+ resources << {
106
+ type: tag.downcase,
107
+ url: absolute_url,
108
+ has_integrity: has_integrity,
109
+ has_crossorigin: has_crossorigin
110
+ }
111
+ rescue URI::InvalidURIError
112
+ # Skip invalid URIs
113
+ next
114
+ end
115
+
116
+ resources.uniq { |r| r[:url] }
117
+ end
118
+
119
+ def resolve_url(url, base_uri)
120
+ return url if url.match?(%r{^https?://})
121
+
122
+ if url.start_with?('//')
123
+ "#{base_uri.scheme}:#{url}"
124
+ elsif url.start_with?('/')
125
+ "#{base_uri.scheme}://#{base_uri.host}#{base_uri.port && ![80, 443].include?(base_uri.port) ? ":#{base_uri.port}" : ''}#{url}"
126
+ else
127
+ # Relative URL
128
+ base_path = base_uri.path.split('/')[0..-2].join('/')
129
+ "#{base_uri.scheme}://#{base_uri.host}#{base_uri.port && ![80, 443].include?(base_uri.port) ? ":#{base_uri.port}" : ''}#{base_path}/#{url}"
130
+ end
131
+ rescue StandardError
132
+ nil
133
+ end
134
+
135
+ def same_origin?(uri1, uri2)
136
+ uri1.scheme == uri2.scheme &&
137
+ uri1.host == uri2.host &&
138
+ (uri1.port || default_port(uri1.scheme)) == (uri2.port || default_port(uri2.scheme))
139
+ end
140
+
141
+ def default_port(scheme)
142
+ scheme == 'https' ? 443 : 80
143
+ end
144
+
145
+ def truncate_url(url, max_length = 60)
146
+ return url if url.length <= max_length
147
+
148
+ "#{url[0...max_length]}..."
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'uri'
4
+
5
+ module Hedra
6
+ # URL validation and sanitization
7
+ class UrlValidator
8
+ ALLOWED_SCHEMES = %w[http https].freeze
9
+ MAX_URL_LENGTH = 2048
10
+ DANGEROUS_CHARS = ['<', '>', '"', '{', '}', '|', '\\', '^', '`', '[', ']'].freeze
11
+
12
+ class << self
13
+ def valid?(url)
14
+ return false if url.nil? || url.empty?
15
+ return false if url.length > MAX_URL_LENGTH
16
+
17
+ uri = parse_url(url)
18
+ return false unless uri
19
+ return false unless ALLOWED_SCHEMES.include?(uri.scheme)
20
+ return false unless valid_host?(uri.host)
21
+ return false if contains_dangerous_chars?(url)
22
+
23
+ true
24
+ rescue StandardError
25
+ false
26
+ end
27
+
28
+ def validate!(url)
29
+ raise Error, 'URL cannot be empty' if url.nil? || url.empty?
30
+ raise Error, "URL too long (max #{MAX_URL_LENGTH} characters)" if url.length > MAX_URL_LENGTH
31
+
32
+ uri = parse_url(url)
33
+ raise Error, 'Invalid URL format' unless uri
34
+ raise Error, "Invalid scheme: #{uri.scheme}. Only HTTP/HTTPS allowed" unless ALLOWED_SCHEMES.include?(uri.scheme)
35
+ raise Error, 'Invalid or missing host' unless valid_host?(uri.host)
36
+ raise Error, 'URL contains dangerous characters' if contains_dangerous_chars?(url)
37
+
38
+ uri
39
+ end
40
+
41
+ def sanitize(url)
42
+ url = url.strip
43
+ url = "https://#{url}" unless url.start_with?('http://', 'https://')
44
+ url
45
+ end
46
+
47
+ def normalize(url)
48
+ uri = parse_url(url)
49
+ return url unless uri
50
+
51
+ # Remove default ports
52
+ port = if (uri.scheme == 'http' && uri.port == 80) || (uri.scheme == 'https' && uri.port == 443)
53
+ nil
54
+ else
55
+ uri.port
56
+ end
57
+
58
+ # Rebuild URL
59
+ normalized = "#{uri.scheme}://#{uri.host}"
60
+ normalized += ":#{port}" if port
61
+ normalized += uri.path if uri.path && uri.path != '/'
62
+ normalized += "?#{uri.query}" if uri.query
63
+ normalized
64
+ end
65
+
66
+ private
67
+
68
+ def parse_url(url)
69
+ URI.parse(url)
70
+ rescue URI::InvalidURIError
71
+ nil
72
+ end
73
+
74
+ def valid_host?(host)
75
+ return false if host.nil? || host.empty?
76
+ return false if host.length > 253 # Max domain length
77
+
78
+ # Check for valid hostname format
79
+ return false if host.start_with?('.') || host.end_with?('.')
80
+ return false if host.include?('..')
81
+
82
+ # Check for localhost/private IPs in production
83
+ return false if host == 'localhost'
84
+ return false if host.start_with?('127.', '10.', '192.168.', '172.')
85
+
86
+ true
87
+ end
88
+
89
+ def contains_dangerous_chars?(url)
90
+ DANGEROUS_CHARS.any? { |char| url.include?(char) }
91
+ end
92
+ end
93
+ end
94
+ end
data/lib/hedra/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Hedra
4
- VERSION = '2.0.2'
4
+ VERSION = '2.1.0'
5
5
  end
data/lib/hedra.rb CHANGED
@@ -7,6 +7,8 @@ module Hedra
7
7
  end
8
8
 
9
9
  require_relative 'hedra/version'
10
+ require_relative 'hedra/banner'
11
+ require_relative 'hedra/url_validator'
10
12
  require_relative 'hedra/config'
11
13
  require_relative 'hedra/scorer'
12
14
  require_relative 'hedra/circuit_breaker'
@@ -20,5 +22,10 @@ require_relative 'hedra/http_client'
20
22
  require_relative 'hedra/plugin_manager'
21
23
  require_relative 'hedra/exporter'
22
24
  require_relative 'hedra/html_reporter'
25
+ require_relative 'hedra/sri_checker'
26
+ require_relative 'hedra/cors_checker'
27
+ require_relative 'hedra/protocol_checker'
28
+ require_relative 'hedra/ct_checker'
29
+ require_relative 'hedra/dns_checker'
23
30
  require_relative 'hedra/analyzer'
24
31
  require_relative 'hedra/cli'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hedra
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.2
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - bl4ckstack
@@ -79,6 +79,20 @@ dependencies:
79
79
  - - "~>"
80
80
  - !ruby/object:Gem::Version
81
81
  version: '0.8'
82
+ - !ruby/object:Gem::Dependency
83
+ name: resolv
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: 0.2.0
89
+ type: :runtime
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: 0.2.0
82
96
  - !ruby/object:Gem::Dependency
83
97
  name: thor
84
98
  requirement: !ruby/object:Gem::Requirement
@@ -123,20 +137,27 @@ files:
123
137
  - config/example_rules.yml
124
138
  - lib/hedra.rb
125
139
  - lib/hedra/analyzer.rb
140
+ - lib/hedra/banner.rb
126
141
  - lib/hedra/baseline.rb
127
142
  - lib/hedra/cache.rb
128
143
  - lib/hedra/certificate_checker.rb
129
144
  - lib/hedra/circuit_breaker.rb
130
145
  - lib/hedra/cli.rb
131
146
  - lib/hedra/config.rb
147
+ - lib/hedra/cors_checker.rb
148
+ - lib/hedra/ct_checker.rb
149
+ - lib/hedra/dns_checker.rb
132
150
  - lib/hedra/exporter.rb
133
151
  - lib/hedra/html_reporter.rb
134
152
  - lib/hedra/http_client.rb
135
153
  - lib/hedra/plugin_manager.rb
136
154
  - lib/hedra/progress_tracker.rb
155
+ - lib/hedra/protocol_checker.rb
137
156
  - lib/hedra/rate_limiter.rb
138
157
  - lib/hedra/scorer.rb
139
158
  - lib/hedra/security_txt_checker.rb
159
+ - lib/hedra/sri_checker.rb
160
+ - lib/hedra/url_validator.rb
140
161
  - lib/hedra/version.rb
141
162
  homepage: https://github.com/bl4ckstack/hedra
142
163
  licenses: