mirror of
https://github.com/inspec/inspec
synced 2024-11-10 23:24:18 +00:00
Merge pull request #5596 from inspec/nm/control-tags
Filter active controls in profile by tags
This commit is contained in:
commit
5bbe34acb0
9 changed files with 158 additions and 3 deletions
|
@ -336,6 +336,8 @@ This subcommand has additional options:
|
|||
Simple targeting option using URIs, e.g. ssh://user:pass@host:port
|
||||
* ``--target-id=TARGET_ID``
|
||||
Provide a ID which will be included on reports
|
||||
* ``--tags=one two three``
|
||||
A list of tags, a list of regular expressions that match tags, or a hash map where each value is a tag. `exec` will run controls referenced by the listed or matching tags.
|
||||
* ``--user=USER``
|
||||
The login user for a remote scan.
|
||||
* ``--vendor-cache=VENDOR_CACHE``
|
||||
|
@ -383,6 +385,8 @@ This subcommand has additional options:
|
|||
Save the created profile to a path
|
||||
* ``--profiles-path=PROFILES_PATH``
|
||||
Folder which contains referenced profiles.
|
||||
* ``--tags=one two three``
|
||||
A list of tags that reference certain controls. Other controls are ignored.
|
||||
* ``--vendor-cache=VENDOR_CACHE``
|
||||
Use the given path for caching dependencies. (default: ~/.inspec/cache)
|
||||
|
||||
|
|
|
@ -136,6 +136,8 @@ module Inspec
|
|||
profile_options
|
||||
option :controls, type: :array,
|
||||
desc: "A list of control names to run, or a list of /regexes/ to match against control names. Ignore all other tests."
|
||||
option :tags, type: :array,
|
||||
desc: "A list of tags names that are part of controls to filter and run controls, or a list of /regexes/ to match against tags names of controls. Ignore all other tests."
|
||||
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, yaml"
|
||||
|
|
|
@ -65,6 +65,8 @@ class Inspec::InspecCLI < Inspec::BaseCLI
|
|||
desc: "Save the created profile to a path"
|
||||
option :controls, type: :array,
|
||||
desc: "A list of controls to include. Ignore all other tests."
|
||||
option :tags, type: :array,
|
||||
desc: "A list of tags to filter controls and include only those. Ignore all other tests."
|
||||
profile_options
|
||||
def json(target)
|
||||
require "json" unless defined?(JSON)
|
||||
|
|
|
@ -53,12 +53,23 @@ module Inspec
|
|||
|
||||
def control(id, opts = {}, &block)
|
||||
opts[:skip_only_if_eval] = @skip_only_if_eval
|
||||
if control_exist_in_controls_list?(id) || controls_list_empty?
|
||||
tag_ids = control_tags(&block)
|
||||
if (controls_list_empty? && tags_list_empty?) || control_exist_in_controls_list?(id) || tag_exist_in_control_tags?(tag_ids)
|
||||
register_control(Inspec::Rule.new(id, profile_id, resources_dsl, opts, &block))
|
||||
end
|
||||
end
|
||||
|
||||
alias rule control
|
||||
|
||||
def control_tags(&block)
|
||||
tag_source = block.source.split("\n").select { |src| src.split.first.eql?("tag") }
|
||||
tag_source = tag_source.map { |src| src.sub("tag", "").strip }.map { |src| src.split(",").map { |final_src| final_src.sub(/([^:]*):/, "") } }.flatten
|
||||
output = tag_source.map { |src| src.sub(/\[|\]/, "") }.map { |src| instance_eval(src) }
|
||||
output.compact.uniq
|
||||
rescue
|
||||
[]
|
||||
end
|
||||
|
||||
# Describe allows users to write rspec-like bare describe
|
||||
# blocks without declaring an inclosing control. Here, we
|
||||
# generate a control for them automatically and then execute
|
||||
|
@ -74,7 +85,7 @@ module Inspec
|
|||
res = describe(*args, &block)
|
||||
end
|
||||
|
||||
if control_exist_in_controls_list?(id) || controls_list_empty?
|
||||
if controls_list_empty? || control_exist_in_controls_list?(id)
|
||||
register_control(rule, &block)
|
||||
end
|
||||
|
||||
|
@ -187,11 +198,19 @@ module Inspec
|
|||
!@conf.empty? && @conf.key?("profile") && !@conf["profile"].include_controls_list.empty?
|
||||
end
|
||||
|
||||
def profile_tag_config_exist?
|
||||
!@conf.empty? && @conf.key?("profile") && !@conf["profile"].include_tags_list.empty?
|
||||
end
|
||||
|
||||
# Returns true if configuration hash is empty or configuration hash does not have the list of controls that needs to be included
|
||||
def controls_list_empty?
|
||||
!@conf.empty? && @conf.key?("profile") && @conf["profile"].include_controls_list.empty? || @conf.empty?
|
||||
end
|
||||
|
||||
def tags_list_empty?
|
||||
!@conf.empty? && @conf.key?("profile") && @conf["profile"].include_tags_list.empty? || @conf.empty?
|
||||
end
|
||||
|
||||
# Check if the given control exist in the --controls option
|
||||
def control_exist_in_controls_list?(id)
|
||||
id_exist_in_list = false
|
||||
|
@ -203,5 +222,25 @@ module Inspec
|
|||
end
|
||||
id_exist_in_list
|
||||
end
|
||||
|
||||
# Check if the given control exist in the --tags option
|
||||
def tag_exist_in_control_tags?(tag_ids)
|
||||
tag_option_matches_with_list = false
|
||||
if !tag_ids.empty? && !tag_ids.nil? && profile_tag_config_exist?
|
||||
tag_option_matches_with_list = !(tag_ids & @conf["profile"].include_tags_list).empty?
|
||||
unless tag_option_matches_with_list
|
||||
@conf["profile"].include_tags_list.any? do |inclusion|
|
||||
# Try to see if the inclusion is a regex, and if it matches
|
||||
if inclusion.is_a?(Regexp)
|
||||
tag_ids.each do |id|
|
||||
tag_option_matches_with_list = (inclusion =~ id)
|
||||
break if tag_option_matches_with_list
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
tag_option_matches_with_list
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -87,6 +87,7 @@ module Inspec
|
|||
@logger = options[:logger] || Logger.new(nil)
|
||||
@locked_dependencies = options[:dependencies]
|
||||
@controls = options[:controls] || []
|
||||
@tags = options[:tags] || []
|
||||
@writable = options[:writable] || false
|
||||
@profile_id = options[:id]
|
||||
@profile_name = options[:profile_name]
|
||||
|
@ -206,7 +207,7 @@ module Inspec
|
|||
@params ||= load_params
|
||||
end
|
||||
|
||||
def collect_tests(include_list = @controls)
|
||||
def collect_tests
|
||||
unless @tests_collected || failed?
|
||||
return unless supports_platform?
|
||||
|
||||
|
@ -253,6 +254,30 @@ module Inspec
|
|||
included_controls
|
||||
end
|
||||
|
||||
# This creates the list of controls to be filtered by tag values provided in the --tags options
|
||||
def include_tags_list
|
||||
return [] if @tags.nil? || @tags.empty?
|
||||
|
||||
included_tags = @tags
|
||||
# Check for anything that might be a regex in the list, and make it official
|
||||
included_tags.each_with_index do |inclusion, index|
|
||||
next if inclusion.is_a?(Regexp)
|
||||
# Insist the user wrap the regex in slashes to demarcate it as a regex
|
||||
next unless inclusion.start_with?("/") && inclusion.end_with?("/")
|
||||
|
||||
inclusion = inclusion[1..-2] # Trim slashes
|
||||
begin
|
||||
re = Regexp.new(inclusion)
|
||||
included_tags[index] = re
|
||||
rescue RegexpError => e
|
||||
warn "Ignoring unparseable regex '/#{inclusion}/' in --control CLI option: #{e.message}"
|
||||
included_tags[index] = nil
|
||||
end
|
||||
end
|
||||
included_tags.compact!
|
||||
included_tags
|
||||
end
|
||||
|
||||
def load_libraries
|
||||
return @runner_context if @libraries_loaded
|
||||
|
||||
|
|
|
@ -50,6 +50,7 @@ module Inspec
|
|||
@conf[:logger] ||= Logger.new(nil)
|
||||
@target_profiles = []
|
||||
@controls = @conf[:controls] || []
|
||||
@tags = @conf[:tags] || []
|
||||
@depends = @conf[:depends] || []
|
||||
@create_lockfile = @conf[:create_lockfile]
|
||||
@cache = Inspec::Cache.new(@conf[:vendor_cache])
|
||||
|
@ -199,6 +200,7 @@ module Inspec
|
|||
vendor_cache: @cache,
|
||||
backend: @backend,
|
||||
controls: @controls,
|
||||
tags: @tags,
|
||||
runner_conf: @conf)
|
||||
raise "Could not resolve #{target} to valid input." if profile.nil?
|
||||
|
||||
|
|
29
test/fixtures/profiles/control-tags/controls/example.rb
vendored
Normal file
29
test/fixtures/profiles/control-tags/controls/example.rb
vendored
Normal file
|
@ -0,0 +1,29 @@
|
|||
control "basic" do
|
||||
tag "tag1"
|
||||
tag severity: nil
|
||||
tag data: "tag2"
|
||||
tag data_arr: ["tag3", "tag4"]
|
||||
describe(true) { it { should eq true } }
|
||||
end
|
||||
|
||||
control "tag keyword used in control name and tag value" do
|
||||
tag "tag5"
|
||||
describe(true) { it { should eq true } }
|
||||
end
|
||||
|
||||
control "multiple tags in one line" do
|
||||
tag "tag6", "tag7", "tagname with space"
|
||||
tag data1: "tag8", data2: "tag9"
|
||||
tag data_arr1: ["tag10", "tag11"], data_arr2: ["tag12", "tag13"]
|
||||
describe(true) { it { should eq true } }
|
||||
end
|
||||
|
||||
control "all different formats of tags in one line" do
|
||||
tag "tag14", data: "tag15", data_arr: ["tag16", "tag17"]
|
||||
describe(true) { it { should eq true } }
|
||||
end
|
||||
|
||||
control "failure control" do
|
||||
tag "tag18"
|
||||
describe(true) { it { should eq false } }
|
||||
end
|
7
test/fixtures/profiles/control-tags/inspec.yml
vendored
Normal file
7
test/fixtures/profiles/control-tags/inspec.yml
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
name: control-tags
|
||||
title: InSpec Profile for testing filtering on controls using tags
|
||||
license: Apache-2.0
|
||||
summary: An InSpec Compliance Profile for testing filtering on controls using tags
|
||||
version: 0.1.0
|
||||
supports:
|
||||
platform: os
|
|
@ -237,6 +237,51 @@ Test Summary: 0 successful, 0 failures, 0 skipped
|
|||
assert_exit_code 100, out
|
||||
end
|
||||
|
||||
it "executes only specified controls when selecting the controls by literal single tag name" do
|
||||
inspec("exec " + File.join(profile_path, "control-tags") + " --no-create-lockfile --tags tag1")
|
||||
_(stdout).must_include "true is expected to eq true\n"
|
||||
_(stdout).must_include "Test Summary: 1 successful, 0 failures, 0 skipped\n"
|
||||
_(stderr).must_equal ""
|
||||
|
||||
assert_exit_code 0, out
|
||||
end
|
||||
|
||||
it "executes only specified controls when selecting the controls by literal multiple tag names" do
|
||||
inspec("exec " + File.join(profile_path, "control-tags") + " --no-create-lockfile --tags tag1 tag5 tag6 tag17 'tagname with space'")
|
||||
_(stdout).must_include "true is expected to eq true\n"
|
||||
_(stdout).must_include "Test Summary: 4 successful, 0 failures, 0 skipped\n"
|
||||
_(stderr).must_equal ""
|
||||
|
||||
assert_exit_code 0, out
|
||||
end
|
||||
|
||||
it "executes only specified controls when selecting the controls by using regex on tags" do
|
||||
inspec("exec " + File.join(profile_path, "control-tags") + " --no-create-lockfile --tags '/\s+/'")
|
||||
_(stdout).must_include "true is expected to eq true\n"
|
||||
_(stdout).must_include "Test Summary: 1 successful, 0 failures, 0 skipped\n"
|
||||
_(stderr).must_equal ""
|
||||
|
||||
assert_exit_code 0, out
|
||||
end
|
||||
|
||||
it "executes only specified controls when selecting failing controls by using literal name of tag" do
|
||||
inspec("exec " + File.join(profile_path, "control-tags") + " --no-create-lockfile --tags tag18")
|
||||
_(stdout).must_include "true is expected to eq false\n"
|
||||
_(stdout).must_include "Test Summary: 0 successful, 1 failure, 0 skipped\n"
|
||||
_(stderr).must_equal ""
|
||||
|
||||
assert_exit_code 100, out
|
||||
end
|
||||
|
||||
it "executes only specified controls when selecting failing controls by using regex on tags" do
|
||||
inspec("exec " + File.join(profile_path, "control-tags") + " --no-create-lockfile --tags '/(18)/'")
|
||||
_(stdout).must_include "true is expected to eq false\n"
|
||||
_(stdout).must_include "Test Summary: 0 successful, 1 failure, 0 skipped\n"
|
||||
_(stderr).must_equal ""
|
||||
|
||||
assert_exit_code 100, out
|
||||
end
|
||||
|
||||
it "reports whan a profile cannot be loaded" do
|
||||
inspec("exec " + File.join(profile_path, "raise_outside_control") + " --no-create-lockfile")
|
||||
_(stdout).must_match(/Profile:[\W]+InSpec Profile \(raise_outside_control\)/)
|
||||
|
|
Loading…
Reference in a new issue