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 +4 -4
- data/LICENSE +1 -1
- data/README.md +5 -1
- data/lib/hedra/analyzer.rb +64 -6
- data/lib/hedra/cache.rb +31 -5
- data/lib/hedra/cli.rb +39 -2
- data/lib/hedra/html_reporter.rb +75 -68
- data/lib/hedra/http_client.rb +4 -1
- data/lib/hedra/version.rb +1 -1
- metadata +4 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 29f821e98e18bbcc4bcf7fbc46ee4fa54f064c64b270af72e16311ab41c751e3
|
|
4
|
+
data.tar.gz: bbe84abcc44aa9e0329a5fac5537465fae853b6f29a0b7a07725f3eb2b9f6b65
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1bea6171146693b87f815945d6c572d0336b3035d56eafeb382406f8e74578f408b4c2a1282e6cb3dc2a131d4e03d9606733173f669a5a8f9d1993ae15cb84b5
|
|
7
|
+
data.tar.gz: 0b14d12d73cfa5037ab5481d722ddbf399dbc8a634f9b856748d6b9411d282af66c1ffb14afcafebb7ea695fa92a25dc348bfc793bdee1fba8f04633acfc360b
|
data/LICENSE
CHANGED
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/
|
|
507
|
+
**Built by [BlackStack](https://github.com/bl4ckstack)** • Securing the web, one header at a time.
|
data/lib/hedra/analyzer.rb
CHANGED
|
@@ -117,7 +117,14 @@ module Hedra
|
|
|
117
117
|
private
|
|
118
118
|
|
|
119
119
|
def normalize_headers(headers)
|
|
120
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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)
|
data/lib/hedra/html_reporter.rb
CHANGED
|
@@ -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: #
|
|
19
|
-
.container { max-width:
|
|
20
|
-
.header {
|
|
21
|
-
.header h1 { font-size:
|
|
22
|
-
.header .meta {
|
|
23
|
-
.summary { display: grid; grid-template-columns: repeat(auto-fit, minmax(
|
|
24
|
-
.summary-card {
|
|
25
|
-
.summary-card .
|
|
26
|
-
.summary-card .
|
|
27
|
-
.score-a { color: #
|
|
28
|
-
.score-b { color: #
|
|
29
|
-
.score-c { color: #
|
|
30
|
-
.
|
|
31
|
-
.result-
|
|
32
|
-
.result-header {
|
|
33
|
-
.result-
|
|
34
|
-
.
|
|
35
|
-
.
|
|
36
|
-
.
|
|
37
|
-
.
|
|
38
|
-
.
|
|
39
|
-
.
|
|
40
|
-
.finding
|
|
41
|
-
.finding
|
|
42
|
-
.finding
|
|
43
|
-
.
|
|
44
|
-
.
|
|
45
|
-
.badge
|
|
46
|
-
.badge.
|
|
47
|
-
.
|
|
48
|
-
.
|
|
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
|
|
55
|
-
<div class="meta"
|
|
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
|
|
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">
|
|
65
|
-
<div class="value <%= score_class(avg_score) %>"><%= avg_score.round
|
|
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">
|
|
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
|
|
73
|
-
<div class="value
|
|
79
|
+
<div class="label">Critical</div>
|
|
80
|
+
<div class="value score-c"><%= critical_count %></div>
|
|
74
81
|
</div>
|
|
75
82
|
</div>
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
<div class="result-
|
|
80
|
-
<
|
|
81
|
-
|
|
82
|
-
<span class="score <%= score_class(result[:score]) %>"
|
|
83
|
-
<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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
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
|
-
|
|
109
|
+
<% end %>
|
|
103
110
|
</div>
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
111
|
+
</div>
|
|
112
|
+
<% end %>
|
|
113
|
+
|
|
107
114
|
<div class="footer">
|
|
108
|
-
Generated by Hedra <%= Hedra::VERSION %>
|
|
115
|
+
Generated by Hedra <%= Hedra::VERSION %> · <a href="https://github.com/bl4ckstack/hedra">GitHub</a>
|
|
109
116
|
</div>
|
|
110
117
|
</div>
|
|
111
118
|
</body>
|
data/lib/hedra/http_client.rb
CHANGED
|
@@ -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
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.
|
|
4
|
+
version: 2.0.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
|
-
-
|
|
7
|
+
- bl4ckstack
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2025-11-
|
|
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@
|
|
99
|
+
- info@bl4ckstack.com
|
|
100
100
|
executables:
|
|
101
101
|
- hedra
|
|
102
102
|
extensions: []
|