# 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 puts options[:configurable] # 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 # 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| 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| 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| # Build the sectest image send('build:image', name) # Build the specs for testing the sectest send('build:specs', name) end # Define arguments and options desc 'execute SECTESTNAME ARGUMENTS', 'Executes the specified security test SECTESTNAME w/ ARGUMENTS' 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 tools docker container to build' def execute(name, arguments) # Ensure container exists if !Docker::Image.exist?("#{options[:registry]}/#{name}:#{options[:version]}") say("Requested image #{options[:registry]}/#{name}:#{options[:version]} does not exist!", :red) exit(1) end # Setup and run the container env = ['NORAD_ROOT=', %(ASSESSMENT_PATHS=[{"id":"1", "assessment": "1"}]), 'NORAD_SECRET=1234'] container = Docker::Container.create(Image: "#{options[:registry]}/#{name}:#{options[:version]}", Env: env, Cmd: arguments) # Start the container, watch stdout container.tap(&:start).attach { |stream, chunk| puts "#{stream}: #{chunk}" } end desc 'spec:image SECTESTNAME', 'Run the rspec tests for SECTESTNAME' option :verbose, aliases: '-v', type: :boolean, default: false, desc: 'Turn on verbose logging' option :scan_assessment, aliases: '-s', type: :boolean, default: true, desc: 'Fix me' define_method 'spec:image' do |name| # Set environment variables ENV['ENABLE_LOGS'] = options[:verbose] ? 'true' : 'false' ENV['SCAN_ASSESSMENT'] = options[:scan_assessment] ? 'true' : 'false' 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 :scan_assessment, aliases: '-s', type: :boolean, default: true, desc: 'Fix me' def spec # 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 # Error check to ensure this is a plugin directory # Generate the seed file SeedGenerator.process_manifests(options[:seedfile], options[:docsite]) 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 end end