mirror of
https://github.com/inspec/inspec
synced 2024-11-10 07:04:15 +00:00
CFINSPEC-246/CFINSPEC-247 Attestation changes for N/R outcomes (#6222)
* Added attestation file option to read attestation in various formats Signed-off-by: Nikita Mathur <nikita.mathur@chef.io> * Added method to add attestation data on control level Signed-off-by: Nikita Mathur <nikita.mathur@chef.io> * Enhanced outcomes flag to be true when attestation file is passed Signed-off-by: Nikita Mathur <nikita.mathur@chef.io> * Added logic for attestation for reporters Signed-off-by: Nikita Mathur <nikita.mathur@chef.io> * Attestation integration with streaming reporters and lots of refactoring Signed-off-by: Nikita Mathur <nikita.mathur@chef.io> * Support for mitre - with frequency, updated and explanation fields' Signed-off-by: Nikita Mathur <nikita.mathur@chef.io> * To only revise enhanced outcomes when attestation data is present for the control - fix in streaming reporter Signed-off-by: Nikita Mathur <nikita.mathur@chef.io> * Added test cases for attestation and also added validation warnings Signed-off-by: Nikita Mathur <nikita.mathur@chef.io> * Attestation test for different formats of attestation file Signed-off-by: Nikita Mathur <nikita.mathur@chef.io> * Validating presence of status column to be mandtory for attestation files Signed-off-by: Nikita Mathur <nikita.mathur@chef.io> * Build fix Signed-off-by: Nikita Mathur <nikita.mathur@chef.io> * Attestation build fix for windows Signed-off-by: Nikita Mathur <nikita.mathur@chef.io> * No justification and no status graceful handling Signed-off-by: Nikita Mathur <nikita.mathur@chef.io> * New class attestations added for logic and added missing test attestations file Signed-off-by: Nikita Mathur <nikita.mathur@chef.io> * Code comments and cli doc changes for attestation option Signed-off-by: Nikita Mathur <nikita.mathur@chef.io> * Moved logic of attestations and enhanced outcomes to the base of streaming reporter Signed-off-by: Nikita Mathur <nikita.mathur@chef.io> * Attestation documentation added Signed-off-by: Nikita Mathur <nikita.mathur@chef.io> * Added information on what happens if justification is missing in attestation file Signed-off-by: Nikita Mathur <nikita.mathur@chef.io> * Attestation doc changes as per the review Signed-off-by: Nikita Mathur <nikita.mathur@chef.io> * File fields doc changes in attestation doc Signed-off-by: Nikita Mathur <nikita.mathur@chef.io> * Content Review Signed-off-by: Deepa Kumaraswamy <dkumaras@progress.com> * Edits Signed-off-by: Deepa Kumaraswamy <dkumaras@progress.com> * Attestation test changes matching regex and separated logic for expiration using frequency and updated date Signed-off-by: Nikita Mathur <nikita.mathur@chef.io> * Proof-read Signed-off-by: Deepa Kumaraswamy <dkumaras@progress.com> * Name changes for expiry calculation method Signed-off-by: Nikita Mathur <nikita.mathur@chef.io> * Generic tests in attestations for cross platform Signed-off-by: Nikita Mathur <nikita.mathur@chef.io> Signed-off-by: Nikita Mathur <nikita.mathur@chef.io> Signed-off-by: Deepa Kumaraswamy <dkumaras@progress.com> Co-authored-by: Deepa Kumaraswamy <dkumaras@progress.com>
This commit is contained in:
parent
b745e55499
commit
efc6f2c63a
37 changed files with 1017 additions and 45 deletions
179
docs-chef-io/content/inspec/attestations.md
Normal file
179
docs-chef-io/content/inspec/attestations.md
Normal file
|
@ -0,0 +1,179 @@
|
|||
+++
|
||||
title = "Attestations"
|
||||
draft = false
|
||||
gh_repo = "inspec"
|
||||
|
||||
[menu]
|
||||
[menu.inspec]
|
||||
title = "Attestations"
|
||||
identifier = "inspec/reference/attestations.md Attestations"
|
||||
parent = "inspec/reference"
|
||||
weight = 140
|
||||
+++
|
||||
|
||||
Attestations is a mechanism to mark the `Not Reviewed (N/R)` tests as `passed` or `failed` manually using an attestations file.
|
||||
|
||||
## Example
|
||||
|
||||
A fire alarm needs to be audited, but it cannot be reviewed (N/R) through automation. Hence, to audit the fire alarm using an InSpec profile, the outcome of its working must be marked as `passed` or `failed` in a test through manual intervention. By using attestations and passing the status using an attestations file, we can audit the fire alarm.
|
||||
|
||||
### Attestations File to an audit fire alarm
|
||||
|
||||
```yaml
|
||||
fire-alarm-1:
|
||||
expiration_date: 2090-10-1
|
||||
status: passed
|
||||
justification: "Fire alarm 1 was tested manually and it works."
|
||||
fire-alarm-2:
|
||||
expiration_date: 2090-10-1
|
||||
status: failed
|
||||
justification: "Fire alarm 2 was tested manually and it does not work."
|
||||
```
|
||||
|
||||
### InSpec Test
|
||||
|
||||
```ruby
|
||||
control "fire-alarm-1" do
|
||||
only_if("Fire alarm 1 needs to be tested manually") {
|
||||
false
|
||||
}
|
||||
end
|
||||
|
||||
control "fire-alarm-2" do
|
||||
only_if("Fire alarm 2 needs to be tested manually") {
|
||||
false
|
||||
}
|
||||
end
|
||||
```
|
||||
|
||||
### Running attestations to an audit fire alarm
|
||||
|
||||
```bash
|
||||
inspec exec path/to/fire-alarm-audit-profile --attestation-file attestation.yaml
|
||||
|
||||
Profile: InSpec Profile (attestation)
|
||||
Version: 0.1.0
|
||||
Target: local://
|
||||
Target ID: fa3923b9-f806-4cc2-960d-1ddefb4c7654
|
||||
|
||||
✔ fire-alarm-1: No-op (1 skipped)
|
||||
↺ Skipped control due to only_if condition: Fire alarm 1 needs to be tested manually
|
||||
✔ Control Attested : Fire alarm 1 was tested manually and it works.
|
||||
× fire-alarm-2: No-op (1 failed) (1 skipped)
|
||||
↺ Skipped control due to only_if condition: Fire alarm 2 needs to be tested manually
|
||||
× Control Attested : Fire alarm 2 was tested manually and it does not work.
|
||||
|
||||
|
||||
Profile Summary: 1 successful control, 1 control failure, 0 controls not reviewed, 0 controls not applicable, 0 controls have error
|
||||
Test Summary: 1 successful, 1 failure, 2 skipped
|
||||
```
|
||||
|
||||
## Attestations Fields
|
||||
|
||||
An attestations file identifies:
|
||||
|
||||
1. the controls need to be attested.
|
||||
1. an explanation of why it is manually attested.
|
||||
1. control status `passed` or `failed` to attest controls.
|
||||
1. (optional) an URL pointing to a website containing information on control attestation.
|
||||
1. (optional) an expiration date of attestation.
|
||||
|
||||
## Usage
|
||||
|
||||
To use attestations, you must have a correctly formatted attestations file and
|
||||
invoke `inspec exec` with `--attestation-file [path]`.
|
||||
|
||||
```bash
|
||||
inspec exec path/to/profile --attestation-file attestation.yaml
|
||||
```
|
||||
|
||||
## File Format
|
||||
|
||||
Attestations files support YAML, JSON, CSV, XLSX, and XLS formats.
|
||||
|
||||
```yaml
|
||||
control_id:
|
||||
expiration_date: YYYY-MM-DD
|
||||
status: passed
|
||||
justification: "reason for attesting this control"
|
||||
evidence_url: "URL pointing to a website containing information on control attestation"
|
||||
```
|
||||
|
||||
OR
|
||||
|
||||
```json
|
||||
{
|
||||
"control_id": {
|
||||
"expiration_date": "YYYY-MM-DD",
|
||||
"status": "passed",
|
||||
"justification": "reason for attesting this control",
|
||||
"evidence_url": "URL pointing to a website containing information on control attestation"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `status` is mandatory. If absent, the control will not be attested. It can only be `passed` or `failed`.
|
||||
- `expiration_date` sets the day the attestations file expires in **YYYY-MM-DD** format. Attestations files expire at 00:00 at the local time of the system on the specified date. Attestations files without expiration date are permanent. `expiration_date` is optional.
|
||||
- `justification` is a text containing the reason why attestations is required. It might as well as include information on who initiated the attestation. If it is absent, it shows a warning message to include justification in the attestations file.
|
||||
- `evidence_url` is an URL of a website containing information on control attestation. It is optional.
|
||||
|
||||
### File Format Examples
|
||||
|
||||
#### Example in YAML
|
||||
|
||||
```yaml
|
||||
example-3.0.1:
|
||||
justification: "Passed by the auditor manually"
|
||||
evidence_url: "https://www.attestation-info-chef-example/"
|
||||
expiration_date: 2050-06-01
|
||||
status: passed
|
||||
example-3.0.2:
|
||||
justification: "Failed by the auditor manually"
|
||||
evidence_url: "https://www.attestation-info-chef-example/"
|
||||
expiration_date: 2050-07-01
|
||||
status: failed
|
||||
```
|
||||
|
||||
#### Example in JSON
|
||||
|
||||
```json
|
||||
{
|
||||
"example-3.0.1": {
|
||||
"justification": "Passed by the auditor manually",
|
||||
"evidence_url": "https://www.attestation-info-chef-example/",
|
||||
"expiration_date": "2050-06-01",
|
||||
"status": "passed"
|
||||
},
|
||||
"example-3.0.2": {
|
||||
"justification": "Failed by the auditor manually",
|
||||
"evidence_url": "https://www.attestation-info-chef-example/",
|
||||
"expiration_date": "2050-07-01",
|
||||
"status": "failed"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Example in CSV/XLSX/XLS
|
||||
|
||||
These file formats support the following fields in a file:
|
||||
|
||||
- `control_id`
|
||||
_Required_.
|
||||
- `justification`
|
||||
_Required_.
|
||||
- `status`
|
||||
_Required_.
|
||||
- `evidence_url`
|
||||
_Optional_.
|
||||
- `expiration_date`
|
||||
_Optional_.
|
||||
|
||||
![Attestations File Excel Example](/images/inspec/attestations_file_excel.png)
|
||||
|
||||
{{< note >}}
|
||||
|
||||
How is the Attestations mechanism different than Waivers?
|
||||
|
||||
The waivers mechanism skips the controls for various reasons which are required for waiving. Whereas attestations mark the skipped controls which are not reviewed as `passed` or `failed` using the status passed through the attestations file by the auditor.
|
||||
|
||||
{{< /note >}}
|
|
@ -405,6 +405,8 @@ This subcommand has the following additional options:
|
|||
Specify which transport to use, defaults to negotiate (WinRM).
|
||||
* `--enhanced-outcomes`
|
||||
Includes enhanced outcome of controls in report data.
|
||||
* `--attestation-file=one two three`
|
||||
Loads one or more attestation files. Using this option, makes the `enhanced-outcomes` option true.
|
||||
|
||||
## habitat
|
||||
|
||||
|
|
BIN
docs-chef-io/static/images/inspec/attestations_file_excel.png
Normal file
BIN
docs-chef-io/static/images/inspec/attestations_file_excel.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 16 KiB |
50
lib/inspec/attestation_file_reader.rb
Normal file
50
lib/inspec/attestation_file_reader.rb
Normal file
|
@ -0,0 +1,50 @@
|
|||
require "inspec/secrets/yaml"
|
||||
require "inspec/utils/waivers/csv_file_reader"
|
||||
require "inspec/utils/waivers/json_file_reader"
|
||||
require "inspec/utils/waivers/excel_file_reader"
|
||||
|
||||
module Inspec
|
||||
class AttestationFileReader < WaiverFileReader
|
||||
|
||||
# Invoked from rule.rb and streaming reporter base class to fetch attestation data
|
||||
def self.fetch_attestation_by_profile(profile_id, files)
|
||||
read_attestation_from_file(profile_id, files) if @attestation_data.nil? || @attestation_data[profile_id].nil?
|
||||
@attestation_data[profile_id]
|
||||
end
|
||||
|
||||
def self.read_attestation_from_file(profile_id, files)
|
||||
@attestation_data ||= {}
|
||||
output = {}
|
||||
|
||||
files.each do |file_path|
|
||||
data = read_from_file(file_path)
|
||||
output.merge!(data) if !data.nil? && data.is_a?(Hash)
|
||||
|
||||
if data.nil?
|
||||
raise Inspec::Exceptions::AttestationFileNotReadable,
|
||||
"Cannot find parser for attestation file '#{file_path}'. " \
|
||||
"Check to make sure file has the appropriate extension."
|
||||
end
|
||||
end
|
||||
|
||||
@attestation_data[profile_id] = output
|
||||
end
|
||||
|
||||
# Attestation file has different headers than waiver file
|
||||
# Overriding header validation logic of WaiverFileReader
|
||||
def self.validate_headers(headers, json_yaml = false)
|
||||
required_fields = json_yaml ? %w{status} : %w{control_id status}
|
||||
missing_cols = (required_fields - headers)
|
||||
missing_cols << "justification" if (!headers.include? "justification") && (!headers.include? "explanation")
|
||||
|
||||
Inspec::Log.warn "Missing column headers: #{missing_cols}" unless missing_cols.empty?
|
||||
Inspec::Log.warn "Invalid column header: Column can't be nil" if headers.include? nil
|
||||
Inspec::Log.warn "Extra column headers: #{(headers - all_fields)}" unless (headers - all_fields).empty?
|
||||
end
|
||||
|
||||
# defining all fields used in attestation files of different formats
|
||||
def self.all_fields
|
||||
%w{control_id justification expiration_date evidence_url status explanation frequency updated}
|
||||
end
|
||||
end
|
||||
end
|
155
lib/inspec/attestations.rb
Normal file
155
lib/inspec/attestations.rb
Normal file
|
@ -0,0 +1,155 @@
|
|||
module Inspec
|
||||
module Attestations
|
||||
|
||||
# Invoked from reporters base classes & run_data.rb to modify run data
|
||||
def self.attest(run_data)
|
||||
run_data[:profiles].each do |profile|
|
||||
profile[:controls].each do |control|
|
||||
# logic for attestation applied for N/R controls here.
|
||||
if control[:status] == "not_reviewed" && !control[:attestation_data].empty?
|
||||
expiry = determine_expiry(control[:attestation_data], control[:id])
|
||||
# if expiration date parsing was successful
|
||||
if expiry
|
||||
control[:attestation_data]["message"] = validate_attestation_expiry(expiry, control[:id])
|
||||
attestation_result = attestation_check(control[:attestation_data]["message"], control[:attestation_data], control[:id])
|
||||
if attestation_result
|
||||
status, attestation_msg = attestation_result
|
||||
|
||||
control[:status] = status # N/R status -> to passed/failed based on attestation logic
|
||||
|
||||
# replicated test result hash to invoke pass/fail test
|
||||
control[:results].push({
|
||||
status: control[:status],
|
||||
code_desc: attestation_msg,
|
||||
expectation_message: attestation_msg,
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Invoked from streaming reporter base class
|
||||
def self.attest_streaming_data(attestation_data, status, control_id)
|
||||
# logic check for N/R controls here for streaming reporters
|
||||
if status == "not_reviewed" && !attestation_data.blank?
|
||||
expiry = determine_expiry(attestation_data, control_id)
|
||||
|
||||
# if expiration date parsing was successful
|
||||
if expiry
|
||||
expiry_message = validate_attestation_expiry(expiry, control_id)
|
||||
attestation_check(expiry_message, attestation_data, control_id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.validate_attestation_expiry(expiry, control_id)
|
||||
# logic to check for expiry
|
||||
|
||||
if [Date, Time].include?(expiry.class) || (expiry.is_a?(String) && Time.parse(expiry).year != 0)
|
||||
expiry = expiry.to_time if expiry.is_a? Date
|
||||
expiry = Time.parse(expiry) if expiry.is_a? String
|
||||
if expiry < Time.now # If the attestation expired, return - no attestation done
|
||||
expiry_message = "Attestation expired on #{expiry}"
|
||||
expiry_message
|
||||
end
|
||||
else
|
||||
ui = Inspec::UI.new
|
||||
ui.error("Unable to parse attestation expiration date '#{expiry}' for control #{control_id}")
|
||||
ui.exit(:usage_error)
|
||||
end
|
||||
rescue => e
|
||||
ui = Inspec::UI.new
|
||||
ui.error("Unable to parse attestation expiration date '#{expiry}' for control #{control_id}. Error: #{e.message}")
|
||||
ui.exit(:usage_error)
|
||||
end
|
||||
|
||||
def self.attestation_check(expiry_message, attestation_data, control_id)
|
||||
# logic to update enhanced outcome status
|
||||
status, msg = nil
|
||||
if %w{passed failed}.include? attestation_data["status"]
|
||||
if expiry_message
|
||||
status = "failed"
|
||||
msg = "Control not attested : #{expiry_message}"
|
||||
else
|
||||
# use justification and evidence url to show information in msg
|
||||
attestation_message = attestation_data["justification"] || attestation_data["explanation"] || ""
|
||||
|
||||
unless attestation_data["evidence_url"].blank?
|
||||
if attestation_message.blank?
|
||||
attestation_message = "Evidence URL: #{attestation_data["evidence_url"]}"
|
||||
else
|
||||
attestation_message += " | Evidence URL: #{attestation_data["evidence_url"]}"
|
||||
end
|
||||
end
|
||||
|
||||
status = attestation_data["status"]
|
||||
if attestation_message.blank?
|
||||
msg = "Control Attested : No justification provided."
|
||||
else
|
||||
msg = "Control Attested : #{attestation_message}"
|
||||
end
|
||||
end
|
||||
else
|
||||
if attestation_data["status"].blank?
|
||||
Inspec::Log.warn "No attestation status for control #{control_id}. Use 'passed' or 'failed'."
|
||||
else
|
||||
Inspec::Log.warn "Invalid attestation status '#{attestation_data["status"]}' for control #{control_id}. Use 'passed' or 'failed'."
|
||||
end
|
||||
return nil
|
||||
end
|
||||
[status, msg]
|
||||
end
|
||||
|
||||
def self.determine_expiry(attestation_data, control_id)
|
||||
if attestation_data["expiration_date"]
|
||||
attestation_data["expiration_date"]
|
||||
elsif !attestation_data["updated"].blank? && !attestation_data["frequency"].blank?
|
||||
begin
|
||||
calculate_expiry(attestation_data["updated"], attestation_data["frequency"], control_id)
|
||||
rescue => e
|
||||
ui = Inspec::UI.new
|
||||
ui.error("Unable to parse attestation updated date '#{attestation_data["updated"]}' for control #{control_id}. Error: #{e.message}")
|
||||
ui.exit(:usage_error)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.calculate_expiry(updated_date, frequency, control_id)
|
||||
# logic to find expiration date using frequency and updated date.
|
||||
if updated_date.is_a?(Date) || (updated_date.is_a?(String) && Date.parse(updated_date).year != 0)
|
||||
updated_date = Date.parse(updated_date) if updated_date.is_a? String
|
||||
if updated_date < Time.now.to_date
|
||||
case frequency
|
||||
when "annually"
|
||||
updated_date.to_date.next_year(1)
|
||||
when "semiannually"
|
||||
updated_date.next_month(6)
|
||||
when "quarterly"
|
||||
updated_date.next_month(3)
|
||||
when "monthly"
|
||||
updated_date.next_month(1)
|
||||
when "every2weeks"
|
||||
updated_date.next_day(14)
|
||||
when "weekly"
|
||||
updated_date.next_day(7)
|
||||
when "every3days"
|
||||
updated_date.next_day(3)
|
||||
when "daily"
|
||||
updated_date.next_day(1)
|
||||
else
|
||||
Inspec::Log.warn "Invalid frequency value '#{frequency}' for control #{control_id}."
|
||||
updated_date
|
||||
end
|
||||
else
|
||||
updated_date
|
||||
end
|
||||
else
|
||||
ui = Inspec::UI.new
|
||||
ui.error("Unable to parse attestation updated date '#{updated_date}' for control #{control_id}")
|
||||
ui.exit(:usage_error)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -177,6 +177,8 @@ module Inspec
|
|||
desc: "Load one or more input files, a YAML file with values for the profile to use"
|
||||
option :waiver_file, type: :array,
|
||||
desc: "Load one or more waiver files."
|
||||
option :attestation_file, type: :array,
|
||||
desc: "Load one or more attestation files."
|
||||
option :attrs, type: :array,
|
||||
desc: "Legacy name for --input-file - deprecated."
|
||||
option :create_lockfile, type: :boolean,
|
||||
|
|
|
@ -14,5 +14,6 @@ module Inspec
|
|||
"passed"
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
|
|
@ -12,5 +12,7 @@ module Inspec
|
|||
class ProfileSigningKeyNotFound < ArgumentError; end
|
||||
class WaiversFileNotReadable < ArgumentError; end
|
||||
class WaiversFileDoesNotExist < ArgumentError; end
|
||||
class AttestationFileNotReadable < ArgumentError; end
|
||||
class AttestationFileDoesNotExist < ArgumentError; end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -160,7 +160,7 @@ module Inspec::Formatters
|
|||
end
|
||||
|
||||
# added this additionally because stats summary is also used for determining exit code in runner rspec
|
||||
skipped += 1 if control[:results].any? { |r| r[:status] == "skipped" }
|
||||
skipped += 1 if control[:results] && (control[:results].any? { |r| r[:status] == "skipped" })
|
||||
|
||||
end
|
||||
total = error + not_applicable + not_reviewed + failed + passed
|
||||
|
@ -236,6 +236,7 @@ module Inspec::Formatters
|
|||
resource_title: example.metadata[:described_class] || example.metadata[:example_group][:description],
|
||||
expectation_message: format_expectation_message(example),
|
||||
waiver_data: example.metadata[:waiver_data],
|
||||
attestation_data: example.metadata[:attestation_data],
|
||||
# This enforces the resource name as expected based off of the class
|
||||
# name. However, if we wanted the `name` attribute against the class
|
||||
# to be canonical for this case (consider edge cases!) we would use
|
||||
|
@ -358,6 +359,7 @@ module Inspec::Formatters
|
|||
# (that is, per-describe-block) basis, because that is the only granularity
|
||||
# available to us in the RSpec report data structure which we use as a vehicle.
|
||||
control[:waiver_data] ||= example[:waiver_data] || {}
|
||||
control[:attestation_data] ||= example[:attestation_data] || {}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
require "inspec/attestations"
|
||||
module Inspec::Plugin::V2::PluginType
|
||||
class StreamingReporter < Inspec::Plugin::V2::PluginBase
|
||||
register_plugin_type(:streaming_reporter)
|
||||
|
@ -12,6 +13,8 @@ module Inspec::Plugin::V2::PluginType
|
|||
@control_checks_count_map = {}
|
||||
@controls_count = nil
|
||||
@notifications = {}
|
||||
@enhanced_outcome_control_wise = {}
|
||||
@attestation_message_control_wise = {}
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -29,16 +32,57 @@ module Inspec::Plugin::V2::PluginType
|
|||
|
||||
# method to identify when the control ended running
|
||||
# this will be useful in executing operations on control's level end
|
||||
def control_ended?(control_id)
|
||||
def control_ended?(notification, control_id)
|
||||
set_control_checks_count_map_value
|
||||
unless @control_checks_count_map[control_id].nil?
|
||||
@control_checks_count_map[control_id] -= 1
|
||||
@control_checks_count_map[control_id] == 0
|
||||
control_ended = @control_checks_count_map[control_id] == 0
|
||||
# after a control has ended it checks for certain operations, like enhanced outcomes & attestations
|
||||
run_control_operations(notification, control_id) if control_ended
|
||||
control_ended
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
def run_control_operations(notification, control_id)
|
||||
check_for_enhanced_outcomes(notification, control_id)
|
||||
check_for_attestation(notification, control_id)
|
||||
end
|
||||
|
||||
def check_for_enhanced_outcomes(notification, control_id)
|
||||
if enhanced_outcomes
|
||||
control_outcome = add_enhanced_outcomes(control_id)
|
||||
@enhanced_outcome_control_wise[control_id] = control_outcome
|
||||
end
|
||||
end
|
||||
|
||||
def check_for_attestation(notification, control_id)
|
||||
control_outcome = control_outcome(control_id)
|
||||
if control_outcome
|
||||
attestation_result = attest_control(notification, control_id, control_outcome)
|
||||
unless attestation_result.blank?
|
||||
@enhanced_outcome_control_wise[control_id] = attestation_result[0]
|
||||
@attestation_message_control_wise[control_id] = attestation_result[1]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def format_message(indicator, control_id, title, full_description)
|
||||
message_to_format = ""
|
||||
message_to_format += "#{indicator} "
|
||||
message_to_format += "#{control_id.to_s.strip.dup.force_encoding(Encoding::UTF_8)} "
|
||||
message_to_format += "#{title.gsub(/\n*\s+/, " ").to_s.force_encoding(Encoding::UTF_8)} " if title
|
||||
message_to_format += "#{full_description.gsub(/\n*\s+/, " ").to_s.force_encoding(Encoding::UTF_8)} " unless title
|
||||
# append attestation message if control is attested
|
||||
message_to_format += "#{@attestation_message_control_wise[control_id].gsub(/\n*\s+/, " ").to_s.force_encoding(Encoding::UTF_8)} " if @attestation_message_control_wise[control_id]
|
||||
message_to_format
|
||||
end
|
||||
|
||||
def control_outcome(control_id)
|
||||
@enhanced_outcome_control_wise[control_id]
|
||||
end
|
||||
|
||||
# method to identify total no. of controls
|
||||
def controls_count
|
||||
@controls_count ||= RSpec.configuration.formatters.grep(Inspec::Formatters::Base).first.get_controls_count
|
||||
|
@ -103,5 +147,23 @@ module Inspec::Plugin::V2::PluginType
|
|||
@notifications[control_id].push([notification, status])
|
||||
end
|
||||
end
|
||||
|
||||
def attest_control(notification, control_id, control_outcome)
|
||||
status = control_outcome
|
||||
attestation_data = read_attestation_file(notification, control_id)
|
||||
Inspec::Attestations.attest_streaming_data(attestation_data, status, control_id) unless attestation_data.blank?
|
||||
end
|
||||
|
||||
def read_attestation_file(notification, control_id)
|
||||
# need to re-read the file from config since not using run data for streaming reporters.
|
||||
profile_id = notification.example.metadata[:profile_id]
|
||||
attestation_files = Inspec::Config.cached.final_options["attestation_file"] if Inspec::Config.cached.respond_to?(:final_options)
|
||||
|
||||
attestation_data_by_profile = Inspec::AttestationFileReader.fetch_attestation_by_profile(profile_id, attestation_files) unless attestation_files.nil?
|
||||
|
||||
return unless attestation_data_by_profile && attestation_data_by_profile[control_id] && attestation_data_by_profile[control_id].is_a?(Hash)
|
||||
|
||||
attestation_data_by_profile[control_id]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
require_relative "../utils/run_data_filters"
|
||||
require "inspec/attestations"
|
||||
|
||||
module Inspec::Reporters
|
||||
class Base
|
||||
|
@ -12,6 +13,8 @@ module Inspec::Reporters
|
|||
@run_data = config[:run_data] || {}
|
||||
apply_run_data_filters_to_hash
|
||||
|
||||
# only try for attestation when attestation file is passed
|
||||
Inspec::Attestations.attest(@run_data) if Inspec::Config.cached[:attestation_file]
|
||||
@output = ""
|
||||
end
|
||||
|
||||
|
|
|
@ -317,7 +317,7 @@ module Inspec::Reporters
|
|||
not_applicable = 0
|
||||
|
||||
all_unique_controls.each do |control|
|
||||
next if control[:status].empty?
|
||||
next if control[:status].blank?
|
||||
|
||||
if control[:status] == "failed"
|
||||
failed += 1
|
||||
|
|
|
@ -9,6 +9,7 @@ require "inspec/resource"
|
|||
require "inspec/resources/os"
|
||||
require "inspec/input_registry"
|
||||
require "inspec/waiver_file_reader"
|
||||
require "inspec/attestation_file_reader"
|
||||
require "inspec/utils/convert"
|
||||
|
||||
module Inspec
|
||||
|
@ -16,6 +17,7 @@ module Inspec
|
|||
include ::RSpec::Matchers
|
||||
|
||||
attr_reader :__waiver_data
|
||||
attr_reader :__attestation_data
|
||||
attr_accessor :resource_dsl
|
||||
attr_reader :__profile_id
|
||||
|
||||
|
@ -50,6 +52,7 @@ module Inspec
|
|||
# By applying waivers *after* the instance eval, we assure that
|
||||
# waivers have higher precedence than only_if.
|
||||
__apply_waivers
|
||||
__add_attestation_data
|
||||
|
||||
rescue SystemStackError, StandardError => e
|
||||
# We've encountered an exception while trying to eval the code inside the
|
||||
|
@ -392,6 +395,19 @@ module Inspec
|
|||
__waiver_data["skipped_due_to_waiver"] = true
|
||||
end
|
||||
|
||||
# fetches attestation data for the rule which is used in runner_rspec.rb to assign it inside metadata
|
||||
def __add_attestation_data
|
||||
# this adds attestation data to a rule, accesible on run data layer.
|
||||
control_id = @__rule_id
|
||||
attestation_files = Inspec::Config.cached.final_options["attestation_file"] if Inspec::Config.cached.respond_to?(:final_options)
|
||||
|
||||
attestation_data_by_profile = Inspec::AttestationFileReader.fetch_attestation_by_profile(__profile_id, attestation_files) unless attestation_files.nil?
|
||||
|
||||
return unless attestation_data_by_profile && attestation_data_by_profile[control_id] && attestation_data_by_profile[control_id].is_a?(Hash)
|
||||
|
||||
@__attestation_data = attestation_data_by_profile[control_id]
|
||||
end
|
||||
|
||||
#
|
||||
# Takes a block and returns a block that will run the given block
|
||||
# with access to the resource_dsl of the current class. This is to
|
||||
|
|
|
@ -27,11 +27,16 @@ module Inspec
|
|||
) do
|
||||
include HashLikeStruct
|
||||
def initialize(raw_run_data)
|
||||
self.controls = raw_run_data[:controls].map { |c| Inspec::RunData::Control.new(c) }
|
||||
self.profiles = raw_run_data[:profiles].map { |p| Inspec::RunData::Profile.new(p) }
|
||||
self.statistics = Inspec::RunData::Statistics.new(raw_run_data[:statistics])
|
||||
self.platform = Inspec::RunData::Platform.new(raw_run_data[:platform])
|
||||
self.version = raw_run_data[:version]
|
||||
@raw_run_data = raw_run_data
|
||||
|
||||
# only try for attestation when attestation file is passed
|
||||
Inspec::Attestations.attest(@raw_run_data) if Inspec::Config.cached[:attestation_file]
|
||||
|
||||
self.controls = @raw_run_data[:controls].map { |c| Inspec::RunData::Control.new(c) }
|
||||
self.profiles = @raw_run_data[:profiles].map { |p| Inspec::RunData::Profile.new(p) }
|
||||
self.statistics = Inspec::RunData::Statistics.new(@raw_run_data[:statistics])
|
||||
self.platform = Inspec::RunData::Platform.new(@raw_run_data[:platform])
|
||||
self.version = @raw_run_data[:version]
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -13,7 +13,8 @@ module Inspec
|
|||
:source_location, # Complex local
|
||||
:tags, # Hash with custom keys
|
||||
:title, # String
|
||||
:waiver_data # Complex local
|
||||
:waiver_data, # Complex local
|
||||
:attestation_data # Complex local
|
||||
) do
|
||||
include HashLikeStruct
|
||||
def initialize(raw_ctl_data)
|
||||
|
@ -21,6 +22,7 @@ module Inspec
|
|||
self.results = (raw_ctl_data[:results] || []).map { |r| Inspec::RunData::Result.new(r) }
|
||||
self.source_location = Inspec::RunData::Control::SourceLocation.new(raw_ctl_data[:source_location] || {})
|
||||
self.waiver_data = Inspec::RunData::Control::WaiverData.new(raw_ctl_data[:waiver_data] || {})
|
||||
self.attestation_data = Inspec::RunData::Control::AttestationData.new(raw_ctl_data[:attestation_data] || {})
|
||||
|
||||
[
|
||||
:code, # String
|
||||
|
@ -84,6 +86,26 @@ module Inspec
|
|||
}.each { |f| self[f] = raw_wv_data[f.to_s] }
|
||||
end
|
||||
end
|
||||
|
||||
AttestationData = Struct.new(
|
||||
:expiration_date,
|
||||
:justification,
|
||||
:evidence_url,
|
||||
:status,
|
||||
:message
|
||||
) do
|
||||
include HashLikeStruct
|
||||
def initialize(raw_attestation_data)
|
||||
# These have string keys in the raw data!
|
||||
%i{
|
||||
expiration_date
|
||||
justification
|
||||
evidence_url
|
||||
status
|
||||
message
|
||||
}.each { |f| self[f] = raw_attestation_data[f.to_s] }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -67,6 +67,14 @@ module Inspec
|
|||
end
|
||||
end
|
||||
|
||||
if @conf[:attestation_file]
|
||||
@conf[:attestation_file].each do |file|
|
||||
unless File.file?(file)
|
||||
raise Inspec::Exceptions::AttestationFileDoesNotExist, "Attestation file #{file} does not exist."
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# About reading inputs:
|
||||
# @conf gets passed around a lot, eventually to
|
||||
# Inspec::InputRegistry.register_external_inputs.
|
||||
|
@ -168,7 +176,9 @@ module Inspec
|
|||
return if @conf["reporter"].nil?
|
||||
|
||||
@conf["reporter"].each do |reporter|
|
||||
result = Inspec::Reporters.render(reporter, run_data, @conf["enhanced_outcomes"])
|
||||
# if attestation file is used then we need enhanced outcomes
|
||||
enhanced_outcome_flag = @conf["attestation_file"] ? true : @conf["enhanced_outcomes"]
|
||||
result = Inspec::Reporters.render(reporter, run_data, enhanced_outcome_flag)
|
||||
raise Inspec::ReporterError, "Error generating reporter '#{reporter[0]}'" if result == false
|
||||
end
|
||||
end
|
||||
|
|
|
@ -196,7 +196,9 @@ module Inspec
|
|||
def configure_output
|
||||
RSpec.configuration.output_stream = $stdout
|
||||
@formatter = RSpec.configuration.add_formatter(Inspec::Formatters::Base)
|
||||
@formatter.enhanced_outcomes = @conf.final_options["enhanced_outcomes"]
|
||||
|
||||
# if attestation file is used then we need enhanced outcomes
|
||||
@formatter.enhanced_outcomes = @conf.final_options["attestation_file"] ? true : @conf.final_options["enhanced_outcomes"]
|
||||
RSpec.configuration.add_formatter(Inspec::Formatters::ShowProgress, $stderr) if @conf[:show_progress]
|
||||
set_optional_formatters
|
||||
RSpec.configuration.color = @conf["color"]
|
||||
|
@ -228,6 +230,7 @@ module Inspec
|
|||
metadata[:code] = rule.instance_variable_get(:@__code)
|
||||
metadata[:source_location] = rule.instance_variable_get(:@__source_location)
|
||||
metadata[:waiver_data] = rule.__waiver_data
|
||||
metadata[:attestation_data] = rule.__attestation_data # data fetched from rule object
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -16,24 +16,8 @@ module Inspec
|
|||
output = {}
|
||||
|
||||
files.each do |file_path|
|
||||
file_extension = File.extname(file_path)
|
||||
data = nil
|
||||
if [".yaml", ".yml"].include? file_extension
|
||||
data = Secrets::YAML.resolve(file_path)
|
||||
data = data.inputs unless data.nil?
|
||||
validate_json_yaml(data)
|
||||
elsif file_extension == ".csv"
|
||||
data = Waivers::CSVFileReader.resolve(file_path)
|
||||
headers = Waivers::CSVFileReader.headers
|
||||
validate_headers(headers)
|
||||
elsif file_extension == ".json"
|
||||
data = Waivers::JSONFileReader.resolve(file_path)
|
||||
validate_json_yaml(data)
|
||||
elsif [".xls", ".xlsx"].include? file_extension
|
||||
data = Waivers::ExcelFileReader.resolve(file_path)
|
||||
headers = Waivers::ExcelFileReader.headers
|
||||
validate_headers(headers)
|
||||
end
|
||||
data = read_from_file(file_path)
|
||||
|
||||
output.merge!(data) if !data.nil? && data.is_a?(Hash)
|
||||
|
||||
if data.nil?
|
||||
|
@ -46,10 +30,34 @@ module Inspec
|
|||
@waivers_data[profile_id] = output
|
||||
end
|
||||
|
||||
def self.read_from_file(file_path)
|
||||
data = nil
|
||||
file_extension = File.extname(file_path)
|
||||
if [".yaml", ".yml"].include? file_extension
|
||||
data = Secrets::YAML.resolve(file_path)
|
||||
data = data.inputs unless data.nil?
|
||||
validate_json_yaml(data)
|
||||
elsif file_extension == ".csv"
|
||||
data = Waivers::CSVFileReader.resolve(file_path)
|
||||
headers = Waivers::CSVFileReader.headers
|
||||
validate_headers(headers)
|
||||
elsif file_extension == ".json"
|
||||
data = Waivers::JSONFileReader.resolve(file_path)
|
||||
validate_json_yaml(data)
|
||||
elsif [".xls", ".xlsx"].include? file_extension
|
||||
data = Waivers::ExcelFileReader.resolve(file_path)
|
||||
headers = Waivers::ExcelFileReader.headers
|
||||
validate_headers(headers)
|
||||
end
|
||||
data
|
||||
end
|
||||
|
||||
def self.all_fields
|
||||
%w{control_id justification expiration_date run}
|
||||
end
|
||||
|
||||
def self.validate_headers(headers, json_yaml = false)
|
||||
required_fields = json_yaml ? %w{justification} : %w{control_id justification}
|
||||
all_fields = %w{control_id justification expiration_date run}
|
||||
|
||||
Inspec::Log.warn "Missing column headers: #{(required_fields - headers)}" unless (required_fields - headers).empty?
|
||||
Inspec::Log.warn "Invalid column header: Column can't be nil" if headers.include? nil
|
||||
Inspec::Log.warn "Extra column headers: #{(headers - all_fields)}" unless (headers - all_fields).empty?
|
||||
|
|
|
@ -85,23 +85,20 @@ module InspecPlugins::StreamingReporterProgressBar
|
|||
full_description = notification.example.metadata[:full_description]
|
||||
set_status_mapping(control_id, status)
|
||||
collect_notifications(notification, control_id, status)
|
||||
control_ended = control_ended?(control_id)
|
||||
if control_ended
|
||||
control_outcome = add_enhanced_outcomes(control_id) if enhanced_outcomes
|
||||
show_progress(control_id, title, full_description, control_outcome)
|
||||
end
|
||||
show_progress(control_id, title, full_description) if control_ended?(notification, control_id)
|
||||
end
|
||||
|
||||
def show_progress(control_id, title, full_description, control_outcome)
|
||||
def show_progress(control_id, title, full_description)
|
||||
@bar ||= ProgressBar.new(controls_count, :bar, :counter, :percentage)
|
||||
sleep 0.1
|
||||
@bar.increment!
|
||||
@bar.puts format_it(control_id, title, full_description, control_outcome)
|
||||
@bar.puts format_it(control_id, title, full_description)
|
||||
rescue StandardError => e
|
||||
raise "Exception in Progress Bar streaming reporter: #{e}"
|
||||
end
|
||||
|
||||
def format_it(control_id, title, full_description, control_outcome)
|
||||
def format_it(control_id, title, full_description)
|
||||
control_outcome = control_outcome(control_id)
|
||||
if control_outcome
|
||||
control_status = control_outcome
|
||||
else
|
||||
|
@ -115,11 +112,7 @@ module InspecPlugins::StreamingReporterProgressBar
|
|||
end
|
||||
end
|
||||
indicator = INDICATORS[control_status]
|
||||
message_to_format = ""
|
||||
message_to_format += "#{indicator} "
|
||||
message_to_format += "#{control_id.to_s.strip.dup.force_encoding(Encoding::UTF_8)} "
|
||||
message_to_format += "#{title.gsub(/\n*\s+/, " ").to_s.force_encoding(Encoding::UTF_8)} " if title
|
||||
message_to_format += "#{full_description.gsub(/\n*\s+/, " ").to_s.force_encoding(Encoding::UTF_8)} " unless title
|
||||
message_to_format = format_message(indicator, control_id, title, full_description)
|
||||
format_with_color(control_status, message_to_format)
|
||||
rescue Exception => e
|
||||
raise "Exception in show_progress: #{e}"
|
||||
|
|
93
test/fixtures/profiles/attestation/controls/example.rb
vendored
Normal file
93
test/fixtures/profiles/attestation/controls/example.rb
vendored
Normal file
|
@ -0,0 +1,93 @@
|
|||
# Error
|
||||
control "tmp-1.0.1" do
|
||||
impact 0.7
|
||||
describe "a.1" do
|
||||
it { should_bot "a.1" }
|
||||
end
|
||||
end
|
||||
|
||||
control "tmp-1.0.2" do
|
||||
impact 0.0
|
||||
describe "a.2" do
|
||||
it { should_bot "a.2" }
|
||||
end
|
||||
end
|
||||
|
||||
# Not Applicable
|
||||
control "tmp-2.0.1" do
|
||||
impact 0.0
|
||||
describe "b.1" do
|
||||
it { should cmp "b.1" }
|
||||
end
|
||||
end
|
||||
|
||||
control "tmp-2.0.2" do
|
||||
impact 0.0
|
||||
only_if { false }
|
||||
describe "b.2" do
|
||||
it { should cmp "b.2" }
|
||||
end
|
||||
end
|
||||
|
||||
# Not Reviewed
|
||||
control "tmp-3.0.1" do
|
||||
only_if { false }
|
||||
describe "c.1" do
|
||||
it { should cmp "c.1" }
|
||||
end
|
||||
end
|
||||
|
||||
control "tmp-3.0.2" do
|
||||
only_if { false }
|
||||
describe "c.2" do
|
||||
it { should_bot "c.2" }
|
||||
end
|
||||
end
|
||||
|
||||
control "tmp-3.0.3" do
|
||||
only_if { false }
|
||||
describe "c.2" do
|
||||
it { should_bot "c.2" }
|
||||
end
|
||||
end
|
||||
|
||||
control "tmp-3.0.4" do
|
||||
only_if { false }
|
||||
describe "c.2" do
|
||||
it { should_bot "c.2" }
|
||||
end
|
||||
end
|
||||
|
||||
# Failed
|
||||
control "tmp-4.0" do
|
||||
impact 0.7
|
||||
describe "d.1" do
|
||||
it { should_not cmp "d.1" }
|
||||
it { should cmp "d.1" }
|
||||
end
|
||||
end
|
||||
|
||||
# Passed
|
||||
control "tmp-5.0" do
|
||||
impact 0.7
|
||||
describe "e.1" do
|
||||
it { should cmp "e.1" }
|
||||
end
|
||||
end
|
||||
|
||||
# Example of setting impact using code and marking it N/A
|
||||
control "tmp-6.0.1" do
|
||||
impact 0.5
|
||||
only_if("Some reason for N/A", impact: 0.0) { false }
|
||||
describe "f.1" do
|
||||
it { should cmp "f.1" }
|
||||
end
|
||||
end
|
||||
|
||||
# Example of setting impact using code and not marked as N/A
|
||||
control "tmp-6.0.2" do
|
||||
only_if(impact: 0.5) { false }
|
||||
describe "f.2" do
|
||||
it { should cmp "f.2" }
|
||||
end
|
||||
end
|
5
test/fixtures/profiles/attestation/files/attestations.csv
vendored
Normal file
5
test/fixtures/profiles/attestation/files/attestations.csv
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
control_id,justification,explanation,evidence_url,status,expiration_date,updated,frequency
|
||||
tmp-3.0.1,Sound reasoning,,Dummy url,failed,2001-06-01T00:00:00.000Z,,,
|
||||
tmp-3.0.2,,Unassailable thinking,Dummy url,failed,,2021-06-01T00:00:00.000Z,semiannually
|
||||
tmp-4.0,Sheer cleverness,,Dummy url,passed,2050-06-01T00:00:00.000Z,,,
|
||||
tmp-6.0.2,Sheer cleverness,,Dummy url,passed,2050-06-01T00:00:00.000Z,,,
|
|
27
test/fixtures/profiles/attestation/files/attestations.json
vendored
Normal file
27
test/fixtures/profiles/attestation/files/attestations.json
vendored
Normal file
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"tmp-3.0.1": {
|
||||
"explanation": "Sound reasoning",
|
||||
"evidence_url": "Dummy url",
|
||||
"status": "failed",
|
||||
"updated": "2021-06-01T00:00:00.000Z",
|
||||
"frequency": "semiannually"
|
||||
},
|
||||
"tmp-3.0.2": {
|
||||
"justification": "Unassailable thinking",
|
||||
"evidence_url": "Dummy url",
|
||||
"expiration_date": "2001-06-01T00:00:00.000Z",
|
||||
"status": "passed"
|
||||
},
|
||||
"tmp-4.0": {
|
||||
"justification": "Sheer cleverness",
|
||||
"evidence_url": "Dummy url",
|
||||
"expiration_date": "2050-06-01T00:00:00.000Z",
|
||||
"status": "passed"
|
||||
},
|
||||
"tmp-6.0.2": {
|
||||
"justification": "Sheer cleverness",
|
||||
"evidence_url": "Dummy url",
|
||||
"expiration_date": "2050-06-01T00:00:00.000Z",
|
||||
"status": "passed"
|
||||
}
|
||||
}
|
BIN
test/fixtures/profiles/attestation/files/attestations.xls
vendored
Normal file
BIN
test/fixtures/profiles/attestation/files/attestations.xls
vendored
Normal file
Binary file not shown.
BIN
test/fixtures/profiles/attestation/files/attestations.xlsx
vendored
Normal file
BIN
test/fixtures/profiles/attestation/files/attestations.xlsx
vendored
Normal file
Binary file not shown.
24
test/fixtures/profiles/attestation/files/attestations.yaml
vendored
Normal file
24
test/fixtures/profiles/attestation/files/attestations.yaml
vendored
Normal file
|
@ -0,0 +1,24 @@
|
|||
tmp-3.0.1:
|
||||
explanation: Sound reasoning
|
||||
evidence_url: Dummy url
|
||||
status: failed
|
||||
updated: 2021-06-01
|
||||
frequency: semiannually
|
||||
|
||||
tmp-3.0.2:
|
||||
justification: Unassailable thinking
|
||||
evidence_url: Dummy url
|
||||
expiration_date: 2001-06-01
|
||||
status: passed
|
||||
|
||||
tmp-4.0:
|
||||
justification: Sheer cleverness
|
||||
evidence_url: Dummy url
|
||||
expiration_date: 2050-06-01
|
||||
status: passed
|
||||
|
||||
tmp-6.0.2:
|
||||
justification: Sheer cleverness
|
||||
evidence_url: Dummy url
|
||||
expiration_date: 2050-06-01
|
||||
status: passed
|
5
test/fixtures/profiles/attestation/files/bad-date.yaml
vendored
Normal file
5
test/fixtures/profiles/attestation/files/bad-date.yaml
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
tmp-3.0.2:
|
||||
justification: Unassailable thinking
|
||||
evidence_url: Dummy url
|
||||
expiration_date: bad date
|
||||
status: passed
|
6
test/fixtures/profiles/attestation/files/bad-update-date.yaml
vendored
Normal file
6
test/fixtures/profiles/attestation/files/bad-update-date.yaml
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
tmp-3.0.1:
|
||||
justification: Sound reasoning
|
||||
evidence_url: Dummy url
|
||||
status: failed
|
||||
updated: bad date
|
||||
frequency: semiannually
|
6
test/fixtures/profiles/attestation/files/invalid-frequency.yaml
vendored
Normal file
6
test/fixtures/profiles/attestation/files/invalid-frequency.yaml
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
tmp-3.0.4:
|
||||
justification: Sound reasoning
|
||||
evidence_url: Dummy url
|
||||
status: failed
|
||||
updated: 2021-06-01
|
||||
frequency: biweekly
|
5
test/fixtures/profiles/attestation/files/invalid-status.yaml
vendored
Normal file
5
test/fixtures/profiles/attestation/files/invalid-status.yaml
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
tmp-3.0.3:
|
||||
justification: Unassailable thinking
|
||||
evidence_url: Dummy url
|
||||
expiration_date: 2050-06-01
|
||||
status: pass
|
7
test/fixtures/profiles/attestation/files/no-justification.yaml
vendored
Normal file
7
test/fixtures/profiles/attestation/files/no-justification.yaml
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
tmp-3.0.1:
|
||||
expiration_date: 2050-06-01
|
||||
status: passed
|
||||
tmp-3.0.2:
|
||||
evidence_url: Dummy url
|
||||
expiration_date: 2050-06-01
|
||||
status: passed
|
4
test/fixtures/profiles/attestation/files/no-status.yaml
vendored
Normal file
4
test/fixtures/profiles/attestation/files/no-status.yaml
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
tmp-3.0.3:
|
||||
justification: Unassailable thinking
|
||||
evidence_url: Dummy url
|
||||
expiration_date: 2050-06-01
|
8
test/fixtures/profiles/attestation/files/wrong-headers.csv
vendored
Normal file
8
test/fixtures/profiles/attestation/files/wrong-headers.csv
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
control_id_random,justification_random,run_random,expiration_date_random,,,
|
||||
03_waivered_no_expiry_ran_passes,Sound reasoning,TRUE,,,,
|
||||
04_waivered_no_expiry_ran_fails,Unassailable thinking,TRUE,2077-11-10T00:00:00Z,,,
|
||||
,,,,,,
|
||||
05_waivered_no_expiry_not_ran,Sheer cleverness,FALSE,,,,
|
||||
06_waivered_expiry_in_past_ran_passes,Necessity,TRUE,,,,
|
||||
14_waivered_expiry_in_future_z_not_ran,Lack of imagination,FALSE,2077-11-10T00:00:00Z,,,
|
||||
random contorl id with no data,,,,,,random data in csv!
|
|
21
test/fixtures/profiles/attestation/files/wrong-headers.json
vendored
Normal file
21
test/fixtures/profiles/attestation/files/wrong-headers.json
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"tmp-3.0.1": {
|
||||
"justification": "Sound reasoning",
|
||||
"evidence_url": "Dummy url",
|
||||
"expiration_date": "2050-06-01T00:00:00.000Z",
|
||||
"status": "passed",
|
||||
"random": "haha"
|
||||
},
|
||||
"tmp-3.0.2": {
|
||||
"justification": "Unassailable thinking",
|
||||
"evidence_url": "Dummy url",
|
||||
"expiration_date": "2050-06-01T00:00:00.000Z",
|
||||
"status": "passed"
|
||||
},
|
||||
"tmp-4.0": {
|
||||
"justification": "Sheer cleverness",
|
||||
"evidence_url": "Dummy url",
|
||||
"expiration_date": "2050-06-01T00:00:00.000Z",
|
||||
"status": "passed"
|
||||
}
|
||||
}
|
BIN
test/fixtures/profiles/attestation/files/wrong-headers.xlsx
vendored
Normal file
BIN
test/fixtures/profiles/attestation/files/wrong-headers.xlsx
vendored
Normal file
Binary file not shown.
18
test/fixtures/profiles/attestation/files/wrong-headers.yaml
vendored
Normal file
18
test/fixtures/profiles/attestation/files/wrong-headers.yaml
vendored
Normal file
|
@ -0,0 +1,18 @@
|
|||
tmp-3.0.1:
|
||||
justification: Sound reasoning
|
||||
evidence_url: Dummy url
|
||||
expiration_date: 2050-06-01
|
||||
status: passed
|
||||
random: haha
|
||||
|
||||
tmp-3.0.2:
|
||||
justification: Unassailable thinking
|
||||
evidence_url: Dummy url
|
||||
expiration_date: 2050-06-01
|
||||
status: passed
|
||||
|
||||
tmp-4.0:
|
||||
justification: Sheer cleverness
|
||||
evidence_url: Dummy url
|
||||
expiration_date: 2050-06-01
|
||||
status: passed
|
10
test/fixtures/profiles/attestation/inspec.yml
vendored
Normal file
10
test/fixtures/profiles/attestation/inspec.yml
vendored
Normal file
|
@ -0,0 +1,10 @@
|
|||
name: attestation
|
||||
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
|
218
test/functional/attestation_test.rb
Normal file
218
test/functional/attestation_test.rb
Normal file
|
@ -0,0 +1,218 @@
|
|||
require "functional/helper"
|
||||
|
||||
describe "attestations" do
|
||||
include FunctionalHelper
|
||||
|
||||
parallelize_me!
|
||||
|
||||
let(:attestation_profile) { "#{profile_path}/attestation" }
|
||||
let(:run_result) { run_inspec_process(cmd) }
|
||||
let(:cmd) { "exec #{attestation_profile} --attestation-file #{attestation_profile}/files/#{attestation_file}" }
|
||||
|
||||
attr_accessor :out
|
||||
|
||||
def inspec(commandline, prefix = nil)
|
||||
@stdout = @stderr = nil
|
||||
self.out = super
|
||||
end
|
||||
|
||||
def stdout
|
||||
@stdout ||= out.stdout
|
||||
.force_encoding(Encoding::UTF_8)
|
||||
end
|
||||
|
||||
def stderr
|
||||
@stderr ||= out.stderr
|
||||
.force_encoding(Encoding::UTF_8)
|
||||
end
|
||||
|
||||
describe "with a attestation file that does not exist" do
|
||||
let(:attestation_file) { "no_file.yaml" }
|
||||
it "raise file does not exist standard error" do
|
||||
result = run_result
|
||||
assert_includes result.stderr, "no_file.yaml does not exist"
|
||||
assert_equal 1, result.exit_status
|
||||
end
|
||||
end
|
||||
|
||||
describe "with a attestation file that has wrong headers - yaml format" do
|
||||
let(:attestation_file) { "wrong-headers.yaml" }
|
||||
it "raise file does not exist standard error" do
|
||||
result = run_result
|
||||
assert_includes result.stdout, "Extra column headers: [\"random\"]"
|
||||
end
|
||||
end
|
||||
|
||||
describe "with a attestation file that has wrong headers - csv format" do
|
||||
let(:attestation_file) { "wrong-headers.csv" }
|
||||
it "raise file does not exist standard error" do
|
||||
result = run_result
|
||||
assert_includes result.stdout, "Missing column headers: [\"control_id\", \"status\", \"justification\"]"
|
||||
assert_includes result.stdout, "Extra column headers: [\"control_id_random\", \"justification_random\", \"run_random\", \"expiration_date_random\", nil]\n"
|
||||
end
|
||||
end
|
||||
|
||||
describe "with a attestation file that has wrong headers - json format" do
|
||||
let(:attestation_file) { "wrong-headers.json" }
|
||||
it "raise file does not exist standard error" do
|
||||
result = run_result
|
||||
assert_includes result.stdout, "Extra column headers: [\"random\"]"
|
||||
end
|
||||
end
|
||||
|
||||
describe "with a attestation file that has wrong headers - xlsx format" do
|
||||
let(:attestation_file) { "wrong-headers.xlsx" }
|
||||
it "raise file does not exist standard error" do
|
||||
result = run_result
|
||||
assert_includes result.stdout, "Missing column headers: [\"control_id\", \"status\", \"justification\"]"
|
||||
assert_includes result.stdout, "Extra column headers: [\"control_id_random\", \"justification_random\", \"run_random\", \"expiration_date_random\"]\n"
|
||||
end
|
||||
end
|
||||
|
||||
describe "running attestation on a profile - yaml" do
|
||||
let(:attestation_file) { "attestations.yaml" }
|
||||
|
||||
it "attests N/R controls correctly" do
|
||||
result = run_result
|
||||
assert_includes result.stdout, "tmp-3.0.1: No-op (1 failed)"
|
||||
refute_includes result.stdout, "N/R tmp-3.0.2: No-op"
|
||||
refute_includes result.stdout, "N/R tmp-6.0.2: No-op"
|
||||
end
|
||||
|
||||
it "does not attests non N/R controls" do
|
||||
result = run_result
|
||||
assert_includes result.stdout, "tmp-4.0: d.1 (1 failed)"
|
||||
end
|
||||
end
|
||||
|
||||
describe "running attestation on a profile - json" do
|
||||
let(:attestation_file) { "attestations.json" }
|
||||
|
||||
it "attests N/R controls correctly" do
|
||||
result = run_result
|
||||
assert_includes result.stdout, "tmp-3.0.1: No-op (1 failed)"
|
||||
refute_includes result.stdout, "N/R tmp-3.0.2: No-op"
|
||||
refute_includes result.stdout, "N/R tmp-6.0.2: No-op"
|
||||
end
|
||||
|
||||
it "does not attests non N/R controls" do
|
||||
result = run_result
|
||||
assert_includes result.stdout, "tmp-4.0: d.1 (1 failed)"
|
||||
end
|
||||
end
|
||||
|
||||
describe "running attestation on a profile - csv" do
|
||||
let(:attestation_file) { "attestations.csv" }
|
||||
|
||||
it "attests N/R controls correctly" do
|
||||
result = run_result
|
||||
assert_includes result.stdout, "tmp-3.0.1: No-op (1 failed)"
|
||||
refute_includes result.stdout, "N/R tmp-3.0.2: No-op"
|
||||
refute_includes result.stdout, "N/R tmp-6.0.2: No-op"
|
||||
end
|
||||
|
||||
it "does not attests non N/R controls" do
|
||||
result = run_result
|
||||
assert_includes result.stdout, "tmp-4.0: d.1 (1 failed)"
|
||||
end
|
||||
end
|
||||
|
||||
describe "running attestation on a profile - xlsx" do
|
||||
let(:attestation_file) { "attestations.xlsx" }
|
||||
|
||||
it "attests N/R controls correctly" do
|
||||
result = run_result
|
||||
assert_includes result.stdout, "tmp-3.0.1: No-op (1 failed)"
|
||||
refute_includes result.stdout, "N/R tmp-3.0.2: No-op"
|
||||
refute_includes result.stdout, "N/R tmp-6.0.2: No-op"
|
||||
end
|
||||
|
||||
it "does not attests non N/R controls" do
|
||||
result = run_result
|
||||
assert_includes result.stdout, "tmp-4.0: d.1 (1 failed)"
|
||||
end
|
||||
end
|
||||
|
||||
describe "running attestation on a profile - xls" do
|
||||
let(:attestation_file) { "attestations.xls" }
|
||||
|
||||
it "attests N/R controls correctly" do
|
||||
result = run_result
|
||||
assert_includes result.stdout, "tmp-3.0.1: No-op (1 failed)"
|
||||
refute_includes result.stdout, "N/R tmp-3.0.2: No-op"
|
||||
refute_includes result.stdout, "N/R tmp-6.0.2: No-op"
|
||||
end
|
||||
|
||||
it "does not attests non N/R controls" do
|
||||
result = run_result
|
||||
assert_includes result.stdout, "tmp-4.0: d.1 (1 failed)"
|
||||
end
|
||||
end
|
||||
|
||||
describe "running attestation on profile with streaming reporter" do
|
||||
let(:attestation_file) { "#{attestation_profile}/files/attestations.yaml" }
|
||||
it "attests controls correctly" do
|
||||
inspec("exec " + "#{attestation_profile}" + " --attestation-file #{attestation_file}" + " --no-create-lockfile" + " --no-color" + " --reporter progress-bar")
|
||||
if windows?
|
||||
_(stderr).must_match(/\[FAIL\]\s*tmp-3.0.1\s*No-op Skipped control due to only_if condition. Control not attested : Attestation expired on 2021-12-01/)
|
||||
_(stderr).must_match(/\[FAIL\]\s*tmp-3.0.2\s*No-op Skipped control due to only_if condition. Control not attested : Attestation expired on 2001-06-01/)
|
||||
_(stderr).must_match(/\[PASS\]\s*tmp-6.0.2\s*No-op Skipped control due to only_if condition. Control Attested : Sheer cleverness | Evidence URL: Dummy url/)
|
||||
_(stderr).must_match(/\[FAIL\]\s*tmp-4.0\s*d.1 is expected to cmp == \"d.1\"/)
|
||||
else
|
||||
_(stderr).must_match(/\[FAILED\]\s*tmp-3.0.1\s*No-op Skipped control due to only_if condition. Control not attested : Attestation expired on 2021-12-01/)
|
||||
_(stderr).must_match(/\[FAILED\]\s*tmp-3.0.2\s*No-op Skipped control due to only_if condition. Control not attested : Attestation expired on 2001-06-01/)
|
||||
_(stderr).must_match(/\[PASSED\]\s*tmp-6.0.2\s*No-op Skipped control due to only_if condition. Control Attested : Sheer cleverness | Evidence URL: Dummy url/)
|
||||
_(stderr).must_match(/\[FAILED\]\s*tmp-4.0\s*d.1 is expected to cmp == \"d.1\"/)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "an attestation file with invalid dates" do
|
||||
let(:attestation_file) { "bad-date.yaml" }
|
||||
it "gracefully errors" do
|
||||
result = run_result
|
||||
assert_includes result.stdout, "ERROR"
|
||||
end
|
||||
end
|
||||
|
||||
describe "an attestation file with invalid update dates" do
|
||||
let(:attestation_file) { "bad-update-date.yaml" }
|
||||
it "gracefully errors" do
|
||||
result = run_result
|
||||
assert_includes result.stdout, "ERROR"
|
||||
end
|
||||
end
|
||||
|
||||
describe "an attestation file with invalid status" do
|
||||
let(:attestation_file) { "invalid-status.yaml" }
|
||||
it "throws warning" do
|
||||
result = run_result
|
||||
assert_includes result.stdout, "Invalid attestation status 'pass' for control tmp-3.0.3. Use 'passed' or 'failed'."
|
||||
end
|
||||
end
|
||||
|
||||
describe "an attestation file with no status" do
|
||||
let(:attestation_file) { "no-status.yaml" }
|
||||
it "throws warning" do
|
||||
result = run_result
|
||||
assert_includes result.stdout, "No attestation status for control tmp-3.0.3. Use 'passed' or 'failed'."
|
||||
end
|
||||
end
|
||||
|
||||
describe "an attestation file with invalid frequency value" do
|
||||
let(:attestation_file) { "invalid-frequency.yaml" }
|
||||
it "throws warning" do
|
||||
result = run_result
|
||||
assert_includes result.stdout, "Invalid frequency value 'biweekly' for control tmp-3.0.4."
|
||||
end
|
||||
end
|
||||
|
||||
describe "an attestation file with no justification" do
|
||||
let(:attestation_file) { "no-justification.yaml" }
|
||||
it "throws warning and shows proper message for justification absence" do
|
||||
result = run_result
|
||||
assert_includes result.stdout, "Missing column headers: [\"justification\"]"
|
||||
assert_includes result.stdout, "Control Attested : No justification provided."
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue