Merge pull request #5903 from inspec/nm/resource_id-3

CFINSPEC-70 resource_id support take 3
This commit is contained in:
Clinton Wolfe 2022-03-06 11:07:52 -05:00 committed by GitHub
commit 924c0a24c8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 126 additions and 16 deletions

View file

@ -423,6 +423,7 @@ The `run_data` object contains all data from the Chef InSpec run. Here is an ove
|`run_data.profiles[0].controls[0].results[0].resource`| Undocumented and usually unpopulated; try exploring resource_title |
|`run_data.profiles[0].controls[0].results[0].resource_name`| String, name of the resource used in the test|
|`run_data.profiles[0].controls[0].results[0].resource_title`| Anonymous Class, the actual instance of the Resource. Responds to to_s with the name of the resource.|
|`run_data.profiles[0].controls[0].results[0].resource_title.resource_id`| String, unique identifier of each resource node.|
|`run_data.profiles[0].controls[0].results[0].run_time`| Float, execution time in seconds for the test|
|`run_data.profiles[0].controls[0].results[0].skip_message`| String, if the test was skipped, explains why (user provided)|
|`run_data.profiles[0].controls[0].results[0].start_time`| DateTime, time the test started executing|

View file

@ -54,6 +54,17 @@ The following methods are available to the resource:
- inspec - Contains a registry of all other resources to interact with the operating system or target in general.
- skip_resource - A resource may call this method to indicate that requirements aren't met. All tests that use this resource will be marked as skipped.
The additional methods may be defined within the resource:
- resource_id - An instance method. Place logic here to determine the unique identifier for a resource, and set it using the superclass method. Following is an example of its usage in an InSpec test:
```
# example_config resource can have unique conf file path as an identifier.
describe example_config do
its("resource_id") { should eq PATH_OF_CONF_FILE }
end
```
The following example shows a full resource using attributes and methods
to provide simple access to a configuration file:
@ -88,6 +99,11 @@ class ExampleConfig < Inspec.resource(1)
@params[name]
end
def resource_id
value = example_method_to_determine_resource_id # define logic to determine resource_id value
super(value)
end
private
def read_content

View file

@ -36,21 +36,41 @@ module Inspec::Reporters
def profile_results(control)
(control[:results] || []).map { |r|
{
status: r[:status],
code_desc: r[:code_desc],
run_time: r[:run_time],
start_time: r[:start_time],
resource: r[:resource],
skip_message: r[:skip_message],
message: r[:message],
exception: r[:exception],
backtrace: r[:backtrace],
resource_class: r[:resource_class],
status: r[:status],
code_desc: r[:code_desc],
run_time: r[:run_time],
start_time: r[:start_time],
resource: r[:resource],
skip_message: r[:skip_message],
message: r[:message],
exception: r[:exception],
backtrace: r[:backtrace],
resource_class: r[:resource_class],
resource_params: r[:resource_params].to_s,
resource_id: extract_resource_id(r),
}.reject { |_k, v| v.nil? }
}
end
def extract_resource_id(r)
# According to the RunData API, this is supposed to be an anonymous
# class that represents a resource, with embedded instance methods....
resource_obj = r[:resource_title]
return resource_obj.resource_id if resource_obj.respond_to?(:resource_id)
# But sometimes, it isn't, and has been collapsed into the to_s stringification of the resource.
if resource_obj.is_a?(String)
orig_str = resource_obj
# Try to trim off the resource class - eg "File /some/path" => "/some/path"
trimmed_str = orig_str.sub(/^#{r[:resource_class]}/i, "").strip
trimmed_str.empty? ? orig_str : trimmed_str
else
# Boo, InSpec is crazy, and we don't know what it possibly could be.
# Failsafe for resource_id is empty string.
""
end
end
def profiles
run_data[:profiles].map do |p|
res = {

View file

@ -34,6 +34,12 @@ module Inspec
Inspec::Resource.support_registry[key].push(criteria)
end
def resource_id(value = nil)
@resource_id = value if value
@resource_id = "" if @resource_id.nil?
@resource_id
end
# TODO: this is pretty terrible and is only here to work around
# the idea that we've trained resource authors to make initialize
# methods w/o calling super.

View file

@ -57,6 +57,7 @@ module Inspec
"run_time" => { "type" => "number" },
"start_time" => { "type" => "string" },
"resource_class" => { "type" => "string", "optional" => true },
"resource_id" => { "type" => "string", "optional" => true },
"skip_message" => { "type" => "string", "optional" => true },
"resource" => { "type" => "string", "optional" => true },
"message" => { "type" => "string", "optional" => true },

View file

@ -38,6 +38,7 @@ module Inspec
"message" => Primitives.desc(Primitives::STRING, "An explanation of the test status - usually only provided when the test fails."),
"skip_message" => Primitives.desc(Primitives::STRING, "An explanation of the test status if the status was 'skipped."),
"exception" => Primitives.desc(Primitives::STRING, "The type of exception if an exception was thrown."),
"resource_id" => Primitives.desc(Primitives::STRING, "The unique identifier of the resource."),
"backtrace" => {
"anyOf" => [
Primitives.array(Primitives::STRING),

View file

@ -0,0 +1,5 @@
control "basic custom resource" do
describe basic do
its("resource_id") { should cmp "42" }
end
end

View file

@ -0,0 +1,10 @@
name: resource_ids
title: InSpec Profile
maintainer: The Authors
copyright: The Authors
copyright_email: you@example.com
license: Apache-2.0
summary: An InSpec Compliance Profile
version: 0.1.0
supports:
platform: os

View file

@ -0,0 +1,7 @@
class Basic < Inspec.resource(1)
name 'basic'
def resource_id
super("42")
end
end

View file

@ -62,7 +62,8 @@
"code_desc": "File /etc/hosts mode should eq 420",
"run_time": 0.031503,
"start_time": "2018-07-30T08:56:41-04:00",
"resource_params": ""
"resource_params": "",
"resource_id": "File /etc/hosts"
}
]
},
@ -103,7 +104,8 @@
"code_desc": "File /etc/passwd should exist",
"run_time": 0.003954,
"start_time": "2018-07-30T08:56:41-04:00",
"resource_params": ""
"resource_params": "",
"resource_id": "File /etc/passwd"
}
]
},
@ -134,7 +136,8 @@
"start_time": "2018-07-30T08:56:41-04:00",
"resource": "Operating System Detection",
"skip_message": "Skipped control due to only_if condition.",
"resource_params": ""
"resource_params": "",
"resource_id": "Operating System Detection"
}
]
}

View file

@ -1 +1 @@
{"platform":{"name":"mac_os_x","release":"17.2.0"},"profiles":[{"name":"long_commands","version":"0.1.0","sha256":"4f816f8cf18f165f05f1cf20936aaad06a15287de3f578891197647ca05c7df4","title":"InSpec Profile","maintainer":"The Authors","summary":"An InSpec Compliance Profile","license":"Apache-2.0","copyright":"The Authors","copyright_email":"you@example.com","supports":[{"os-family":"bds"},{"os-name":"mac_os_x","release":"17.*"}],"attributes":[],"groups":[{"id":"controls/example.rb","controls":["(generated from example.rb:7 871cd54043069c5c4f6e382fd5627830)","tmp-1.0","(generated from example.rb:21 2ff474c5357e7070f4c3efa932032dcb)"],"title":"sample section"},{"id":"controls/run_command.rb","controls":["(generated from run_command.rb:5 a411d4ded1530b2f48170840e1127584)"]}],"controls":[{"id":"(generated from example.rb:7 871cd54043069c5c4f6e382fd5627830)","title":null,"desc":null,"descriptions":[],"impact":0.5,"refs":[],"tags":{},"code":"","source_location":{"line":89,"ref":"/Users/jquick/Chef/inspec/lib/inspec/control_eval_context.rb"},"waiver_data":{},"results":[{"status":"passed","code_desc":"File /tmp should be directory","run_time":0.002058,"start_time":"2018-01-05 11:43:04 -0500","resource_params":""}]},{"id":"tmp-1.0","title":"Create /tmp directory","desc":"An optional description...","descriptions":[{"label":"default","data":"An optional description..."}],"impact":0.7,"refs":[],"tags":{},"code":"control 'tmp-1.0' do # A unique ID for this control\n impact 0.7 # The criticality, if this control fails.\n title 'Create /tmp directory' # A human-readable title\n desc 'An optional description...'\n describe file('/tmp') do # The actual test\n it { should be_directory }\n end\nend\n","waiver_data":{},"source_location":{"line":12,"ref":"../inspec-demo/_test/long_commands/controls/example.rb"},"results":[{"status":"passed","code_desc":"File /tmp should be directory","run_time":0.000102,"start_time":"2018-01-05 11:43:04 -0500","resource_params":""}]},{"id":"(generated from example.rb:21 2ff474c5357e7070f4c3efa932032dcb)","title":null,"desc":null,"descriptions":[],"impact":0.5,"refs":[],"tags":{},"code":"","waiver_data":{},"source_location":{"line":89,"ref":"/Users/jquick/Chef/inspec/lib/inspec/control_eval_context.rb"},"results":[{"status":"failed","code_desc":"gem package rubocop should be installed","run_time":0.000168,"start_time":"2018-01-05 11:43:04 -0500","message":"rubocop is not installed","resource_params":""}]},{"id":"(generated from run_command.rb:5 a411d4ded1530b2f48170840e1127584)","title":null,"desc":null,"descriptions":[],"impact":0.5,"refs":[],"tags":{},"code":"","waiver_data":{},"source_location":{"line":89,"ref":"/Users/jquick/Chef/inspec/lib/inspec/control_eval_context.rb"},"results":[{"status":"passed","code_desc":"Command whoami stdout should eq \"jquick\\n\"","run_time":0.034938,"start_time":"2018-01-05 11:43:04 -0500","resource_params":""}]}]}],"statistics":{"duration":0.039182},"version":"1.49.2"}
{"platform":{"name":"mac_os_x","release":"17.2.0"},"profiles":[{"name":"long_commands","version":"0.1.0","sha256":"4f816f8cf18f165f05f1cf20936aaad06a15287de3f578891197647ca05c7df4","title":"InSpec Profile","maintainer":"The Authors","summary":"An InSpec Compliance Profile","license":"Apache-2.0","copyright":"The Authors","copyright_email":"you@example.com","supports":[{"os-family":"bds"},{"os-name":"mac_os_x","release":"17.*"}],"attributes":[],"groups":[{"id":"controls/example.rb","controls":["(generated from example.rb:7 871cd54043069c5c4f6e382fd5627830)","tmp-1.0","(generated from example.rb:21 2ff474c5357e7070f4c3efa932032dcb)"],"title":"sample section"},{"id":"controls/run_command.rb","controls":["(generated from run_command.rb:5 a411d4ded1530b2f48170840e1127584)"]}],"controls":[{"id":"(generated from example.rb:7 871cd54043069c5c4f6e382fd5627830)","title":null,"desc":null,"descriptions":[],"impact":0.5,"refs":[],"tags":{},"code":"","source_location":{"line":89,"ref":"/Users/jquick/Chef/inspec/lib/inspec/control_eval_context.rb"},"waiver_data":{},"results":[{"status":"passed","code_desc":"File /tmp should be directory","run_time":0.002058,"start_time":"2018-01-05 11:43:04 -0500","resource_params":"","resource_id":"File /tmp"}]},{"id":"tmp-1.0","title":"Create /tmp directory","desc":"An optional description...","descriptions":[{"label":"default","data":"An optional description..."}],"impact":0.7,"refs":[],"tags":{},"code":"control 'tmp-1.0' do # A unique ID for this control\n impact 0.7 # The criticality, if this control fails.\n title 'Create /tmp directory' # A human-readable title\n desc 'An optional description...'\n describe file('/tmp') do # The actual test\n it { should be_directory }\n end\nend\n","waiver_data":{},"source_location":{"line":12,"ref":"../inspec-demo/_test/long_commands/controls/example.rb"},"results":[{"status":"passed","code_desc":"File /tmp should be directory","run_time":0.000102,"start_time":"2018-01-05 11:43:04 -0500","resource_params":"","resource_id":"File /tmp"}]},{"id":"(generated from example.rb:21 2ff474c5357e7070f4c3efa932032dcb)","title":null,"desc":null,"descriptions":[],"impact":0.5,"refs":[],"tags":{},"code":"","waiver_data":{},"source_location":{"line":89,"ref":"/Users/jquick/Chef/inspec/lib/inspec/control_eval_context.rb"},"results":[{"status":"failed","code_desc":"gem package rubocop should be installed","run_time":0.000168,"start_time":"2018-01-05 11:43:04 -0500","message":"rubocop is not installed","resource_params":"","resource_id":"gem package rubocop"}]},{"id":"(generated from run_command.rb:5 a411d4ded1530b2f48170840e1127584)","title":null,"desc":null,"descriptions":[],"impact":0.5,"refs":[],"tags":{},"code":"","waiver_data":{},"source_location":{"line":89,"ref":"/Users/jquick/Chef/inspec/lib/inspec/control_eval_context.rb"},"results":[{"status":"passed","code_desc":"Command whoami stdout should eq \"jquick\\n\"","run_time":0.034938,"start_time":"2018-01-05 11:43:04 -0500","resource_params":"","resource_id":"stdout"}]}]}],"statistics":{"duration":0.039182},"version":"1.49.2"}

View file

@ -31,6 +31,7 @@
:run_time: 0.001313935
:start_time: '2018-05-31T16:22:19+05:30'
:resource_params: ''
:resource_id: ''
:statistics:
:duration: 0.002678506
:version: 2.1.83

View file

@ -31,6 +31,7 @@
:run_time: 0.001313935
:start_time: '2018-05-31T16:22:19+05:30'
:resource_params: ''
:resource_id: ''
:statistics:
:duration: 0.002678506
:version: 2.1.83

View file

@ -1299,4 +1299,13 @@ EOT
end
end
describe "when evalutating profiles that reference resource_id" do
let(:run_result) { run_inspec_process("exec #{profile}", json: true) }
let(:profile) { "#{profile_path}/resource_ids" } # A profile with custom resources and test controls that exercise resource ids
it "should evaluate all test controls correctly" do
_(run_result.stderr).must_be_empty
assert_json_controls_passing
end
end
end

View file

@ -19,4 +19,12 @@ describe "inspec schema" do
assert_exit_code 0, out
end
end
describe "validate schema of exec-json" do
it "contains resource_id key" do
out = inspec("schema exec-json")
json_output = JSON.parse(out.stdout)
_(json_output["definitions"]["Control_Result"]["properties"]["resource_id"]).wont_be_nil
end
end
end

View file

@ -56,6 +56,7 @@ describe Inspec::Reporters::Json do
run_time: 0.002058,
start_time: "2018-01-05 11:43:04 -0500",
resource_params: "",
resource_id: "File /tmp",
}
result = report.send(:profile_results, control)
_(result.first).must_equal hash
@ -70,6 +71,7 @@ describe Inspec::Reporters::Json do
run_time: 0.002058,
start_time: "2018-01-05 11:43:04 -0500",
resource_params: "",
resource_id: "File /tmp",
resource: "File",
skip_message: "skipping",
}

View file

@ -44,12 +44,12 @@ describe Inspec::Resource do
end
describe "#example" do
it "will register a description" do
it "will register a example" do
expected = rand.to_s
_(create { example expected }.example).must_equal expected
end
it "can change the description" do
it "can change the example" do
c = create { example rand.to_s }
c.example(x = rand.to_s)
_(c.example).must_equal x
@ -94,4 +94,23 @@ describe Inspec::Resource do
Inspec::Resource.support_registry["os"] = nil
end
end
describe "resource_id" do
it "can set instance variable resource_id and use it" do
cls = create {}
cls_obj = cls.new(nil, "notSoRandomName")
cls_obj.resource_id(x = rand.to_s)
_(cls_obj.resource_id).must_equal x
_(cls_obj.instance_variable_get("@resource_id")).must_equal x
end
it "can change the resource_id value and use it" do
cls = create {}
cls_obj = cls.new(nil, "notSoRandomName")
cls_obj.resource_id(x = rand.to_s)
_(cls_obj.resource_id).must_equal x
cls_obj.resource_id(y = rand.to_s)
_(cls_obj.resource_id).must_equal y
end
end
end