Merge pull request #5596 from inspec/nm/control-tags

Filter active controls in profile by tags
This commit is contained in:
Clinton Wolfe 2021-07-27 23:14:41 -04:00 committed by GitHub
commit 5bbe34acb0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 158 additions and 3 deletions

View file

@ -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)

View file

@ -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"

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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?

View 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

View 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

View file

@ -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\)/)