# 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.{secure,vulnerable}").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 if File.exist?("spec/#{name}/#{name}_spec.rb") RSpec.clear_examples say("Testing spec/#{name}/#{name}_spec.rb") RSpec::Core::Runner.run(["spec/#{name}/#{name}_spec.rb"], $stderr, $stdout) else say("Warning: spec/#{name}/#{name}_spec.rb does not exist!\n", :yellow) say("Warning: No tests will be run for #{name}\n", :yellow) end 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.clear_examples RSpec::Core::Runner.run(["#{File.dirname(File.expand_path(__FILE__))}/../support/readme_spec.rb"], $stderr, $stdout) # Validate the manifest file RSpec.clear_examples 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