2
0
Fork 0
mirror of https://github.com/inspec/inspec synced 2025-02-23 01:18:44 +00:00

Added enhanced outcomes option to schema subcommand and updated schema

Signed-off-by: Nikita Mathur <nikita.mathur@chef.io>
This commit is contained in:
Nikita Mathur 2022-06-29 15:24:27 +05:30
parent dfd66012b4
commit a9fae7cfe2
6 changed files with 231 additions and 5 deletions

View file

@ -463,11 +463,13 @@ class Inspec::InspecCLI < Inspec::BaseCLI
pretty_handle_exception(e)
end
option :enhanced_outcomes, type: :boolean,
desc: "Show enhanced outcomes output"
desc "schema NAME", "print the JSON schema", hide: true
def schema(name)
require "inspec/schema/output_schema"
puts Inspec::Schema::OutputSchema.json(name)
o = config
puts Inspec::Schema::OutputSchema.json(name, o)
rescue StandardError => e
puts e
puts "Valid schemas are #{Inspec::Schema::OutputSchema.names.join(", ")}"

View file

@ -111,6 +111,43 @@ module Inspec
},
}.freeze
CONTROL_ENHANCED_OUTCOME = {
"type" => "object",
"additionalProperties" => false,
"properties" => {
"id" => { "type" => "string" },
"title" => { "type" => %w{string null} },
"desc" => { "type" => %w{string null} },
"descriptions" => { "type" => %w{array} },
"impact" => { "type" => "number" },
"status" => {
"enum" => %w{passed failed not_applicable not_reviewed error},
"description" => Primitives.desc(Primitives::STRING, "The enhanced outcome status of the control"),
},
"refs" => REFS,
"tags" => TAGS,
"code" => { "type" => "string" },
"source_location" => {
"type" => "object",
"properties" => {
"ref" => { "type" => "string" },
"line" => { "type" => "number" },
},
},
"results" => { "type" => "array", "items" => RESULT },
"waiver_data" => {
"type" => "object",
"properties" => {
"skipped_due_to_waiver" => { "type" => "string" },
"run" => { "type" => "boolean" },
"message" => { "type" => "string" },
"expiration_date" => { "type" => "string" },
"justification" => { "type" => "string" },
},
},
},
}.freeze
SUPPORTS = {
"type" => "object",
"additionalProperties" => false,
@ -173,6 +210,45 @@ module Inspec
},
}.freeze
PROFILE_ENHANCED_OUTCOME = {
"type" => "object",
"additionalProperties" => false,
"properties" => {
"name" => { "type" => "string" },
"version" => { "type" => "string", "optional" => true },
"sha256" => { "type" => "string", "optional" => false },
"title" => { "type" => "string", "optional" => true },
"maintainer" => { "type" => "string", "optional" => true },
"copyright" => { "type" => "string", "optional" => true },
"copyright_email" => { "type" => "string", "optional" => true },
"license" => { "type" => "string", "optional" => true },
"summary" => { "type" => "string", "optional" => true },
"status" => { "type" => "string", "optional" => false },
"status_message" => { "type" => "string", "optional" => true },
# skip_message is deprecated, status_message should be used to store the reason for skipping
"skip_message" => { "type" => "string", "optional" => true },
"supports" => {
"type" => "array",
"items" => SUPPORTS,
"optional" => true,
},
"controls" => {
"type" => "array",
"items" => CONTROL_ENHANCED_OUTCOME,
},
"groups" => {
"type" => "array",
"items" => CONTROL_GROUP,
},
"attributes" => { # TODO: rename to inputs, refs #3802
"type" => "array",
# TODO: more detailed specification needed
},
},
}.freeze
EXEC_JSON = {
"type" => "object",
"additionalProperties" => false,
@ -187,6 +263,20 @@ module Inspec
},
}.freeze
EXEC_JSON_ENHANCED_OUTCOME = {
"type" => "object",
"additionalProperties" => false,
"properties" => {
"platform" => PLATFORM,
"profiles" => {
"type" => "array",
"items" => PROFILE_ENHANCED_OUTCOME,
},
"statistics" => STATISTICS,
"version" => { "type" => "string" },
},
}.freeze
MIN_CONTROL = {
"type" => "object",
"additionalProperties" => false,
@ -228,6 +318,7 @@ module Inspec
LIST = {
"exec-json" => EXEC_JSON,
"exec-jsonmin" => EXEC_JSONMIN,
"exec-json-enhanced-outcome" => EXEC_JSON_ENHANCED_OUTCOME,
"platforms" => PLATFORMS,
}.freeze

View file

@ -19,8 +19,8 @@ module Inspec
# Lists the potential values for a control result
CONTROL_RESULT_STATUS = Primitives::SchemaType.new("Control Result Status", {
"type" => "string",
"enum" => %w{passed failed skipped error},
}, [], "The status of a control. Should be one of 'passed', 'failed', 'skipped', or 'error'.")
"enum" => %w{passed failed skipped},
}, [], "The status of a control. Should be one of 'passed', 'failed', or 'skipped'.")
# Represents the statistics/result of a control"s execution
CONTROL_RESULT = Primitives::SchemaType.new("Control Result", {
@ -75,6 +75,36 @@ module Inspec
},
}, [CONTROL_DESCRIPTION, Primitives::REFERENCE, Primitives::SOURCE_LOCATION, CONTROL_RESULT], "Describes a control and any findings it has.")
# Represents a control produced with enhanced outcomes option
ENHANCED_OUTCOME_CONTROL = Primitives::SchemaType.new("Exec JSON Control", {
"type" => "object",
"additionalProperties" => true,
"required" => %w{id title desc impact refs tags code source_location results},
"properties" => {
"id" => Primitives.desc(Primitives::STRING, "The id."),
"title" => Primitives.desc({ "type" => %w{string null} }, "The title - is nullable."), # Nullable string
"desc" => Primitives.desc({ "type" => %w{string null} }, "The description for the overarching control."),
"descriptions" => Primitives.desc(Primitives.array(CONTROL_DESCRIPTION.ref), "A set of additional descriptions. Example: the 'fix' text."),
"impact" => Primitives.desc(Primitives::IMPACT, "The impactfulness or severity."),
"status" => {
"enum" => %w{passed failed not_applicable not_reviewed error},
"description" => Primitives.desc(Primitives::STRING, "The enhanced outcome status of the control"),
},
"refs" => Primitives.desc(Primitives.array(Primitives::REFERENCE.ref), "The set of references to external documents."),
"tags" => Primitives.desc(Primitives::TAGS, "A set of tags - usually metadata."),
"code" => Primitives.desc(Primitives::STRING, "The raw source code of the control. Note that if this is an overlay control, it does not include the underlying source code."),
"source_location" => Primitives.desc(Primitives::SOURCE_LOCATION.ref, "The explicit location of the control within the source code."),
"results" => Primitives.desc(Primitives.array(CONTROL_RESULT.ref), %q(
The set of all tests within the control and their results and findings. Example:
For Chef Inspec, if in the control's code we had the following:
describe sshd_config do
its('Port') { should cmp 22 }
end
The findings from this block would be appended to the results, as well as those of any other blocks within the control.
)),
},
}, [CONTROL_DESCRIPTION, Primitives::REFERENCE, Primitives::SOURCE_LOCATION, CONTROL_RESULT], "Describes a control and any findings it has.")
# Based loosely on https://docs.chef.io/inspec/profiles/ as of July 3, 2019
# However, concessions were made to the reality of current reporters, specifically
# with how description is omitted and version/inspec_version aren't as advertised online
@ -112,6 +142,40 @@ module Inspec
},
}, [CONTROL, Primitives::CONTROL_GROUP, Primitives::DEPENDENCY, Primitives::SUPPORT], "Information on the set of controls assessed. Example: it can include the name of the Inspec profile and any findings.")
ENHANCED_OUTCOME_PROFILE = Primitives::SchemaType.new("Exec JSON Profile", {
"type" => "object",
"additionalProperties" => true,
"required" => %w{name sha256 supports attributes groups controls},
# Name is mandatory in inspec.yml.
# supports, controls, groups, and attributes are always present, even if empty
# sha256, status, status_message
"properties" => {
# These are provided in inspec.yml
"name" => Primitives.desc(Primitives::STRING, "The name - must be unique."),
"title" => Primitives.desc(Primitives::STRING, "The title - should be human readable."),
"maintainer" => Primitives.desc(Primitives::STRING, "The maintainer(s)."),
"copyright" => Primitives.desc(Primitives::STRING, "The copyright holder(s)."),
"copyright_email" => Primitives.desc(Primitives::STRING, "The email address or other contact information of the copyright holder(s)."),
"depends" => Primitives.desc(Primitives.array(Primitives::DEPENDENCY.ref), "The set of dependencies this profile depends on. Example: an overlay profile is dependent on another profile."),
"parent_profile" => Primitives.desc(Primitives::STRING, "The name of the parent profile if the profile is a dependency of another."),
"license" => Primitives.desc(Primitives::STRING, "The copyright license. Example: the full text or the name, such as 'Apache License, Version 2.0'."),
"summary" => Primitives.desc(Primitives::STRING, "The summary. Example: the Security Technical Implementation Guide (STIG) header."),
"version" => Primitives.desc(Primitives::STRING, "The version of the profile."),
"supports" => Primitives.desc(Primitives.array(Primitives::SUPPORT.ref), "The set of supported platform targets."),
"description" => Primitives.desc(Primitives::STRING, "The description - should be more detailed than the summary."),
"inspec_version" => Primitives.desc(Primitives::STRING, "The version of Inspec."),
# These are generated at runtime, and all except status_message and skip_message are guaranteed
"sha256" => Primitives.desc(Primitives::STRING, "The checksum of the profile."),
"status" => Primitives.desc(Primitives::STRING, "The status. Example: loaded."), # enum? loaded, failed, skipped
"status_message" => Primitives.desc(Primitives::STRING, "The reason for the status. Example: why it was skipped or failed to load."),
"skip_message" => Primitives.desc(Primitives::STRING, "The reason for skipping if it was skipped."), # Deprecated field - status_message should be used instead.
"controls" => Primitives.desc(Primitives.array(CONTROL.ref), "The set of controls including any findings."),
"groups" => Primitives.desc(Primitives.array(Primitives::CONTROL_GROUP.ref), "A set of descriptions for the control groups. Example: the ids of the controls."),
"attributes" => Primitives.desc(Primitives.array(Primitives::INPUT), "The input(s) or attribute(s) used in the run."),
},
}, [ENHANCED_OUTCOME_CONTROL, Primitives::CONTROL_GROUP, Primitives::DEPENDENCY, Primitives::SUPPORT], "Information on the set of controls assessed. Example: it can include the name of the Inspec profile and any findings.")
# Result of exec json. Top level value
# TODO: Include the format of top level controls. This was omitted for lack of sufficient examples
OUTPUT = Primitives::SchemaType.new("Exec JSON Output", {
@ -125,6 +189,18 @@ module Inspec
"version" => Primitives.desc(Primitives::STRING, "Version number of the tool that generated the findings. Example: '4.18.108' is a version of Chef InSpec."),
},
}, [Primitives::PLATFORM, PROFILE, Primitives::STATISTICS], "The top level value containing all of the results.")
ENHANCED_OUTCOME_OUTPUT = Primitives::SchemaType.new("Exec JSON Output", {
"type" => "object",
"additionalProperties" => true,
"required" => %w{platform profiles statistics version},
"properties" => {
"platform" => Primitives.desc(Primitives::PLATFORM.ref, "Information on the platform the run from the tool that generated the findings was from. Example: the name of the operating system."),
"profiles" => Primitives.desc(Primitives.array(PROFILE.ref), "Information on the run(s) from the tool that generated the findings. Example: the findings."),
"statistics" => Primitives.desc(Primitives::STATISTICS.ref, "Statistics for the run(s) from the tool that generated the findings. Example: the runtime duration."),
"version" => Primitives.desc(Primitives::STRING, "Version number of the tool that generated the findings. Example: '4.18.108' is a version of Chef InSpec."),
},
}, [Primitives::PLATFORM, ENHANCED_OUTCOME_PROFILE, Primitives::STATISTICS], "The top level value containing all of the results.")
end
end
end

View file

@ -30,6 +30,8 @@ module Inspec
"profile-json" => OutputSchema.finalize(Schema::ProfileJson::PROFILE),
"exec-json" => OutputSchema.finalize(Schema::ExecJson::OUTPUT),
"exec-jsonmin" => OutputSchema.finalize(Schema::ExecJsonMin::OUTPUT),
"profile-json-enhanced-outcomes" => OutputSchema.finalize(Schema::ProfileJson::ENHANCED_OUTCOME_PROFILE),
"exec-json-enhanced-outcomes" => OutputSchema.finalize(Schema::ExecJson::ENHANCED_OUTCOME_OUTPUT),
"platforms" => PLATFORMS,
}.freeze
@ -37,7 +39,8 @@ module Inspec
LIST.keys
end
def self.json(name)
def self.json(name, opts)
name += "-enhanced-outcomes" if opts["enhanced_outcomes"]
if !LIST.key?(name)
raise("Cannot find schema #{name.inspect}.")
elsif LIST[name].is_a?(Proc)

View file

@ -31,6 +31,28 @@ module Inspec
},
}, [CONTROL_DESCRIPTIONS, Primitives::REFERENCE, Primitives::SOURCE_LOCATION], "The set of all tests within the control.")
# Represents a control with enhanced outcomes status information
ENHANCED_OUTCOME_CONTROL = Primitives::SchemaType.new("Profile JSON Control", {
"type" => "object",
"additionalProperties" => true,
"required" => %w{id title desc impact tags code},
"properties" => {
"id" => Primitives.desc(Primitives::STRING, "The id."),
"title" => Primitives.desc({ "type" => %w{string null} }, "The title - is nullable."),
"desc" => Primitives.desc({ "type" => %w{string null} }, "The description for the overarching control."),
"descriptions" => Primitives.desc(CONTROL_DESCRIPTIONS.ref, "A set of additional descriptions. Example: the 'fix' text."),
"impact" => Primitives.desc(Primitives::IMPACT, "The impactfulness or severity."),
"status" => {
"enum" => %w{passed failed not_applicable not_reviewed error},
"description" => Primitives.desc(Primitives::STRING, "The enhanced outcome status of the control"),
},
"refs" => Primitives.desc(Primitives.array(Primitives::REFERENCE.ref), "The set of references to external documents."),
"tags" => Primitives.desc(Primitives::TAGS, "A set of tags - usually metadata."),
"code" => Primitives.desc(Primitives::STRING, "The raw source code of the control. Note that if this is an overlay control, it does not include the underlying source code."),
"source_location" => Primitives.desc(Primitives::SOURCE_LOCATION.ref, "The explicit location of the control within the source code."),
},
}, [CONTROL_DESCRIPTIONS, Primitives::REFERENCE, Primitives::SOURCE_LOCATION], "The set of all tests within the control.")
# A profile that has not been run.
PROFILE = Primitives::SchemaType.new("Profile JSON Profile", {
"type" => "object",
@ -55,6 +77,30 @@ module Inspec
"depends" => Primitives.desc(Primitives.array(Primitives::DEPENDENCY.ref), "The set of dependencies this profile depends on. Example: an overlay profile is dependent on another profile."), # Can have depends, but NOT a parentprofile
},
}, [Primitives::SUPPORT, CONTROL, Primitives::CONTROL_GROUP, Primitives::DEPENDENCY, Primitives::GENERATOR], "Information on the set of controls that can be assessed. Example: it can include the name of the Inspec profile.")
ENHANCED_OUTCOME_PROFILE = Primitives::SchemaType.new("Profile JSON Profile", {
"type" => "object",
"additionalProperties" => true, # Anything in the yaml will be put in here. LTTODO: Make this stricter!
"required" => %w{name supports controls groups sha256},
"properties" => {
"name" => Primitives.desc(Primitives::STRING, "The name - must be unique."),
"supports" => Primitives.desc(Primitives.array(Primitives::SUPPORT.ref), "The set of supported platform targets."),
"controls" => Primitives.desc(Primitives.array(CONTROL.ref), "The set of controls - contains no findings as the assessment has not yet occurred."),
"groups" => Primitives.desc(Primitives.array(Primitives::CONTROL_GROUP.ref), "A set of descriptions for the control groups. Example: the ids of the controls."),
"inputs" => Primitives.desc(Primitives.array(Primitives::INPUT), "The input(s) or attribute(s) used to be in the run."),
"sha256" => Primitives.desc(Primitives::STRING, "The checksum of the profile."),
"status" => Primitives.desc(Primitives::STRING, "The status. Example: skipped."),
"generator" => Primitives.desc(Primitives::GENERATOR.ref, "The tool that generated this file. Example: Chef Inspec."),
"version" => Primitives.desc(Primitives::STRING, "The version of the profile."),
# Other properties possible in inspec docs, but that aren"t guaranteed
"title" => Primitives.desc(Primitives::STRING, "The title - should be human readable."),
"maintainer" => Primitives.desc(Primitives::STRING, "The maintainer(s)."),
"copyright" => Primitives.desc(Primitives::STRING, "The copyright holder(s)."),
"copyright_email" => Primitives.desc(Primitives::STRING, "The email address or other contract information of the copyright holder(s)."),
"depends" => Primitives.desc(Primitives.array(Primitives::DEPENDENCY.ref), "The set of dependencies this profile depends on. Example: an overlay profile is dependent on another profile."), # Can have depends, but NOT a parentprofile
},
}, [Primitives::SUPPORT, ENHANCED_OUTCOME_CONTROL, Primitives::CONTROL_GROUP, Primitives::DEPENDENCY, Primitives::GENERATOR], "Information on the set of controls that can be assessed. Example: it can include the name of the Inspec profile.")
end
end
end

View file

@ -27,4 +27,12 @@ describe "inspec schema" do
_(json_output["definitions"]["Control_Result"]["properties"]["resource_id"]).wont_be_nil
end
end
describe "validate schema of exec-json with enhanced_outcomes option" do
it "contains resource_id key" do
out = inspec("schema exec-json --enhanced-outcomes")
json_output = JSON.parse(out.stdout)
_(json_output["definitions"]["Exec_JSON_Control"]["properties"]["status"]).wont_be_nil
end
end
end