Class: Hedra::CLI

Inherits:
Thor
  • Object
show all
Defined in:
lib/hedra/cli.rb

Overview

rubocop:disable Metrics/ClassLength

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.exit_on_failure?Boolean



154
155
156
# File 'lib/hedra/cli.rb', line 154

def self.exit_on_failure?
  true
end

Instance Method Details

#audit(url) ⇒ Object



241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
# File 'lib/hedra/cli.rb', line 241

def audit(url)
  setup_logging
  client = build_http_client
  analyzer = Analyzer.new(
    check_certificates: options[:check_certificates],
    check_security_txt: options[:check_security_txt]
  )

  begin
    response = client.get(url)
    result = analyzer.analyze(url, response.headers.to_h, http_client: client)

    if options[:json]
      output = JSON.pretty_generate(result)
      if options[:output]
        File.write(options[:output], output)
        say "Audit saved to #{options[:output]}", :green unless options[:quiet]
      else
        puts output
      end
    else
      print_detailed_result(result)
    end
  rescue StandardError => e
    log_error("Audit failed: #{e.message}")
    exit 1
  end
end

#ci_check(target) ⇒ Object

rubocop:disable Metrics/AbcSize



346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
# File 'lib/hedra/cli.rb', line 346

def ci_check(target) # rubocop:disable Metrics/AbcSize
  setup_logging
  urls = options[:file] ? read_urls_from_file(target) : [target]

  client = build_http_client
  analyzer = Analyzer.new
  results = []
  failed = false

  urls.each do |url|
    response = client.get(url)
    result = analyzer.analyze(url, response.headers.to_h, http_client: client)
    results << result

    if result[:score] < options[:threshold]
      say "FAIL: #{url} - Score #{result[:score]} below threshold #{options[:threshold]}", :red
      failed = true
    end

    if options[:fail_on_critical] && result[:findings].any? { |f| f[:severity] == :critical }
      say "FAIL: #{url} - Critical security issues found", :red
      failed = true
    end
  rescue StandardError => e
    log_error("Failed to check #{url}: #{e.message}")
    failed = true
  end

  # Export results if output specified
  if options[:output]
    exporter = Exporter.new
    exporter.export(results, options[:format], options[:output])
    say "Results exported to #{options[:output]}", :green unless options[:quiet]
  end

  if failed
    say "\nCI check failed", :red
    exit 1
  else
    say "\nCI check passed", :green
    exit 0
  end
end

#compare(url1, url2) ⇒ Object



297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
# File 'lib/hedra/cli.rb', line 297

def compare(url1, url2)
  setup_logging
  client = build_http_client
  analyzer = Analyzer.new

  begin
    response1 = client.get(url1)
    response2 = client.get(url2)

    result1 = analyzer.analyze(url1, response1.headers.to_h)
    result2 = analyzer.analyze(url2, response2.headers.to_h)

    print_comparison(result1, result2)
  rescue StandardError => e
    log_error("Comparison failed: #{e.message}")
    exit 1
  end
end

#export(format) ⇒ Object



319
320
321
322
323
324
325
326
327
328
329
# File 'lib/hedra/cli.rb', line 319

def export(format)
  unless %w[json csv html].include?(format)
    say "Invalid format: #{format}. Use json, csv, or html.", :red
    exit 1
  end

  results = options[:input] ? JSON.parse(File.read(options[:input])) : []
  exporter = Exporter.new
  exporter.export(results, format, options[:output])
  say "Exported to #{options[:output]}", :green unless options[:quiet]
end

#scan(target) ⇒ Object

rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity



174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
# File 'lib/hedra/cli.rb', line 174

def scan(target) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
  setup_logging
  urls = options[:file] ? read_urls_from_file(target) : [target]

  client = build_http_client
  analyzer = Analyzer.new(
    check_certificates: options[:check_certificates],
    check_security_txt: options[:check_security_txt]
  )
  cache = options[:cache] ? Cache.new(ttl: options[:cache_ttl]) : nil
  rate_limiter = options[:rate] ? RateLimiter.new(options[:rate]) : nil
  circuit_breakers = {}
  results = []

  progress = options[:progress] && !options[:quiet] ? ProgressTracker.new(urls.length, quiet: options[:quiet]) : nil

  with_concurrency(urls, options[:concurrency]) do |url|
    rate_limiter&.acquire

    # Get or create circuit breaker for this domain
    domain = URI.parse(url).host
    circuit_breakers[domain] ||= CircuitBreaker.new

    begin
      circuit_breakers[domain].call do
        # Check cache first
        cached = cache&.get(url)
        if cached
          result = cached
          log_info("Cache hit: #{url}") if @verbose
        else
          response = client.get(url)
          result = analyzer.analyze(url, response.headers.to_h, http_client: client)
          cache&.set(url, result)
        end

        results << result
        print_result(result) unless options[:quiet] || options[:output]
      end
    rescue CircuitOpenError
      log_error("Circuit breaker open for #{domain}, skipping #{url}")
    rescue StandardError => e
      log_error("Failed to scan #{url}: #{e.message}")
    ensure
      progress&.increment
    end
  end

  progress&.finish

  if options[:save_baseline]
    baseline = Baseline.new
    baseline.save(options[:save_baseline], results)
    say "Baseline saved: #{options[:save_baseline]}", :green unless options[:quiet]
  end

  export_results(results) if options[:output]
end

#watch(url) ⇒ Object



274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
# File 'lib/hedra/cli.rb', line 274

def watch(url)
  setup_logging
  client = build_http_client
  analyzer = Analyzer.new

  say "Watching #{url} every #{options[:interval]} seconds. Press Ctrl+C to stop.", :cyan

  loop do
    begin
      response = client.get(url)
      result = analyzer.analyze(url, response.headers.to_h)
      print_result(result)
    rescue StandardError => e
      log_error("Watch check failed: #{e.message}")
    end
    sleep options[:interval]
  end
rescue Interrupt
  say "\nStopped watching.", :yellow
end