hedra 2.0.0 → 2.0.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 124399236bba4ce146d076b83f5d47cfd4fd646f5ced61096a01efe0a9e80333
4
- data.tar.gz: ae5c000c2421d787a9e6c28c6574aef21555bb1bbffbe3e1ae1ccd8169478bf9
3
+ metadata.gz: 29f821e98e18bbcc4bcf7fbc46ee4fa54f064c64b270af72e16311ab41c751e3
4
+ data.tar.gz: bbe84abcc44aa9e0329a5fac5537465fae853b6f29a0b7a07725f3eb2b9f6b65
5
5
  SHA512:
6
- metadata.gz: 0cffa289a6c2d3413f118f160a6f57badab79b68bf2333573c9efd6e830e1970f33afcf0d24aa6f27740f50f03d45c65275b4c2951826bc8d3df081f4bd197f4
7
- data.tar.gz: 02b4706392f72c1a42883eeb8dec92d7b7776856806fbab65aa4e7283c967be679ff471fb2e8da8677901f00db47be15afe9fb3fc0836ef275e05e3d38462a2b
6
+ metadata.gz: 1bea6171146693b87f815945d6c572d0336b3035d56eafeb382406f8e74578f408b4c2a1282e6cb3dc2a131d4e03d9606733173f669a5a8f9d1993ae15cb84b5
7
+ data.tar.gz: 0b14d12d73cfa5037ab5481d722ddbf399dbc8a634f9b856748d6b9411d282af66c1ffb14afcafebb7ea695fa92a25dc348bfc793bdee1fba8f04633acfc360b
data/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2025 BlackStack
3
+ Copyright (c) 2025 bl4ckstack
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -7,6 +7,10 @@
7
7
 
8
8
  > Security header analyzer with SSL/TLS validation, baseline tracking, and CI/CD integration.
9
9
 
10
+ <p align="center">
11
+ <img src="logo.png" width="380" alt="Hedra Logo"/>
12
+ </p>
13
+
10
14
  ## Installation
11
15
  ```bash
12
16
  gem install hedra
@@ -500,4 +504,4 @@ MIT License - see [LICENSE](LICENSE) for details.
500
504
 
501
505
  ---
502
506
 
503
- **Built by [BlackStack](https://github.com/blackstack)** • Securing the web, one header at a time.
507
+ **Built by [BlackStack](https://github.com/bl4ckstack)** • Securing the web, one header at a time.
@@ -117,7 +117,14 @@ module Hedra
117
117
  private
118
118
 
119
119
  def normalize_headers(headers)
120
- headers.transform_keys { |k| k.to_s.downcase }
120
+ normalized = {}
121
+ headers.each do |key, value|
122
+ normalized_key = key.to_s.downcase
123
+ # Handle array values (multiple header values)
124
+ normalized_value = value.is_a?(Array) ? value.join(', ') : value.to_s
125
+ normalized[normalized_key] = normalized_value
126
+ end
127
+ normalized
121
128
  end
122
129
 
123
130
  def validate_header_values(headers)
@@ -125,7 +132,7 @@ module Hedra
125
132
 
126
133
  # Validate CSP
127
134
  if headers['content-security-policy']
128
- csp = headers['content-security-policy']
135
+ csp = headers['content-security-policy'].to_s.downcase
129
136
  if csp.include?('unsafe-inline') || csp.include?('unsafe-eval')
130
137
  findings << {
131
138
  header: 'content-security-policy',
@@ -134,11 +141,21 @@ module Hedra
134
141
  recommended_fix: 'Remove unsafe-inline and unsafe-eval, use nonces or hashes'
135
142
  }
136
143
  end
144
+
145
+ # Check for wildcard sources
146
+ if csp =~ /\*(?!\.)/ && !csp.include?("'unsafe-inline'")
147
+ findings << {
148
+ header: 'content-security-policy',
149
+ issue: 'CSP uses wildcard (*) source',
150
+ severity: :info,
151
+ recommended_fix: 'Restrict sources to specific domains'
152
+ }
153
+ end
137
154
  end
138
155
 
139
156
  # Validate HSTS
140
157
  if headers['strict-transport-security']
141
- hsts = headers['strict-transport-security']
158
+ hsts = headers['strict-transport-security'].to_s
142
159
  if hsts =~ /max-age=(\d+)/
143
160
  max_age = ::Regexp.last_match(1).to_i
144
161
  if max_age < 31_536_000
@@ -149,12 +166,29 @@ module Hedra
149
166
  recommended_fix: 'Set max-age to at least 31536000'
150
167
  }
151
168
  end
169
+ else
170
+ findings << {
171
+ header: 'strict-transport-security',
172
+ issue: 'HSTS header missing max-age directive',
173
+ severity: :critical,
174
+ recommended_fix: 'Add max-age directive with value >= 31536000'
175
+ }
176
+ end
177
+
178
+ # Check for includeSubDomains
179
+ unless hsts.downcase.include?('includesubdomains')
180
+ findings << {
181
+ header: 'strict-transport-security',
182
+ issue: 'HSTS missing includeSubDomains directive',
183
+ severity: :info,
184
+ recommended_fix: 'Add includeSubDomains to protect all subdomains'
185
+ }
152
186
  end
153
187
  end
154
188
 
155
189
  # Validate X-Frame-Options
156
190
  if headers['x-frame-options']
157
- xfo = headers['x-frame-options'].upcase
191
+ xfo = headers['x-frame-options'].to_s.upcase
158
192
  unless %w[DENY SAMEORIGIN].include?(xfo.split.first)
159
193
  findings << {
160
194
  header: 'x-frame-options',
@@ -167,7 +201,7 @@ module Hedra
167
201
 
168
202
  # Validate X-Content-Type-Options
169
203
  if headers['x-content-type-options']
170
- xcto = headers['x-content-type-options'].downcase
204
+ xcto = headers['x-content-type-options'].to_s.downcase.strip
171
205
  unless xcto == 'nosniff'
172
206
  findings << {
173
207
  header: 'x-content-type-options',
@@ -177,6 +211,25 @@ module Hedra
177
211
  }
178
212
  end
179
213
  end
214
+
215
+ # Check for information disclosure headers
216
+ if headers['server']
217
+ findings << {
218
+ header: 'server',
219
+ issue: 'Server header exposes server information',
220
+ severity: :info,
221
+ recommended_fix: 'Remove or obfuscate Server header'
222
+ }
223
+ end
224
+
225
+ if headers['x-powered-by']
226
+ findings << {
227
+ header: 'x-powered-by',
228
+ issue: 'X-Powered-By header exposes technology stack',
229
+ severity: :info,
230
+ recommended_fix: 'Remove X-Powered-By header'
231
+ }
232
+ end
180
233
 
181
234
  findings
182
235
  end
@@ -186,8 +239,13 @@ module Hedra
186
239
  config_path = File.expand_path('~/.hedra/rules.yml')
187
240
  return unless File.exist?(config_path)
188
241
 
189
- rules = YAML.load_file(config_path)
242
+ content = File.read(config_path)
243
+ return if content.strip.empty?
244
+
245
+ rules = YAML.safe_load(content, permitted_classes: [Symbol])
190
246
  @custom_rules = rules['rules'] || []
247
+ rescue Psych::SyntaxError => e
248
+ warn "Invalid YAML in rules file: #{e.message}"
191
249
  rescue StandardError => e
192
250
  warn "Failed to load custom rules: #{e.message}"
193
251
  end
data/lib/hedra/cache.rb CHANGED
@@ -8,11 +8,14 @@ module Hedra
8
8
  # Simple file-based cache for HTTP responses
9
9
  class Cache
10
10
  DEFAULT_TTL = 3600 # 1 hour
11
+ MAX_CACHE_SIZE = 1000 # Maximum number of cache files
11
12
 
12
- def initialize(cache_dir: nil, ttl: DEFAULT_TTL)
13
+ def initialize(cache_dir: nil, ttl: DEFAULT_TTL, verbose: false)
13
14
  @cache_dir = cache_dir || File.join(Config::CONFIG_DIR, 'cache')
14
15
  @ttl = ttl
16
+ @verbose = verbose
15
17
  FileUtils.mkdir_p(@cache_dir)
18
+ cleanup_if_needed
16
19
  end
17
20
 
18
21
  def get(key)
@@ -20,11 +23,19 @@ module Hedra
20
23
  return nil unless File.exist?(cache_file)
21
24
 
22
25
  data = JSON.parse(File.read(cache_file))
23
- return nil if expired?(data['timestamp'])
26
+
27
+ if expired?(data['timestamp'])
28
+ File.delete(cache_file) # Clean up expired file immediately
29
+ return nil
30
+ end
24
31
 
25
32
  data['value']
33
+ rescue JSON::ParserError
34
+ # Corrupted cache file, delete it
35
+ File.delete(cache_file) if File.exist?(cache_file)
36
+ nil
26
37
  rescue StandardError => e
27
- warn "Cache read error: #{e.message}"
38
+ warn "Cache read error: #{e.message}" if @verbose
28
39
  nil
29
40
  end
30
41
 
@@ -34,9 +45,14 @@ module Hedra
34
45
  'timestamp' => Time.now.to_i,
35
46
  'value' => value
36
47
  }
37
- File.write(cache_file, JSON.generate(data))
48
+
49
+ # Atomic write to prevent corruption
50
+ temp_file = "#{cache_file}.tmp"
51
+ File.write(temp_file, JSON.generate(data))
52
+ File.rename(temp_file, cache_file)
38
53
  rescue StandardError => e
39
- warn "Cache write error: #{e.message}"
54
+ warn "Cache write error: #{e.message}" if @verbose
55
+ File.delete(temp_file) if File.exist?(temp_file)
40
56
  end
41
57
 
42
58
  def clear
@@ -63,5 +79,15 @@ module Hedra
63
79
  def expired?(timestamp)
64
80
  (Time.now.to_i - timestamp) > @ttl
65
81
  end
82
+
83
+ def cleanup_if_needed
84
+ cache_files = Dir.glob(File.join(@cache_dir, '*'))
85
+ return if cache_files.length < MAX_CACHE_SIZE
86
+
87
+ # Remove oldest files if cache is too large
88
+ cache_files.sort_by { |f| File.mtime(f) }
89
+ .first(cache_files.length - MAX_CACHE_SIZE + 100)
90
+ .each { |f| File.delete(f) rescue nil }
91
+ end
66
92
  end
67
93
  end
data/lib/hedra/cli.rb CHANGED
@@ -406,22 +406,59 @@ module Hedra
406
406
  end
407
407
 
408
408
  def read_urls_from_file(file)
409
- File.readlines(file).map(&:strip).reject(&:empty?)
409
+ unless File.exist?(file)
410
+ log_error("File not found: #{file}")
411
+ exit 1
412
+ end
413
+
414
+ urls = File.readlines(file).map(&:strip).reject(&:empty?)
415
+
416
+ if urls.empty?
417
+ log_error("No URLs found in file: #{file}")
418
+ exit 1
419
+ end
420
+
421
+ # Validate URLs
422
+ invalid_urls = urls.reject { |url| valid_url?(url) }
423
+ if invalid_urls.any?
424
+ log_error("Invalid URLs found: #{invalid_urls.join(', ')}")
425
+ exit 1
426
+ end
427
+
428
+ urls
410
429
  rescue StandardError => e
411
430
  log_error("Failed to read file #{file}: #{e.message}")
412
431
  exit 1
413
432
  end
414
433
 
434
+ def valid_url?(url)
435
+ uri = URI.parse(url)
436
+ uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS)
437
+ rescue URI::InvalidURIError
438
+ false
439
+ end
440
+
415
441
  def with_concurrency(items, concurrency)
416
442
  require 'concurrent'
417
443
  pool = Concurrent::FixedThreadPool.new(concurrency)
444
+ mutex = Mutex.new
445
+ errors = []
418
446
 
419
447
  items.each do |item|
420
- pool.post { yield item }
448
+ pool.post do
449
+ yield item
450
+ rescue StandardError => e
451
+ mutex.synchronize { errors << { item: item, error: e } }
452
+ end
421
453
  end
422
454
 
423
455
  pool.shutdown
424
456
  pool.wait_for_termination
457
+
458
+ # Log any errors that occurred
459
+ errors.each do |err|
460
+ log_error("Error processing #{err[:item]}: #{err[:error].message}")
461
+ end
425
462
  end
426
463
 
427
464
  def print_result(result)
@@ -15,97 +15,104 @@ module Hedra
15
15
  <title>Hedra Security Report</title>
16
16
  <style>
17
17
  * { margin: 0; padding: 0; box-sizing: border-box; }
18
- body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; padding: 20px; }
19
- .container { max-width: 1200px; margin: 0 auto; background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
20
- .header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 30px; border-radius: 8px 8px 0 0; }
21
- .header h1 { font-size: 32px; margin-bottom: 10px; }
22
- .header .meta { opacity: 0.9; font-size: 14px; }
23
- .summary { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; padding: 30px; border-bottom: 1px solid #eee; }
24
- .summary-card { text-align: center; padding: 20px; background: #f9f9f9; border-radius: 8px; }
25
- .summary-card .value { font-size: 36px; font-weight: bold; margin: 10px 0; }
26
- .summary-card .label { color: #666; font-size: 14px; }
27
- .score-a { color: #10b981; }
28
- .score-b { color: #f59e0b; }
29
- .score-c { color: #ef4444; }
30
- .results { padding: 30px; }
31
- .result-item { margin-bottom: 30px; border: 1px solid #eee; border-radius: 8px; overflow: hidden; }
32
- .result-header { background: #f9f9f9; padding: 20px; border-bottom: 1px solid #eee; }
33
- .result-header h2 { font-size: 18px; margin-bottom: 10px; word-break: break-all; }
34
- .result-header .score { display: inline-block; padding: 5px 15px; border-radius: 20px; font-weight: bold; font-size: 14px; }
35
- .result-body { padding: 20px; }
36
- .finding { padding: 15px; margin-bottom: 10px; border-left: 4px solid; border-radius: 4px; background: #f9f9f9; }
37
- .finding.critical { border-color: #ef4444; background: #fef2f2; }
38
- .finding.warning { border-color: #f59e0b; background: #fffbeb; }
39
- .finding.info { border-color: #3b82f6; background: #eff6ff; }
40
- .finding-header { font-weight: bold; margin-bottom: 5px; }
41
- .finding-issue { margin-bottom: 5px; }
42
- .finding-fix { font-size: 14px; color: #666; font-style: italic; }
43
- .badge { display: inline-block; padding: 3px 8px; border-radius: 3px; font-size: 12px; font-weight: bold; text-transform: uppercase; }
44
- .badge.critical { background: #ef4444; color: white; }
45
- .badge.warning { background: #f59e0b; color: white; }
46
- .badge.info { background: #3b82f6; color: white; }
47
- .no-findings { text-align: center; padding: 40px; color: #10b981; font-size: 18px; }
48
- .footer { text-align: center; padding: 20px; color: #666; font-size: 14px; border-top: 1px solid #eee; }
18
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #fafafa; color: #1a1a1a; line-height: 1.6; }
19
+ .container { max-width: 900px; margin: 40px auto; padding: 0 20px; }
20
+ .header { margin-bottom: 48px; }
21
+ .header h1 { font-size: 28px; font-weight: 600; margin-bottom: 8px; letter-spacing: -0.5px; }
22
+ .header .meta { color: #666; font-size: 14px; }
23
+ .summary { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 16px; margin-bottom: 48px; }
24
+ .summary-card { background: white; border: 1px solid #e5e5e5; border-radius: 6px; padding: 20px; }
25
+ .summary-card .label { color: #666; font-size: 13px; margin-bottom: 8px; text-transform: uppercase; letter-spacing: 0.5px; }
26
+ .summary-card .value { font-size: 32px; font-weight: 600; }
27
+ .score-a { color: #16a34a; }
28
+ .score-b { color: #ea580c; }
29
+ .score-c { color: #dc2626; }
30
+ .result-item { background: white; border: 1px solid #e5e5e5; border-radius: 6px; margin-bottom: 24px; overflow: hidden; }
31
+ .result-header { padding: 24px; border-bottom: 1px solid #e5e5e5; }
32
+ .result-header h2 { font-size: 16px; font-weight: 500; margin-bottom: 12px; word-break: break-all; color: #1a1a1a; }
33
+ .result-meta { display: flex; align-items: center; gap: 16px; font-size: 14px; }
34
+ .score-badge { display: inline-flex; align-items: center; padding: 4px 12px; border-radius: 4px; font-weight: 500; font-size: 14px; }
35
+ .score-badge.score-a { background: #dcfce7; color: #166534; }
36
+ .score-badge.score-b { background: #fed7aa; color: #9a3412; }
37
+ .score-badge.score-c { background: #fee2e2; color: #991b1b; }
38
+ .timestamp { color: #666; }
39
+ .result-body { padding: 24px; }
40
+ .finding { padding: 16px; margin-bottom: 12px; border-radius: 4px; border-left: 3px solid; }
41
+ .finding.critical { border-color: #dc2626; background: #fef2f2; }
42
+ .finding.warning { border-color: #ea580c; background: #fff7ed; }
43
+ .finding.info { border-color: #2563eb; background: #eff6ff; }
44
+ .finding-header { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
45
+ .severity-badge { display: inline-block; padding: 2px 8px; border-radius: 3px; font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; }
46
+ .severity-badge.critical { background: #dc2626; color: white; }
47
+ .severity-badge.warning { background: #ea580c; color: white; }
48
+ .severity-badge.info { background: #2563eb; color: white; }
49
+ .header-name { font-family: 'Courier New', monospace; font-size: 13px; color: #666; }
50
+ .finding-issue { font-size: 14px; margin-bottom: 8px; color: #1a1a1a; }
51
+ .finding-fix { font-size: 13px; color: #666; padding-left: 16px; border-left: 2px solid #e5e5e5; }
52
+ .no-findings { text-align: center; padding: 32px; color: #16a34a; font-size: 15px; }
53
+ .footer { text-align: center; padding: 32px 0; color: #999; font-size: 13px; border-top: 1px solid #e5e5e5; margin-top: 48px; }
54
+ .footer a { color: #666; text-decoration: none; }
55
+ .footer a:hover { color: #1a1a1a; }
49
56
  </style>
50
57
  </head>
51
58
  <body>
52
59
  <div class="container">
53
60
  <div class="header">
54
- <h1>🛡️ Hedra Security Report</h1>
55
- <div class="meta">Generated: <%= Time.now.strftime('%Y-%m-%d %H:%M:%S %Z') %></div>
61
+ <h1>Security Report</h1>
62
+ <div class="meta"><%= Time.now.strftime('%B %d, %Y at %H:%M %Z') %></div>
56
63
  </div>
57
- #{' '}
64
+
58
65
  <div class="summary">
59
66
  <div class="summary-card">
60
- <div class="label">URLs Scanned</div>
67
+ <div class="label">URLs</div>
61
68
  <div class="value"><%= results.length %></div>
62
69
  </div>
63
70
  <div class="summary-card">
64
- <div class="label">Average Score</div>
65
- <div class="value <%= score_class(avg_score) %>"><%= avg_score.round %>/100</div>
71
+ <div class="label">Avg Score</div>
72
+ <div class="value <%= score_class(avg_score) %>"><%= avg_score.round %></div>
66
73
  </div>
67
74
  <div class="summary-card">
68
- <div class="label">Total Findings</div>
75
+ <div class="label">Findings</div>
69
76
  <div class="value"><%= total_findings %></div>
70
77
  </div>
71
78
  <div class="summary-card">
72
- <div class="label">Critical Issues</div>
73
- <div class="value" style="color: #ef4444;"><%= critical_count %></div>
79
+ <div class="label">Critical</div>
80
+ <div class="value score-c"><%= critical_count %></div>
74
81
  </div>
75
82
  </div>
76
- #{' '}
77
- <div class="results">
78
- <% results.each do |result| %>
79
- <div class="result-item">
80
- <div class="result-header">
81
- <h2><%= result[:url] %></h2>
82
- <span class="score <%= score_class(result[:score]) %>">Score: <%= result[:score] %>/100</span>
83
- <span style="color: #666; margin-left: 15px; font-size: 14px;"><%= result[:timestamp] %></span>
83
+
84
+ <% results.each do |result| %>
85
+ <div class="result-item">
86
+ <div class="result-header">
87
+ <h2><%= result[:url] %></h2>
88
+ <div class="result-meta">
89
+ <span class="score-badge <%= score_class(result[:score]) %>"><%= result[:score] %>/100</span>
90
+ <span class="timestamp"><%= result[:timestamp] %></span>
84
91
  </div>
85
- <div class="result-body">
86
- <% if result[:findings].empty? %>
87
- <div class="no-findings">✓ All security headers properly configured</div>
88
- <% else %>
89
- <% result[:findings].each do |finding| %>
90
- <div class="finding <%= finding[:severity] %>">
91
- <div class="finding-header">
92
- <span class="badge <%= finding[:severity] %>"><%= finding[:severity] %></span>
93
- <%= finding[:header] %>
94
- </div>
95
- <div class="finding-issue"><%= finding[:issue] %></div>
96
- <% if finding[:recommended_fix] %>
97
- <div class="finding-fix">💡 <%= finding[:recommended_fix] %></div>
98
- <% end %>
92
+ </div>
93
+ <div class="result-body">
94
+ <% if result[:findings].empty? %>
95
+ <div class="no-findings">✓ All security headers properly configured</div>
96
+ <% else %>
97
+ <% result[:findings].each do |finding| %>
98
+ <div class="finding <%= finding[:severity] %>">
99
+ <div class="finding-header">
100
+ <span class="severity-badge <%= finding[:severity] %>"><%= finding[:severity] %></span>
101
+ <span class="header-name"><%= finding[:header] %></span>
99
102
  </div>
100
- <% end %>
103
+ <div class="finding-issue"><%= finding[:issue] %></div>
104
+ <% if finding[:recommended_fix] %>
105
+ <div class="finding-fix"><%= finding[:recommended_fix] %></div>
106
+ <% end %>
107
+ </div>
101
108
  <% end %>
102
- </div>
109
+ <% end %>
103
110
  </div>
104
- <% end %>
105
- </div>
106
- #{' '}
111
+ </div>
112
+ <% end %>
113
+
107
114
  <div class="footer">
108
- Generated by Hedra <%= Hedra::VERSION %> | <a href="https://github.com/blackstack/hedra">GitHub</a>
115
+ Generated by Hedra <%= Hedra::VERSION %> · <a href="https://github.com/bl4ckstack/hedra">GitHub</a>
109
116
  </div>
110
117
  </div>
111
118
  </body>
@@ -94,9 +94,12 @@ module Hedra
94
94
  def retryable_error?(error)
95
95
  # Retry on network errors, timeouts, but not on HTTP errors like 404
96
96
  error.is_a?(HTTP::TimeoutError) ||
97
+ error.is_a?(HTTP::ConnectionError) ||
97
98
  error.is_a?(Errno::ECONNREFUSED) ||
98
99
  error.is_a?(Errno::ETIMEDOUT) ||
99
- error.is_a?(Errno::EHOSTUNREACH)
100
+ error.is_a?(Errno::EHOSTUNREACH) ||
101
+ error.is_a?(Errno::ECONNRESET) ||
102
+ error.is_a?(Errno::EPIPE)
100
103
  end
101
104
 
102
105
  def log(message)
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.0'
4
+ VERSION = '2.0.1'
5
5
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hedra
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0
4
+ version: 2.0.1
5
5
  platform: ruby
6
6
  authors:
7
- - BlackStack
7
+ - bl4ckstack
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-11-19 00:00:00.000000000 Z
10
+ date: 2025-11-20 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: concurrent-ruby
@@ -96,7 +96,7 @@ dependencies:
96
96
  description: A comprehensive security header analyzer with scanning, auditing, and
97
97
  monitoring capabilities
98
98
  email:
99
- - info@blackstack.com
99
+ - info@bl4ckstack.com
100
100
  executables:
101
101
  - hedra
102
102
  extensions: []