#!/usr/bin/env ruby
# frozen_string_literal: true

# Ruby internal
# Project internal
require 'tls_map'
require 'tls_map/cli/cli'
# External
require 'docopt'
require 'paint'

# can't specify 2 options with the same name even if used in different commands
# https://github.com/docopt/docopt/issues/296#issuecomment-857477191
doc = <<~DOCOPT
  #{Paint['TLS map', :bold, '#81c8b6']} #{Paint[TLSmap::VERSION, :bold]}

  #{Paint['Usage:', '#81c8b6']}
    tls-map search <criteria> <term> [-o <output> --force -e -a] [--no-color --debug]
    tls-map bulk <criteria> <file> [(-q <output> | --audit) --force] [--no-color --debug]
    tls-map export <filename> <format> [--force] [--debug]
    tls-map extract <filename> <format> [--no-color --debug [--only-weak | --hide-weak]]
    tls-map update [--with-extended] [--debug]
    tls-map -h | --help
    tls-map --version

  #{Paint['Search options:', '#81c8b6']} #{Paint['(offline) search and translate cipher names between SSL/TLS libraries', :underline]}
    <criteria>              The type of term. Accepted values: codepoint, iana, openssl, gnutls, nss.
    <term>                  The cipher algorithm name.
    -o, --output <output>   Displayed fields. Accepted values: all, codepoint, iana, openssl, gnutls, nss. [default: all]
    -e, --extended          (Online) Display additional information about the cipher (requires output = all or iana)
    -a, --acronym           (Online) Display full acronym name (requires -e / --extended option)

  #{Paint['Bulk options:', '#81c8b6']} #{Paint['(offline) search and translate cipher names between SSL/TLS libraries in bulk', :underline]}
    <criteria>              The type of term. Accepted values: codepoint, iana, openssl, gnutls, nss.
    <file>                  File containing the cipher algorithm names, one per line.
    -q, --output2 <output>  Displayed fields. Accepted values: codepoint, iana, openssl, gnutls, nss. [default: iana]
    --audit                 Highlight weak (security level equal to weak or insecure) cipher suites. (work only with TLS not SSL).

  #{Paint['Export options:', '#81c8b6']} #{Paint['(offline) export the list of all ciphers (mapping) in various formats', :underline]}
    <filename>              The output file name to write to.
    <format>                Supported formats: markdown (a markdown table), json_pretty (expanded JSON), json_compact (minified JSON), marshal (Ruby marshalized hash).

  #{Paint['Extract options:', '#81c8b6']} #{Paint['(offline) extract ciphers from external tools output file', :underline]}
    <filename>              The external tool output file
    <format>                Supported formats: sslyze, sslscan2, testssl, ssllabs-scan, tlsx (check the documentation for the expected file format)
    --only-weak             Show only ciphers with a security level equal to weak or insecure (hide secure and recommended) (work only with TLS not SSL).
    --hide-weak             Hide ciphers with a security level equal to weak or insecure (show only secure and recommended) (work only with TLS not SSL).

  #{Paint['Update options:', '#81c8b6']} #{Paint['(online) DANGEROUS, will break database integrity, force option will be required', :underline]}
    --with-extended         (Online) Also save extended information used by search --extended option.

  #{Paint['Other options:', '#81c8b6']}
    --force     Force parsing even if integrity check failed (DANGEROUS, may result in command execution vulnerability)
    --no-color  Disable colorized output
    --debug     Display arguments
    -h, --help  Show this screen
    --version   Show version
DOCOPT

begin
  args = Docopt.docopt(doc, version: TLSmap::VERSION)
  Paint.mode = 0 if args['--no-color']
  puts args if args['--debug']
  if args['search']
    cli = TLSmap::CLI.new(args['--force'])
    res = cli.search(args['<criteria>'].to_sym, args['<term>'], args['--output'].to_sym)
    puts Paint['No match found', :red] if res.empty?
    res.each do |k, v|
      puts "#{Paint[k, :green]}: #{Paint[v, :white]}"
    end
    if args['--extended']
      tmext = TLSmap::App::Extended
      tmext_i = tmext.new
      ext = tmext_i.extend(res[:iana])
      dic = tmext::DICO
      sev = tmext::VULN_SEVERITY
      sec_lvl = tmext::SECURITY_LEVEL
      ext.each do |k, v|
        case k
        when 'vulns'
          puts "#{Paint[dic[k], :magenta]}:"
          v.each do |vuln|
            print "  - #{Paint[sev[vuln[:severity]][:title], sev[vuln[:severity]][:color]]} - "
            puts Paint[vuln[:description], :white]
          end
        when 'tls_version'
          puts "#{Paint[dic[k], :magenta]}: #{Paint[v.join(', '), :white]}"
        when 'security'
          puts "#{Paint[dic[k], :magenta]}: #{Paint[v, sec_lvl[v][:color]]}"
        else
          print "#{Paint[dic[k], :magenta]}: #{Paint[v, :white]}"
          print " (#{tmext_i.translate_acronym(v)})" if args['--acronym'] && !tmext_i.translate_acronym(v).nil?
          puts
        end
      end
    end
  elsif args['bulk']
    cli = TLSmap::CLI.new(args['--force'])
    res = cli.bulk_search(args['<criteria>'].to_sym, args['<file>'], args['--output2'].to_sym)
    puts Paint['No match found', :red] if res.empty?
    res.each do |h|
      cs = h[args['--output2'].to_sym] # cipher suite
      next if cs.nil?

      if args['--audit']
        cliext = TLSmap::CLI::Extended.new
        ci = TLSmap::App::Cipher.new(:iana, cs, enhanced_data: cliext.enhanced_data)
        if ci.should_i_use?
          print Paint[cs, :green]
        else
          print Paint[cs, :red]
          print ' -- '
          print Paint['weak', :red, :bold]
        end
        puts
      else
        puts Paint[cs, :green]
      end
    end
  elsif args['export']
    cli = TLSmap::CLI.new(args['--force'])
    cli.export(args['<filename>'], args['<format>'].to_sym)
    puts "#{args['<filename>']} exported"
  elsif args['extract']
    extractor = TLSmap::App::Extractor.new
    ciphers = extractor.parse(args['<format>'], args['<filename>'])
    ciphers.each do |k, v|
      if args['--only-weak'] || args['--hide-weak']
        cliext = TLSmap::CLI::Extended.new
        v.each do |alg|
          ci = TLSmap::App::Cipher.new(:iana, alg, enhanced_data: cliext.enhanced_data)
          next unless (args['--only-weak'] && !ci.should_i_use?) ||
                      (args['--hide-weak'] && ci.should_i_use?)

          print Paint["#{k} ", :blue] unless v.empty?
          puts Paint[alg, :white]
        end
      else
        puts Paint[k, :blue] unless v.empty?
        puts Paint[v.join("\n"), :white] unless v.empty?
      end
    end
  elsif args['update']
    cli = TLSmap::CLI.new
    cli.update
    if args['--with-extended']
      cliext = TLSmap::CLI::Extended.new
      cliext.update
    end
    puts 'Database(s) updated'
  end
rescue Docopt::Exit => e
  puts e.message
end
