# frozen_string_literal: true
require 'thor'
require 'git'
require 'docker'
require 'norad_cli/support/api_security_container_seed_script'
require 'rspec'

class Sectest < Thor
  include Thor::Actions

  def self.source_root
    File.join(File.dirname(File.expand_path(__FILE__)), '../templates/')
  end

  desc 'scaffold TESTNAME', 'Create a new security test with standard files + testing'
  option :test_type, aliases: '-t', default: 'whole_host', desc: 'The security test type, Options: [authenticated|web_application|brute_force|ssl_crypto|ssh_crypto|whole_host]'
  option :registry, aliases: '-r', default: 'norad-registry.cisco.com:5000', desc: 'The Docker registry to store docker images'
  option :version, aliases: '-v', default: 'latest', desc: 'The version of the security test'
  option :base_image, aliases: '-b', default: 'norad-registry.cisco.com:5000/norad:0.0.1', desc: 'Base Docker image to use (i.e. FROM field in the Dockerfile)'
  option :configurable, type: :boolean, aliases: '-c', desc: 'Is the security test configurable (e.g. Qualys username and password)'
  def scaffold(sectest_name)
    # Grab the current directory
    repo_dir = Dir.pwd

    # Check for the root_dir
    root_dir?

    # Check for valid test types
    if !%w(authenticated web_application brute_force ssl_crypto ssh_crypto whole_host).include?(options[:test_type])
      say("#{options[:test_type]} is not a supported test type", :red)
      say('Exiting...', :red)
      exit(1)
    end

    # Set options for templates
    options[:name] = sectest_name
    options[:spec_class_name] = sectest_name.split('-').map { |t| t =~ /\d+/ ? t : t.capitalize! }.join

    # Error check to ensure this is a norad security test repository

    # Create the security tests standard files
    template('tool/Dockerfile.erb', "#{repo_dir}/sectests/#{sectest_name}/Dockerfile")
    template('tool/README.md.erb', "#{repo_dir}/sectests/#{sectest_name}/README.md")
    template('tool/manifest.yml.erb', "#{repo_dir}/sectests/#{sectest_name}/manifest.yml")

    # Create a starter wrapper script
    template('tool/wrapper.rb.erb', "#{repo_dir}/sectests/#{sectest_name}/#{sectest_name}-wrapper.rb")

    # Create the spec files
    template('tool/tool_spec.rb.erb', "#{repo_dir}/spec/#{sectest_name}/#{sectest_name}_spec.rb")
    if options[:test_type] == 'authenticated'
      template('tool/Dockerfile.auth.target.erb', "#{repo_dir}/spec/#{sectest_name}/targets/Dockerfile.secure")
      template('tool/Dockerfile.auth.target.erb', "#{repo_dir}/spec/#{sectest_name}/targets/Dockerfile.vulnerable")
    else
      template('tool/Dockerfile.unauth.target.erb', "#{repo_dir}/spec/#{sectest_name}/targets/Dockerfile.secure")
      template('tool/Dockerfile.unauth.target.erb', "#{repo_dir}/spec/#{sectest_name}/targets/Dockerfile.vulnerable")
    end
  end

  desc 'build', 'Build all sectest images and specs for the entire repository'
  option :registry, aliases: '-r', default: 'norad-registry.cisco.com:5000', desc: 'The Docker registry for Docker images'
  option :version, aliases: '-v', default: 'latest', desc: 'The version of the sectest container to build'
  def build
    # Check for the root_dir
    root_dir?

    # Error check to ensure this is a plugin directory
    Dir.glob('sectests/*').select do |f|
      if File.directory? f
        # Build all for the sectest
        send('build:all', f.split('/')[-1])
      end
    end
  end

  # Define arguments and options
  desc 'build:image SECTESTNAME', 'Build the docker image for the security test SECTESTNAME'
  option :registry, aliases: '-r', default: 'norad-registry.cisco.com:5000', desc: 'The Docker registry for Docker images'
  option :version, aliases: '-v', default: 'latest', desc: 'The version of the sectest container to build'
  define_method 'build:image' do |name|
    # Check for the root_dir
    root_dir?

    imgs_to_build = {}
    imgs_to_build["sectests/#{name}"] = "#{options[:registry]}/#{name}:#{options[:version]}"

    # Check for the Dockerfile
    if !dockerfile?(imgs_to_build.keys[0])
      say("Missing #{imgs_to_build.keys[0]}/Dockerfile", :red)
      exit(1)
    end

    # Determine if base image needs to be built
    base_img = extract_base_img(imgs_to_build.keys[0])
    while dockerfile?("base/#{base_img[0]}")
      imgs_to_build["base/#{base_img[0]}"] = base_img[1]
      base_img = extract_base_img(imgs_to_build.keys[-1])
    end

    # Build the images in reverse (Note: Hashes enumerate their values in insertion order.)
    Docker.options[:read_timeout] = 36_000
    imgs_to_build.keys.reverse_each do |img_dir|
      say("Building image #{img_dir}...", :green)
      Docker::Image.build_from_dir(img_dir, t: imgs_to_build[img_dir]) do |v|
        $stdout.puts v
      end
    end
  end

  # Define arguments and options
  desc 'build:specs SECTESTNAME', 'Build the spec images (test images) for the security test SECTESTNAME'
  option :registry, aliases: '-r', default: 'norad-registry.cisco.com:5000', desc: 'The Docker registry for Docker images'
  option :version, aliases: '-v', default: 'latest', desc: 'The version of the sectest container to build'
  define_method 'build:specs' do |name|
    # Check for the root_dir
    root_dir?

    imgs_to_build = {}
    imgs_to_build["#{File.expand_path(File.dirname(__FILE__))}/../templates/spec/support/Dockerfile.testserver"] = 'docker-images-test-results-server:latest'
    imgs_to_build["#{File.expand_path(File.dirname(__FILE__))}/../templates/spec/support/Dockerfile.ubuntu_ssh"] = 'docker-images-test-ubuntu-ssh-server:latest'

    # Determine the Dockerfiles in the assessment spec dir
    Dir.glob("spec/#{name}/targets/Dockerfile*").each do |entry|
      entry_components = entry.split('.')
      imgs_to_build[entry] = "#{name}-#{entry_components[-1]}:latest"
    end

    # Build the images
    Docker.options[:read_timeout] = 36_000
    imgs_to_build.keys.each do |img_dir|
      say("Building image #{img_dir}...", :green)
      docker_file = img_dir.split('/')[-1]
      Docker::Image.build_from_dir(img_dir.gsub(docker_file, ''), dockerfile: docker_file, t: imgs_to_build[img_dir]) do |v|
        $stdout.puts v
      end
    end

    # Pull the apline image for base testing
    Docker::Image.create('fromImage' => 'alpine:3.4')
  end

  # Define arguments and options
  desc 'build:all SECTESTNAME', 'Build sectest images for SECTESTNAME and all testing images for SECTESTNAME'
  option :registry, aliases: '-r', default: 'norad-registry.cisco.com:5000', desc: 'The Docker registry for Docker images'
  option :version, aliases: '-v', default: 'latest', desc: 'The version of the sectest container to build'
  define_method 'build:all' do |name|
    # Check for the root_dir
    root_dir?

    # Build the sectest image
    send('build:image', name)

    # Build the specs for testing the sectest
    send('build:specs', name)
  end

  desc 'spec:image SECTESTNAME', 'Run the rspec tests for SECTESTNAME'
  option :verbose, aliases: '-v', type: :boolean, desc: 'Turn on verbose logging'
  option :debug, aliases: '-d', type: :boolean, desc: 'Turn on debugging'
  define_method 'spec:image' do |name|
    # Check for the root_dir
    root_dir?

    # Set environment variables
    if options[:verbose]
      ENV['ENABLE_LOGS'] = 'true'
    end

    if options[:debug]
      ENV['ENABLE_NORAD_DEBUG'] = 'true'
    end

    ENV['SCAN_ASSESSMENT'] = 'true'
    ENV['TEST_RESULTS_SERVER_IMAGE'] = 'docker-images-test-results-server'
    ENV['UBUNTU_SSH_SERVER_IMAGE'] = 'docker-images-test-ubuntu-ssh-server'

    # Run the tests
    RSpec::Core::Runner.run(["spec/#{name}/#{name}_spec.rb"], $stderr, $stdout)
  end

  desc 'spec', 'Run all rspec tests for the entire repo (all sectests)'
  option :verbose, aliases: '-v', type: :boolean, default: false, desc: 'Turn on verbose logging'
  option :debug, aliases: '-d', type: :boolean, desc: 'Turn on debugging'
  def spec
    # Check for the root_dir
    root_dir?

    # Error check to ensure this is a plugin directory
    Dir.glob('sectests/*').select do |f|
      if File.directory? f
        # Build all for the sectest
        send('spec:image', f.split('/')[-1])
      end
    end
  end

  desc 'seed', 'Create the containers.rb seed to import into the api'
  option :seedfile, aliases: '-s', type: :string, default: './containers.rb', desc: 'The name of the seed file to generate'
  option :docsite, aliases: '-d', type: :string, default: 'https://norad.gitlab.io/docs/', desc: 'Set the documentation site'
  def seed
    # Check for the root_dir
    root_dir?

    # Generate the seed file
    SeedGenerator.process_manifests(options[:seedfile], options[:docsite])
  end

  desc 'validate:image SECTESTNAME', 'Validate SECTESTNAME manifest.yml and readme.md'
  define_method 'validate:image' do |name|
    # Check for the root_dir
    root_dir?

    # Validate the readme file
    ENV['sectest_name'] = name
    RSpec::Core::Runner.run(["#{File.dirname(File.expand_path(__FILE__))}/../support/readme_spec.rb"], $stderr, $stdout)

    # Validate the manifest file
    RSpec::Core::Runner.run(["#{File.dirname(File.expand_path(__FILE__))}/../support/manifest_spec.rb"], $stderr, $stdout)
  end

  desc 'validate', 'Validate all manifest.yml and readme.md'
  def validate
    # Check for the root_dir
    root_dir?

    # Error check to ensure this is a plugin directory
    Dir.glob('sectests/*').select do |f|
      if File.directory? f
        # Build all for the sectest
        send('validate:image', f.split('/')[-1])
      end
    end
  end

  no_tasks do
    def dockerfile?(img_dir)
      # Ensure the Dockerfile exists for the new tool
      File.file?("#{img_dir}/Dockerfile")
    end

    # Check for a base image
    def extract_base_img(img_dir)
      from_line = File.readlines("#{img_dir}/Dockerfile").select { |line| line =~ /^FROM/ }
      # Check for multiple from lines?
      from_line_arr = from_line[0].split(' ')
      from_image = from_line[0][%r{\AFROM\s+(.*?\/)?(.*?)(:.*?)?\Z}i, 2] || raise('bad from')
      [from_image, from_line_arr[1]]
    end

    # Ensure commands are run from the root dir
    def root_dir?
      %w(base spec sectests).each do |dirrepo_name|
        if !File.exist?(dirrepo_name)
          say("Commands must be run from the root of the test repository\nExiting....", :red)
          exit(1)
        end
      end
    end
  end
end