Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Filter active controls in profile by tags #5596

Merged
merged 3 commits into from Jul 28, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs-chef-io/content/inspec/cli.md
Expand Up @@ -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``
Expand Down Expand Up @@ -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)

Expand Down
2 changes: 2 additions & 0 deletions lib/inspec/base_cli.rb
Expand Up @@ -136,6 +136,8 @@ def self.exec_options
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"
Expand Down
2 changes: 2 additions & 0 deletions lib/inspec/cli.rb
Expand Up @@ -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)
Expand Down
43 changes: 41 additions & 2 deletions lib/inspec/control_eval_context.rb
Expand Up @@ -53,12 +53,23 @@ def profile_name

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
Expand All @@ -74,7 +85,7 @@ def describe(*args, &block)
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

Expand Down Expand Up @@ -187,11 +198,19 @@ def profile_config_exist?
!@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
Expand All @@ -203,5 +222,25 @@ def control_exist_in_controls_list?(id)
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
27 changes: 26 additions & 1 deletion lib/inspec/profile.rb
Expand Up @@ -87,6 +87,7 @@ def initialize(source_reader, options = {})
@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]
Expand Down Expand Up @@ -206,7 +207,7 @@ def params
@params ||= load_params
end

def collect_tests(include_list = @controls)
def collect_tests
unless @tests_collected || failed?
return unless supports_platform?

Expand Down Expand Up @@ -253,6 +254,30 @@ def include_controls_list
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

Expand Down
2 changes: 2 additions & 0 deletions lib/inspec/runner.rb
Expand Up @@ -50,6 +50,7 @@ def initialize(conf = {})
@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])
Expand Down Expand Up @@ -199,6 +200,7 @@ def add_target(target, _opts = [])
vendor_cache: @cache,
backend: @backend,
controls: @controls,
tags: @tags,
runner_conf: @conf)
raise "Could not resolve #{target} to valid input." if profile.nil?

Expand Down
29 changes: 29 additions & 0 deletions test/fixtures/profiles/control-tags/controls/example.rb
@@ -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 changes: 7 additions & 0 deletions test/fixtures/profiles/control-tags/inspec.yml
@@ -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
45 changes: 45 additions & 0 deletions test/functional/inspec_exec_test.rb
Expand Up @@ -237,6 +237,51 @@ def stderr
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\)/)
Expand Down