hedra 2.0.3 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 04726510b1778538e154b1eea4d68bbbed1fe306d9e026e73275e6f6cbf586ed
4
- data.tar.gz: 36f7716980ea5fb86bd431c2b6f48ea591b4b9e793557893623d0a4c1f88a6fd
3
+ metadata.gz: b852e94cfa441a1597fb40438491f15939625b1da90734dcb0a05cb3ac06609a
4
+ data.tar.gz: 00bd6a8bccc0be126058ec0b8795f1b193d5e42c642041e2d5415955ccef9e81
5
5
  SHA512:
6
- metadata.gz: d7396aca2eb8bc377f7a203c15729aa48864b3394a79b53b3490c4c2e06719696e313b3ebd097fce4f69b9006c5a44903b8ab94a7dd68da257289e584c811539
7
- data.tar.gz: b50e9f48dda4b48c505ce948a01d3b5b2d5f2a669d459d6398f6c8801ce18bda53edf1da2132b045743e8575225c194fde6e42deb20eb530d5175aa1af6f54a1
6
+ metadata.gz: 908b5f2e81cefb60ee58af16e89e50f092fc7338af99bd89d1b47168a8b0ef1d65044344666f46581246405c291d69fe61a1b2e930234829d48b8003df3bfa65
7
+ data.tar.gz: 309c0cf041eb18ab775696d8b4ca9a8771cf8c57c75709ad86f422c42ee42aebb731f620d2f573ec8edc1f338a4a7d66ee2f8078939f3c75ed33c825c847c550
data/README.md CHANGED
@@ -150,6 +150,30 @@ hedra plugin remove plugin_name
150
150
  - Signature algorithm strength
151
151
  - Key size validation
152
152
  - Chain verification
153
+ - TLS version detection (TLS 1.2/1.3)
154
+ - Certificate Transparency log verification
155
+
156
+ **Protocol Security:**
157
+ - HTTP/2 and HTTP/3 detection
158
+ - TLS version enforcement
159
+ - Insecure protocol warnings
160
+
161
+ **CORS Security:**
162
+ - Access-Control-Allow-Origin validation
163
+ - Wildcard and null origin detection
164
+ - Credentials with wildcard prevention
165
+ - Dangerous HTTP methods detection
166
+ - Sensitive header exposure checks
167
+
168
+ **Subresource Integrity (SRI):**
169
+ - External script/stylesheet SRI validation
170
+ - Crossorigin attribute verification
171
+ - Same-origin resource detection
172
+
173
+ **DNS Security:**
174
+ - DNSSEC validation
175
+ - CAA (Certificate Authority Authorization) records
176
+ - DNS-based security policy enforcement
153
177
 
154
178
  **RFC 9116:**
155
179
  - security.txt file presence and format
@@ -375,6 +399,80 @@ hedra scan -f urls.txt --output report.html --format html
375
399
 
376
400
  Interactive report with sorting, filtering, and charts.
377
401
 
402
+ ## Advanced Security Checks
403
+
404
+ ### Subresource Integrity (SRI)
405
+ Validates that external scripts and stylesheets use SRI attributes to prevent tampering:
406
+ ```bash
407
+ hedra scan https://myapp.com --check-sri
408
+ ```
409
+
410
+ **Checks:**
411
+ - External resources without integrity attributes
412
+ - Missing crossorigin attributes
413
+ - Same-origin vs cross-origin detection
414
+
415
+ ### CORS Policy Analysis
416
+ Validates Cross-Origin Resource Sharing configuration for security issues:
417
+ ```bash
418
+ hedra scan https://api.myapp.com --check-cors
419
+ ```
420
+
421
+ **Detects:**
422
+ - Wildcard origins with credentials (critical vulnerability)
423
+ - Null origin allowance
424
+ - Insecure HTTP origins
425
+ - Dangerous HTTP methods (TRACE, TRACK)
426
+ - Overly permissive configurations
427
+ - Sensitive header exposure
428
+
429
+ ### Protocol Version Detection
430
+ Checks HTTP and TLS protocol versions:
431
+ ```bash
432
+ hedra scan https://myapp.com --check-protocol
433
+ ```
434
+
435
+ **Validates:**
436
+ - HTTP/1.1 vs HTTP/2 vs HTTP/3
437
+ - TLS 1.2/1.3 enforcement
438
+ - Deprecated protocol detection (SSLv3, TLS 1.0/1.1)
439
+ - Protocol upgrade recommendations
440
+
441
+ ### Certificate Transparency
442
+ Verifies certificates are logged in CT logs:
443
+ ```bash
444
+ hedra audit https://myapp.com --check-ct
445
+ ```
446
+
447
+ **Checks:**
448
+ - SCT (Signed Certificate Timestamp) presence
449
+ - Multiple independent CT logs
450
+ - SCT delivery methods (extension, OCSP, TLS)
451
+
452
+ ### DNS Security
453
+ Validates DNS-level security features:
454
+ ```bash
455
+ hedra audit https://myapp.com --check-dns
456
+ ```
457
+
458
+ **Validates:**
459
+ - DNSSEC enablement
460
+ - CAA records for certificate issuance control
461
+ - CAA tags (issue, issuewild, iodef)
462
+ - DNS-based security policies
463
+
464
+ ### Combined Advanced Scan
465
+ Run all advanced checks together:
466
+ ```bash
467
+ hedra audit https://myapp.com \
468
+ --check-sri \
469
+ --check-cors \
470
+ --check-protocol \
471
+ --check-ct \
472
+ --check-dns \
473
+ --output comprehensive-report.json
474
+ ```
475
+
378
476
  ## Real-World Examples
379
477
 
380
478
  ### Basic Security Audit
@@ -382,6 +480,23 @@ Interactive report with sorting, filtering, and charts.
382
480
  hedra scan https://myapp.com
383
481
  ```
384
482
 
483
+ ### Comprehensive Security Audit with All Checks
484
+ ```bash
485
+ hedra audit https://myapp.com \
486
+ --check-sri \
487
+ --check-ct \
488
+ --check-dns \
489
+ --json \
490
+ --output full-audit.json
491
+ ```
492
+
493
+ ### Quick CORS and Protocol Check
494
+ ```bash
495
+ hedra scan https://api.myapp.com \
496
+ --check-cors \
497
+ --check-protocol
498
+ ```
499
+
385
500
  ### Production Deployment Check
386
501
  ```bash
387
502
  # Save baseline after deployment
@@ -503,5 +618,3 @@ hedra scan https://slow-server.com --timeout 60
503
618
  MIT License - see [LICENSE](LICENSE) for details.
504
619
 
505
620
  ---
506
-
507
- **Built by [BlackStack](https://github.com/bl4ckstack)** • Securing the web, one header at a time.
@@ -61,17 +61,22 @@ module Hedra
61
61
  }
62
62
  }.freeze
63
63
 
64
- def initialize(check_certificates: true, check_security_txt: false)
64
+ def initialize(check_certificates: true, check_security_txt: false, check_sri: false, check_cors: true, check_protocol: true, check_ct: false, check_dns: false)
65
65
  @plugin_manager = PluginManager.new
66
66
  @scorer = Scorer.new
67
67
  @certificate_checker = check_certificates ? CertificateChecker.new : nil
68
68
  @security_txt_checker = check_security_txt ? SecurityTxtChecker.new : nil
69
+ @sri_checker = check_sri ? SriChecker.new : nil
70
+ @cors_checker = check_cors ? CorsChecker.new : nil
71
+ @protocol_checker = check_protocol ? ProtocolChecker.new : nil
72
+ @ct_checker = check_ct ? CtChecker.new : nil
73
+ @dns_checker = check_dns ? DnsChecker.new : nil
69
74
  @custom_rules = []
70
75
  @mutex = Mutex.new # Thread safety for custom rules
71
76
  load_custom_rules
72
77
  end
73
78
 
74
- def analyze(url, headers, http_client: nil)
79
+ def analyze(url, headers, http_client: nil, response: nil, html_content: nil)
75
80
  normalized_headers = normalize_headers(headers)
76
81
  findings = []
77
82
 
@@ -104,6 +109,24 @@ module Hedra
104
109
  # Check security.txt
105
110
  findings.concat(@security_txt_checker.check(url, http_client)) if @security_txt_checker && http_client
106
111
 
112
+ # Check Subresource Integrity (SRI)
113
+ if @sri_checker
114
+ @sri_checker.instance_variable_set(:@http_client, http_client) if http_client
115
+ findings.concat(@sri_checker.check(url, html_content))
116
+ end
117
+
118
+ # Check CORS configuration
119
+ findings.concat(@cors_checker.check(normalized_headers, url)) if @cors_checker
120
+
121
+ # Check HTTP/TLS protocol versions
122
+ findings.concat(@protocol_checker.check(url, response)) if @protocol_checker
123
+
124
+ # Check Certificate Transparency
125
+ findings.concat(@ct_checker.check(url)) if @ct_checker
126
+
127
+ # Check DNS security (DNSSEC, CAA)
128
+ findings.concat(@dns_checker.check(url)) if @dns_checker
129
+
107
130
  # Calculate security score
108
131
  score = @scorer.calculate(normalized_headers, findings)
109
132
 
data/lib/hedra/cli.rb CHANGED
@@ -71,7 +71,8 @@ module Hedra
71
71
 
72
72
  urls.each do |url|
73
73
  response = client.get(url)
74
- result = analyzer.analyze(url, response.headers.to_h)
74
+ headers = safe_headers_to_hash(response.headers)
75
+ result = analyzer.analyze(url, headers)
75
76
  current_results << result
76
77
  rescue StandardError => e
77
78
  warn "Failed to scan #{url}: #{e.message}"
@@ -177,6 +178,11 @@ module Hedra
177
178
  option :cache_ttl, type: :numeric, default: 3600, desc: 'Cache TTL in seconds'
178
179
  option :check_certificates, type: :boolean, default: true, desc: 'Check SSL certificates'
179
180
  option :check_security_txt, type: :boolean, default: false, desc: 'Check for security.txt'
181
+ option :check_sri, type: :boolean, default: false, desc: 'Check Subresource Integrity'
182
+ option :check_cors, type: :boolean, default: true, desc: 'Check CORS configuration'
183
+ option :check_protocol, type: :boolean, default: true, desc: 'Check HTTP/TLS protocol versions'
184
+ option :check_ct, type: :boolean, default: false, desc: 'Check Certificate Transparency'
185
+ option :check_dns, type: :boolean, default: false, desc: 'Check DNSSEC and CAA records'
180
186
  option :save_baseline, type: :string, desc: 'Save results as baseline'
181
187
  option :progress, type: :boolean, default: true, desc: 'Show progress bar'
182
188
  def scan(target) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
@@ -186,7 +192,12 @@ module Hedra
186
192
  client = build_http_client
187
193
  analyzer = Analyzer.new(
188
194
  check_certificates: options[:check_certificates],
189
- check_security_txt: options[:check_security_txt]
195
+ check_security_txt: options[:check_security_txt],
196
+ check_sri: options[:check_sri],
197
+ check_cors: options[:check_cors],
198
+ check_protocol: options[:check_protocol],
199
+ check_ct: options[:check_ct],
200
+ check_dns: options[:check_dns]
190
201
  )
191
202
  cache = options[:cache] ? Cache.new(ttl: options[:cache_ttl]) : nil
192
203
  rate_limiter = options[:rate] ? RateLimiter.new(options[:rate]) : nil
@@ -211,7 +222,9 @@ module Hedra
211
222
  log_info("Cache hit: #{url}") if @verbose
212
223
  else
213
224
  response = client.get(url)
214
- result = analyzer.analyze(url, response.headers.to_h, http_client: client)
225
+ headers = safe_headers_to_hash(response.headers)
226
+ html_content = response.body.to_s if options[:check_sri]
227
+ result = analyzer.analyze(url, headers, http_client: client, response: response, html_content: html_content)
215
228
  cache&.set(url, result)
216
229
  end
217
230
 
@@ -246,17 +259,29 @@ module Hedra
246
259
  option :timeout, type: :numeric, aliases: '-t', default: 10, desc: 'Request timeout'
247
260
  option :check_certificates, type: :boolean, default: true, desc: 'Check SSL certificates'
248
261
  option :check_security_txt, type: :boolean, default: true, desc: 'Check for security.txt'
262
+ option :check_sri, type: :boolean, default: true, desc: 'Check Subresource Integrity'
263
+ option :check_cors, type: :boolean, default: true, desc: 'Check CORS configuration'
264
+ option :check_protocol, type: :boolean, default: true, desc: 'Check HTTP/TLS protocol versions'
265
+ option :check_ct, type: :boolean, default: true, desc: 'Check Certificate Transparency'
266
+ option :check_dns, type: :boolean, default: true, desc: 'Check DNSSEC and CAA records'
249
267
  def audit(url)
250
268
  setup_logging
251
269
  client = build_http_client
252
270
  analyzer = Analyzer.new(
253
271
  check_certificates: options[:check_certificates],
254
- check_security_txt: options[:check_security_txt]
272
+ check_security_txt: options[:check_security_txt],
273
+ check_sri: options[:check_sri],
274
+ check_cors: options[:check_cors],
275
+ check_protocol: options[:check_protocol],
276
+ check_ct: options[:check_ct],
277
+ check_dns: options[:check_dns]
255
278
  )
256
279
 
257
280
  begin
258
281
  response = client.get(url)
259
- result = analyzer.analyze(url, response.headers.to_h, http_client: client)
282
+ headers = safe_headers_to_hash(response.headers)
283
+ html_content = response.body.to_s if options[:check_sri]
284
+ result = analyzer.analyze(url, headers, http_client: client, response: response, html_content: html_content)
260
285
 
261
286
  if options[:json]
262
287
  output = JSON.pretty_generate(result)
@@ -289,7 +314,8 @@ module Hedra
289
314
  loop do
290
315
  begin
291
316
  response = client.get(url)
292
- result = analyzer.analyze(url, response.headers.to_h)
317
+ headers = safe_headers_to_hash(response.headers)
318
+ result = analyzer.analyze(url, headers)
293
319
  print_result(result)
294
320
  rescue StandardError => e
295
321
  log_error("Watch check failed: #{e.message}")
@@ -311,8 +337,10 @@ module Hedra
311
337
  response1 = client.get(url1)
312
338
  response2 = client.get(url2)
313
339
 
314
- result1 = analyzer.analyze(url1, response1.headers.to_h)
315
- result2 = analyzer.analyze(url2, response2.headers.to_h)
340
+ headers1 = safe_headers_to_hash(response1.headers)
341
+ headers2 = safe_headers_to_hash(response2.headers)
342
+ result1 = analyzer.analyze(url1, headers1)
343
+ result2 = analyzer.analyze(url2, headers2)
316
344
 
317
345
  print_comparison(result1, result2)
318
346
  rescue StandardError => e
@@ -367,7 +395,8 @@ module Hedra
367
395
 
368
396
  urls.each do |url|
369
397
  response = client.get(url)
370
- result = analyzer.analyze(url, response.headers.to_h, http_client: client)
398
+ headers = safe_headers_to_hash(response.headers)
399
+ result = analyzer.analyze(url, headers, http_client: client)
371
400
  results << result
372
401
 
373
402
  if result[:score] < options[:threshold]
@@ -558,6 +587,21 @@ module Hedra
558
587
  puts Pastel.new.cyan("INFO: #{message}")
559
588
  end
560
589
 
590
+ def safe_headers_to_hash(headers)
591
+ return {} if headers.nil?
592
+
593
+ # Handle different header object types
594
+ hash = headers.respond_to?(:to_h) ? headers.to_h : {}
595
+
596
+ # Clean up nil values
597
+ hash.compact.transform_values do |value|
598
+ value.nil? ? '' : value
599
+ end
600
+ rescue StandardError => e
601
+ log_error("Failed to convert headers: #{e.message}") if @debug
602
+ {}
603
+ end
604
+
561
605
  def severity_badge(severity)
562
606
  pastel = Pastel.new
563
607
  case severity.to_s
@@ -0,0 +1,251 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hedra
4
+ # Validate CORS (Cross-Origin Resource Sharing) headers for security misconfigurations
5
+ class CorsChecker
6
+ DANGEROUS_ORIGINS = [
7
+ '*',
8
+ 'null'
9
+ ].freeze
10
+
11
+ def check(headers, url = nil)
12
+ findings = []
13
+
14
+ # Check Access-Control-Allow-Origin
15
+ if headers.key?('access-control-allow-origin')
16
+ acao = headers['access-control-allow-origin']
17
+ findings.concat(check_allow_origin(acao, headers))
18
+ end
19
+
20
+ # Check Access-Control-Allow-Credentials with wildcard
21
+ if headers.key?('access-control-allow-credentials')
22
+ findings.concat(check_credentials(headers))
23
+ end
24
+
25
+ # Check Access-Control-Allow-Methods
26
+ if headers.key?('access-control-allow-methods')
27
+ findings.concat(check_allow_methods(headers['access-control-allow-methods']))
28
+ end
29
+
30
+ # Check Access-Control-Allow-Headers
31
+ if headers.key?('access-control-allow-headers')
32
+ findings.concat(check_allow_headers(headers['access-control-allow-headers']))
33
+ end
34
+
35
+ # Check Access-Control-Max-Age
36
+ if headers.key?('access-control-max-age')
37
+ findings.concat(check_max_age(headers['access-control-max-age']))
38
+ end
39
+
40
+ # Check for missing CORS headers when others are present
41
+ if cors_headers_present?(headers) && !headers.key?('access-control-allow-origin')
42
+ findings << {
43
+ header: 'access-control-allow-origin',
44
+ issue: 'CORS headers present but Access-Control-Allow-Origin is missing',
45
+ severity: :warning,
46
+ recommended_fix: 'Add Access-Control-Allow-Origin header or remove other CORS headers'
47
+ }
48
+ end
49
+
50
+ findings
51
+ rescue StandardError => e
52
+ warn "CORS check failed: #{e.message}" if ENV['DEBUG']
53
+ []
54
+ end
55
+
56
+ private
57
+
58
+ def check_allow_origin(acao, headers)
59
+ findings = []
60
+
61
+ # Wildcard with credentials is a critical security issue
62
+ if acao == '*' && credentials_enabled?(headers)
63
+ findings << {
64
+ header: 'access-control-allow-origin',
65
+ issue: 'CORS allows all origins (*) with credentials enabled - critical security risk',
66
+ severity: :critical,
67
+ recommended_fix: 'Use specific origin instead of wildcard when credentials are enabled'
68
+ }
69
+ elsif acao == '*'
70
+ findings << {
71
+ header: 'access-control-allow-origin',
72
+ issue: 'CORS allows all origins (*) - potential security risk',
73
+ severity: :warning,
74
+ recommended_fix: 'Restrict to specific trusted origins'
75
+ }
76
+ elsif acao == 'null'
77
+ findings << {
78
+ header: 'access-control-allow-origin',
79
+ issue: 'CORS allows "null" origin - security vulnerability',
80
+ severity: :critical,
81
+ recommended_fix: 'Never allow "null" origin, use specific origins'
82
+ }
83
+ elsif acao.include?(',')
84
+ findings << {
85
+ header: 'access-control-allow-origin',
86
+ issue: 'Multiple origins in Access-Control-Allow-Origin (invalid syntax)',
87
+ severity: :critical,
88
+ recommended_fix: 'Use single origin or implement dynamic origin validation'
89
+ }
90
+ elsif acao.match?(%r{^https?://})
91
+ # Valid origin, check for common issues
92
+ if acao.start_with?('http://') && !acao.include?('localhost') && !acao.include?('127.0.0.1')
93
+ findings << {
94
+ header: 'access-control-allow-origin',
95
+ issue: 'CORS allows insecure HTTP origin',
96
+ severity: :warning,
97
+ recommended_fix: 'Use HTTPS origins only'
98
+ }
99
+ end
100
+ end
101
+
102
+ findings
103
+ end
104
+
105
+ def check_credentials(headers)
106
+ findings = []
107
+ acac = headers['access-control-allow-credentials']
108
+
109
+ if acac.to_s.downcase == 'true'
110
+ acao = headers['access-control-allow-origin']
111
+
112
+ if acao == '*'
113
+ findings << {
114
+ header: 'access-control-allow-credentials',
115
+ issue: 'Credentials enabled with wildcard origin (invalid and dangerous)',
116
+ severity: :critical,
117
+ recommended_fix: 'Use specific origin when credentials are enabled'
118
+ }
119
+ elsif acao.nil?
120
+ findings << {
121
+ header: 'access-control-allow-credentials',
122
+ issue: 'Credentials enabled without Access-Control-Allow-Origin',
123
+ severity: :warning,
124
+ recommended_fix: 'Add Access-Control-Allow-Origin header'
125
+ }
126
+ end
127
+ elsif acac && acac.to_s.downcase != 'true'
128
+ findings << {
129
+ header: 'access-control-allow-credentials',
130
+ issue: 'Invalid Access-Control-Allow-Credentials value (must be "true" or omitted)',
131
+ severity: :warning,
132
+ recommended_fix: 'Set to "true" or remove the header'
133
+ }
134
+ end
135
+
136
+ findings
137
+ end
138
+
139
+ def check_allow_methods(methods)
140
+ findings = []
141
+ method_list = methods.to_s.upcase.split(',').map(&:strip)
142
+
143
+ # Check for overly permissive methods
144
+ dangerous_methods = ['TRACE', 'TRACK', 'CONNECT']
145
+ found_dangerous = method_list & dangerous_methods
146
+
147
+ if found_dangerous.any?
148
+ findings << {
149
+ header: 'access-control-allow-methods',
150
+ issue: "Dangerous HTTP methods allowed: #{found_dangerous.join(', ')}",
151
+ severity: :critical,
152
+ recommended_fix: 'Remove dangerous methods (TRACE, TRACK, CONNECT)'
153
+ }
154
+ end
155
+
156
+ # Check for wildcard
157
+ if method_list.include?('*')
158
+ findings << {
159
+ header: 'access-control-allow-methods',
160
+ issue: 'CORS allows all HTTP methods (*)',
161
+ severity: :warning,
162
+ recommended_fix: 'Specify only required methods (GET, POST, etc.)'
163
+ }
164
+ end
165
+
166
+ # Check for overly permissive DELETE/PUT without proper consideration
167
+ risky_methods = ['DELETE', 'PUT', 'PATCH']
168
+ found_risky = method_list & risky_methods
169
+
170
+ if found_risky.any? && method_list.length > 5
171
+ findings << {
172
+ header: 'access-control-allow-methods',
173
+ issue: "Potentially risky methods allowed: #{found_risky.join(', ')}",
174
+ severity: :info,
175
+ recommended_fix: 'Ensure write methods are properly protected with authentication'
176
+ }
177
+ end
178
+
179
+ findings
180
+ end
181
+
182
+ def check_allow_headers(headers_value)
183
+ findings = []
184
+ header_list = headers_value.to_s.downcase.split(',').map(&:strip)
185
+
186
+ # Check for wildcard
187
+ if header_list.include?('*')
188
+ findings << {
189
+ header: 'access-control-allow-headers',
190
+ issue: 'CORS allows all headers (*)',
191
+ severity: :warning,
192
+ recommended_fix: 'Specify only required headers'
193
+ }
194
+ end
195
+
196
+ # Check for sensitive headers
197
+ sensitive_headers = ['authorization', 'cookie', 'x-api-key', 'x-auth-token']
198
+ found_sensitive = header_list & sensitive_headers
199
+
200
+ if found_sensitive.any?
201
+ findings << {
202
+ header: 'access-control-allow-headers',
203
+ issue: "Sensitive headers exposed: #{found_sensitive.join(', ')}",
204
+ severity: :info,
205
+ recommended_fix: 'Ensure sensitive headers are only allowed for trusted origins'
206
+ }
207
+ end
208
+
209
+ findings
210
+ end
211
+
212
+ def check_max_age(max_age)
213
+ findings = []
214
+ age = max_age.to_i
215
+
216
+ if age <= 0
217
+ findings << {
218
+ header: 'access-control-max-age',
219
+ issue: 'Invalid Access-Control-Max-Age value',
220
+ severity: :info,
221
+ recommended_fix: 'Set to positive number of seconds (e.g., 3600)'
222
+ }
223
+ elsif age > 86400
224
+ findings << {
225
+ header: 'access-control-max-age',
226
+ issue: 'Very long preflight cache duration (>24 hours)',
227
+ severity: :info,
228
+ recommended_fix: 'Consider shorter duration for security updates'
229
+ }
230
+ end
231
+
232
+ findings
233
+ end
234
+
235
+ def credentials_enabled?(headers)
236
+ headers['access-control-allow-credentials'].to_s.downcase == 'true'
237
+ end
238
+
239
+ def cors_headers_present?(headers)
240
+ cors_header_keys = [
241
+ 'access-control-allow-credentials',
242
+ 'access-control-allow-methods',
243
+ 'access-control-allow-headers',
244
+ 'access-control-max-age',
245
+ 'access-control-expose-headers'
246
+ ]
247
+
248
+ cors_header_keys.any? { |key| headers.key?(key) }
249
+ end
250
+ end
251
+ end