lib/inspec/cli.rb in inspec-2.0.16 vs lib/inspec/cli.rb in inspec-2.0.17

- old
+ new

@@ -1,302 +1,292 @@ -# encoding: utf-8 -# Copyright 2015 Dominik Richter -# author: Dominik Richter -# author: Christoph Hartmann - -require 'logger' -require 'thor' -require 'json' -require 'pp' -require 'utils/json_log' -require 'utils/latest_version' -require 'inspec/base_cli' -require 'inspec/plugins' -require 'inspec/runner_mock' -require 'inspec/env_printer' -require 'inspec/schema' - -class Inspec::InspecCLI < Inspec::BaseCLI - class_option :log_level, aliases: :l, type: :string, - desc: 'Set the log level: info (default), debug, warn, error' - - class_option :log_location, type: :string, - desc: 'Location to send diagnostic log messages to. (default: STDOUT or STDERR)' - - class_option :diagnose, type: :boolean, - desc: 'Show diagnostics (versions, configurations)' - - desc 'json PATH', 'read all tests in PATH and generate a JSON summary' - option :output, aliases: :o, type: :string, - desc: 'Save the created profile to a path' - option :controls, type: :array, - desc: 'A list of controls to include. Ignore all other tests.' - profile_options - def json(target) - o = opts.dup - diagnose(o) - o[:ignore_supports] = true - o[:backend] = Inspec::Backend.create(target: 'mock://') - o[:check_mode] = true - - profile = Inspec::Profile.for_target(target, o) - info = profile.info - # add in inspec version - info[:generator] = { - name: 'inspec', - version: Inspec::VERSION, - } - dst = o[:output].to_s - if dst.empty? - puts JSON.dump(info) - else - if File.exist? dst - puts "----> updating #{dst}" - else - puts "----> creating #{dst}" - end - fdst = File.expand_path(dst) - File.write(fdst, JSON.dump(info)) - end - rescue StandardError => e - pretty_handle_exception(e) - end - - desc 'check PATH', 'verify all tests at the specified PATH' - option :format, type: :string - profile_options - def check(path) # rubocop:disable Metrics/AbcSize - o = opts.dup - diagnose(o) - o[:ignore_supports] = true # we check for integrity only - o[:backend] = Inspec::Backend.create(target: 'mock://') - o[:check_mode] = true - - # run check - profile = Inspec::Profile.for_target(path, o) - result = profile.check - - if o['format'] == 'json' - puts JSON.generate(result) - else - %w{location profile controls timestamp valid}.each do |item| - puts format('%-12s %s', item.to_s.capitalize + ':', - mark_text(result[:summary][item.to_sym])) - end - puts - - if result[:errors].empty? and result[:warnings].empty? - puts 'No errors or warnings' - else - red = "\033[31m" - yellow = "\033[33m" - rst = "\033[0m" - - item_msg = lambda { |item| - pos = [item[:file], item[:line], item[:column]].compact.join(':') - pos.empty? ? item[:msg] : pos + ': ' + item[:msg] - } - result[:errors].each do |item| - puts "#{red} ✖ #{item_msg.call(item)}#{rst}" - end - result[:warnings].each do |item| - puts "#{yellow} ! #{item_msg.call(item)}#{rst}" - end - - puts - puts format('Summary: %s%d errors%s, %s%d warnings%s', - red, result[:errors].length, rst, - yellow, result[:warnings].length, rst) - end - end - exit 1 unless result[:summary][:valid] - rescue StandardError => e - pretty_handle_exception(e) - end - - desc 'vendor PATH', 'Download all dependencies and generate a lockfile in a `vendor` directory' - option :overwrite, type: :boolean, default: false, - desc: 'Overwrite existing vendored dependencies and lockfile.' - def vendor(path = nil) - o = opts.dup - vendor_deps(path, o) - end - - desc 'archive PATH', 'archive a profile to tar.gz (default) or zip' - profile_options - option :output, aliases: :o, type: :string, - desc: 'Save the archive to a path' - option :zip, type: :boolean, default: false, - desc: 'Generates a zip archive.' - option :tar, type: :boolean, default: false, - desc: 'Generates a tar.gz archive.' - option :overwrite, type: :boolean, default: false, - desc: 'Overwrite existing archive.' - option :ignore_errors, type: :boolean, default: false, - desc: 'Ignore profile warnings.' - def archive(path) - o = opts.dup - diagnose(o) - - o[:logger] = Logger.new(STDOUT) - o[:logger].level = get_log_level(o.log_level) - o[:backend] = Inspec::Backend.create(target: 'mock://') - - profile = Inspec::Profile.for_target(path, o) - result = profile.check - - if result && !o[:ignore_errors] == false - o[:logger].info 'Profile check failed. Please fix the profile before generating an archive.' - return exit 1 - end - - # generate archive - exit 1 unless profile.archive(o) - rescue StandardError => e - pretty_handle_exception(e) - end - - desc 'exec PATHS', 'run all test files at the specified PATH.' - exec_options - def exec(*targets) - o = opts(:exec).dup - diagnose(o) - configure_logger(o) - - runner = Inspec::Runner.new(o) - targets.each { |target| runner.add_target(target) } - - exit runner.run - rescue ArgumentError, RuntimeError, Train::UserError => e - $stderr.puts e.message - exit 1 - rescue StandardError => e - pretty_handle_exception(e) - end - - desc 'detect', 'detect the target OS' - target_options - option :format, type: :string - def detect - o = opts(:detect).dup - o[:command] = 'platform.params' - (_, res) = run_command(o) - if o['format'] == 'json' - puts res.to_json - else - headline('Platform Details') - %w{name families release arch}.each { |item| - data = res[item.to_sym] - - # Format Array for better output if applicable - data = data.join(', ') if data.is_a?(Array) - - # Do not output fields of data is missing ('unknown' is fine) - next if data.nil? - - puts format('%-10s %s', item.to_s.capitalize + ':', mark_text(data)) - } - end - rescue ArgumentError, RuntimeError, Train::UserError => e - $stderr.puts e.message - exit 1 - rescue StandardError => e - pretty_handle_exception(e) - end - - desc 'shell', 'open an interactive debugging shell' - target_options - option :command, aliases: :c, - desc: 'A single command string to run instead of launching the shell' - option :format, type: :string, default: nil, hide: true, - desc: '[DEPRECATED] Please use --reporter - this will be removed in InSpec 3.0' - option :reporter, type: :array, - banner: 'one two:/output/file/path', - desc: 'Enable one or more output reporters: cli, documentation, html, progress, json, json-min, json-rspec, junit' - option :depends, type: :array, default: [], - desc: 'A space-delimited list of local folders containing profiles whose libraries and resources will be loaded into the new shell' - def shell_func - o = opts(:shell).dup - diagnose(o) - o[:debug_shell] = true - - log_device = suppress_log_output?(o) ? nil : STDOUT - o[:logger] = Logger.new(log_device) - o[:logger].level = get_log_level(o.log_level) - - if o[:command].nil? - runner = Inspec::Runner.new(o) - return Inspec::Shell.new(runner).start - end - - run_type, res = run_command(o) - exit res unless run_type == :ruby_eval - - # No InSpec tests - just print evaluation output. - res = (res.respond_to?(:to_json) ? res.to_json : JSON.dump(res)) if o['reporter']&.keys&.include?('json') - puts res - exit 0 - rescue RuntimeError, Train::UserError => e - $stderr.puts e.message - rescue StandardError => e - pretty_handle_exception(e) - end - - desc 'env', 'Output shell-appropriate completion configuration' - def env(shell = nil) - p = Inspec::EnvPrinter.new(self.class, shell) - p.print_and_exit! - rescue StandardError => e - pretty_handle_exception(e) - end - - desc 'schema NAME', 'print the JSON schema', hide: true - def schema(name) - puts Inspec::Schema.json(name) - rescue StandardError => e - puts e - puts "Valid schemas are #{Inspec::Schema.names.join(', ')}" - end - - desc 'version', 'prints the version of this tool' - option :format, type: :string - def version - if opts['format'] == 'json' - v = { version: Inspec::VERSION } - puts v.to_json - else - puts Inspec::VERSION - # display outdated version - latest = LatestInSpecVersion.new.latest - if Gem::Version.new(Inspec::VERSION) < Gem::Version.new(latest) - puts "\nYour version of InSpec is out of date! The latest version is #{latest}." - end - end - end - map %w{-v --version} => :version - - private - - def run_command(opts) - runner = Inspec::Runner.new(opts) - res = runner.eval_with_virtual_profile(opts[:command]) - runner.load - - return :ruby_eval, res if runner.all_rules.empty? - return :rspec_run, runner.run_tests # rubocop:disable Style/RedundantReturn - end -end - -# Load all plugins on startup -ctl = Inspec::PluginCtl.new -ctl.list.each { |x| ctl.load(x) } - -# load CLI plugins before the Inspec CLI has been started -Inspec::Plugins::CLI.subcommands.each { |_subcommand, params| - Inspec::InspecCLI.register( - params[:klass], - params[:subcommand_name], - params[:usage], - params[:description], - params[:options], - ) -} +# encoding: utf-8 +# Copyright 2015 Dominik Richter +# author: Dominik Richter +# author: Christoph Hartmann + +require 'logger' +require 'thor' +require 'json' +require 'pp' +require 'utils/json_log' +require 'utils/latest_version' +require 'inspec/base_cli' +require 'inspec/plugins' +require 'inspec/runner_mock' +require 'inspec/env_printer' +require 'inspec/schema' + +class Inspec::InspecCLI < Inspec::BaseCLI + class_option :log_level, aliases: :l, type: :string, + desc: 'Set the log level: info (default), debug, warn, error' + + class_option :log_location, type: :string, + desc: 'Location to send diagnostic log messages to. (default: STDOUT or STDERR)' + + class_option :diagnose, type: :boolean, + desc: 'Show diagnostics (versions, configurations)' + + desc 'json PATH', 'read all tests in PATH and generate a JSON summary' + option :output, aliases: :o, type: :string, + desc: 'Save the created profile to a path' + option :controls, type: :array, + desc: 'A list of controls to include. Ignore all other tests.' + profile_options + def json(target) + o = opts.dup + diagnose(o) + o[:ignore_supports] = true + o[:backend] = Inspec::Backend.create(target: 'mock://') + o[:check_mode] = true + + profile = Inspec::Profile.for_target(target, o) + info = profile.info + # add in inspec version + info[:generator] = { + name: 'inspec', + version: Inspec::VERSION, + } + dst = o[:output].to_s + if dst.empty? + puts JSON.dump(info) + else + if File.exist? dst + puts "----> updating #{dst}" + else + puts "----> creating #{dst}" + end + fdst = File.expand_path(dst) + File.write(fdst, JSON.dump(info)) + end + rescue StandardError => e + pretty_handle_exception(e) + end + + desc 'check PATH', 'verify all tests at the specified PATH' + option :format, type: :string + profile_options + def check(path) # rubocop:disable Metrics/AbcSize + o = opts.dup + diagnose(o) + o[:ignore_supports] = true # we check for integrity only + o[:backend] = Inspec::Backend.create(target: 'mock://') + o[:check_mode] = true + + # run check + profile = Inspec::Profile.for_target(path, o) + result = profile.check + + if o['format'] == 'json' + puts JSON.generate(result) + else + %w{location profile controls timestamp valid}.each do |item| + puts format('%-12s %s', item.to_s.capitalize + ':', + mark_text(result[:summary][item.to_sym])) + end + puts + + if result[:errors].empty? and result[:warnings].empty? + puts 'No errors or warnings' + else + red = "\033[31m" + yellow = "\033[33m" + rst = "\033[0m" + + item_msg = lambda { |item| + pos = [item[:file], item[:line], item[:column]].compact.join(':') + pos.empty? ? item[:msg] : pos + ': ' + item[:msg] + } + result[:errors].each do |item| + puts "#{red} ✖ #{item_msg.call(item)}#{rst}" + end + result[:warnings].each do |item| + puts "#{yellow} ! #{item_msg.call(item)}#{rst}" + end + + puts + puts format('Summary: %s%d errors%s, %s%d warnings%s', + red, result[:errors].length, rst, + yellow, result[:warnings].length, rst) + end + end + exit 1 unless result[:summary][:valid] + rescue StandardError => e + pretty_handle_exception(e) + end + + desc 'vendor PATH', 'Download all dependencies and generate a lockfile in a `vendor` directory' + option :overwrite, type: :boolean, default: false, + desc: 'Overwrite existing vendored dependencies and lockfile.' + def vendor(path = nil) + o = opts.dup + vendor_deps(path, o) + end + + desc 'archive PATH', 'archive a profile to tar.gz (default) or zip' + profile_options + option :output, aliases: :o, type: :string, + desc: 'Save the archive to a path' + option :zip, type: :boolean, default: false, + desc: 'Generates a zip archive.' + option :tar, type: :boolean, default: false, + desc: 'Generates a tar.gz archive.' + option :overwrite, type: :boolean, default: false, + desc: 'Overwrite existing archive.' + option :ignore_errors, type: :boolean, default: false, + desc: 'Ignore profile warnings.' + def archive(path) + o = opts.dup + diagnose(o) + + o[:logger] = Logger.new(STDOUT) + o[:logger].level = get_log_level(o.log_level) + o[:backend] = Inspec::Backend.create(target: 'mock://') + + profile = Inspec::Profile.for_target(path, o) + result = profile.check + + if result && !o[:ignore_errors] == false + o[:logger].info 'Profile check failed. Please fix the profile before generating an archive.' + return exit 1 + end + + # generate archive + exit 1 unless profile.archive(o) + rescue StandardError => e + pretty_handle_exception(e) + end + + desc 'exec PATHS', 'run all test files at the specified PATH.' + exec_options + def exec(*targets) + o = opts(:exec).dup + diagnose(o) + configure_logger(o) + + runner = Inspec::Runner.new(o) + targets.each { |target| runner.add_target(target) } + + exit runner.run + rescue ArgumentError, RuntimeError, Train::UserError => e + $stderr.puts e.message + exit 1 + rescue StandardError => e + pretty_handle_exception(e) + end + + desc 'detect', 'detect the target OS' + target_options + option :format, type: :string + def detect + o = opts(:detect).dup + o[:command] = 'platform.params' + (_, res) = run_command(o) + if o['format'] == 'json' + puts res.to_json + else + headline('Platform Details') + puts Inspec::BaseCLI.detect(params: res, indent: 0, color: 36) + end + rescue ArgumentError, RuntimeError, Train::UserError => e + $stderr.puts e.message + exit 1 + rescue StandardError => e + pretty_handle_exception(e) + end + + desc 'shell', 'open an interactive debugging shell' + target_options + option :command, aliases: :c, + desc: 'A single command string to run instead of launching the shell' + option :format, type: :string, default: nil, hide: true, + desc: '[DEPRECATED] Please use --reporter - this will be removed in InSpec 3.0' + option :reporter, type: :array, + banner: 'one two:/output/file/path', + desc: 'Enable one or more output reporters: cli, documentation, html, progress, json, json-min, json-rspec, junit' + option :depends, type: :array, default: [], + desc: 'A space-delimited list of local folders containing profiles whose libraries and resources will be loaded into the new shell' + def shell_func + o = opts(:shell).dup + diagnose(o) + o[:debug_shell] = true + + log_device = suppress_log_output?(o) ? nil : STDOUT + o[:logger] = Logger.new(log_device) + o[:logger].level = get_log_level(o.log_level) + + if o[:command].nil? + runner = Inspec::Runner.new(o) + return Inspec::Shell.new(runner).start + end + + run_type, res = run_command(o) + exit res unless run_type == :ruby_eval + + # No InSpec tests - just print evaluation output. + res = (res.respond_to?(:to_json) ? res.to_json : JSON.dump(res)) if o['reporter']&.keys&.include?('json') + puts res + exit 0 + rescue RuntimeError, Train::UserError => e + $stderr.puts e.message + rescue StandardError => e + pretty_handle_exception(e) + end + + desc 'env', 'Output shell-appropriate completion configuration' + def env(shell = nil) + p = Inspec::EnvPrinter.new(self.class, shell) + p.print_and_exit! + rescue StandardError => e + pretty_handle_exception(e) + end + + desc 'schema NAME', 'print the JSON schema', hide: true + def schema(name) + puts Inspec::Schema.json(name) + rescue StandardError => e + puts e + puts "Valid schemas are #{Inspec::Schema.names.join(', ')}" + end + + desc 'version', 'prints the version of this tool' + option :format, type: :string + def version + if opts['format'] == 'json' + v = { version: Inspec::VERSION } + puts v.to_json + else + puts Inspec::VERSION + # display outdated version + latest = LatestInSpecVersion.new.latest + if Gem::Version.new(Inspec::VERSION) < Gem::Version.new(latest) + puts "\nYour version of InSpec is out of date! The latest version is #{latest}." + end + end + end + map %w{-v --version} => :version + + private + + def run_command(opts) + runner = Inspec::Runner.new(opts) + res = runner.eval_with_virtual_profile(opts[:command]) + runner.load + + return :ruby_eval, res if runner.all_rules.empty? + return :rspec_run, runner.run_tests # rubocop:disable Style/RedundantReturn + end +end + +# Load all plugins on startup +ctl = Inspec::PluginCtl.new +ctl.list.each { |x| ctl.load(x) } + +# load CLI plugins before the Inspec CLI has been started +Inspec::Plugins::CLI.subcommands.each { |_subcommand, params| + Inspec::InspecCLI.register( + params[:klass], + params[:subcommand_name], + params[:usage], + params[:description], + params[:options], + ) +}