mirror of
https://github.com/inspec/inspec
synced 2024-11-27 07:00:39 +00:00
Merge pull request #4865 from inspec/jh/schema-improvements
Jh/schema improvements
This commit is contained in:
commit
7bb0bb2188
19 changed files with 813 additions and 120 deletions
|
@ -26,7 +26,7 @@ Gem::Specification.new do |spec|
|
||||||
spec.add_dependency "chef-telemetry", "~> 1.0"
|
spec.add_dependency "chef-telemetry", "~> 1.0"
|
||||||
spec.add_dependency "license-acceptance", ">= 0.2.13", "< 2.0"
|
spec.add_dependency "license-acceptance", ">= 0.2.13", "< 2.0"
|
||||||
spec.add_dependency "thor", ">= 0.20", "< 2.0"
|
spec.add_dependency "thor", ">= 0.20", "< 2.0"
|
||||||
spec.add_dependency "json-schema", "~> 2.8"
|
spec.add_dependency "json_schemer", "~> 0.2.1"
|
||||||
spec.add_dependency "method_source", "~> 0.8"
|
spec.add_dependency "method_source", "~> 0.8"
|
||||||
spec.add_dependency "rubyzip", "~> 1.2", ">= 1.2.2"
|
spec.add_dependency "rubyzip", "~> 1.2", ">= 1.2.2"
|
||||||
spec.add_dependency "rspec", "~> 3.9"
|
spec.add_dependency "rspec", "~> 3.9"
|
||||||
|
|
|
@ -377,12 +377,12 @@ class Inspec::InspecCLI < Inspec::BaseCLI
|
||||||
|
|
||||||
desc "schema NAME", "print the JSON schema", hide: true
|
desc "schema NAME", "print the JSON schema", hide: true
|
||||||
def schema(name)
|
def schema(name)
|
||||||
require "inspec/schema"
|
require "inspec/schema/output_schema"
|
||||||
|
|
||||||
puts Inspec::Schema.json(name)
|
puts Inspec::Schema::OutputSchema.json(name)
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
puts e
|
puts e
|
||||||
puts "Valid schemas are #{Inspec::Schema.names.join(", ")}"
|
puts "Valid schemas are #{Inspec::Schema::OutputSchema.names.join(", ")}"
|
||||||
end
|
end
|
||||||
|
|
||||||
desc "version", "prints the version of this tool"
|
desc "version", "prints the version of this tool"
|
||||||
|
|
17
lib/inspec/schema/README.md
Normal file
17
lib/inspec/schema/README.md
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
# Schema Generation
|
||||||
|
|
||||||
|
These files handle the generation of JSON schema's for inspec output as yielded by `inspec schema <schema-name>`.
|
||||||
|
This schema is in the JSON Schema 4th revision spec.
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
Data/schema structures shared between all outputs are located in `primitives.rb`
|
||||||
|
The files `exec_json_min.rb`, `exec_json.rb`, and `profile_json.rb` contain JSON structures for their specific output types, respectively.
|
||||||
|
The output schema is "flat" in terms of schema structure, with each "type" defined in the JSONs `definitions` block and referred to via $ref.
|
||||||
|
The logic for this is handled via the `SchemaType` class in `primitives.rb`.
|
||||||
|
All relevant dependencies are included recursively via the `dependencies` property of that class, constructed using the `build_definitions` method in `output_schema.rb`.
|
||||||
|
|
||||||
|
## Reasoning and validation
|
||||||
|
In general the first iteration of this updated schema was made by examining the output of all of the profiles in the tests directory.
|
||||||
|
All of the profiles generated by those tests successfully validate against this schema.
|
||||||
|
However, in light of potential missed properties that aren't covered by existing tests, the schema was left very non-strict.
|
||||||
|
More specifically, "additionalProperties" was left as True on all schema objects.
|
128
lib/inspec/schema/exec_json.rb
Normal file
128
lib/inspec/schema/exec_json.rb
Normal file
|
@ -0,0 +1,128 @@
|
||||||
|
require "inspec/schema/primitives"
|
||||||
|
|
||||||
|
# These type occur only when running "inspec exec --reporter json <file>".
|
||||||
|
|
||||||
|
module Inspec
|
||||||
|
module Schema
|
||||||
|
module ExecJson
|
||||||
|
# Represents a label and description, to provide human-readable info about a control
|
||||||
|
CONTROL_DESCRIPTION = Primitives::SchemaType.new("Control Description", {
|
||||||
|
"type" => "object",
|
||||||
|
"additionalProperties" => true,
|
||||||
|
"required" => %w{label data},
|
||||||
|
"properties" => {
|
||||||
|
"label" => Primitives::STRING,
|
||||||
|
"data" => Primitives::STRING,
|
||||||
|
},
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
# 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},
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
# Represents the statistics/result of a control"s execution
|
||||||
|
CONTROL_RESULT = Primitives::SchemaType.new("Control Result", {
|
||||||
|
"type" => "object",
|
||||||
|
"additionalProperties" => true,
|
||||||
|
"required" => %w{code_desc run_time start_time},
|
||||||
|
"properties" => {
|
||||||
|
"status" => CONTROL_RESULT_STATUS.ref,
|
||||||
|
"code_desc" => Primitives::STRING,
|
||||||
|
"run_time" => Primitives::NUMBER,
|
||||||
|
"start_time" => Primitives::STRING,
|
||||||
|
|
||||||
|
# All optional
|
||||||
|
"resource" => Primitives::STRING,
|
||||||
|
"message" => Primitives::STRING,
|
||||||
|
"skip_message" => Primitives::STRING,
|
||||||
|
"exception" => Primitives::STRING,
|
||||||
|
"backtrace" => {
|
||||||
|
"anyOf" => [
|
||||||
|
Primitives.array(Primitives::STRING),
|
||||||
|
Primitives::NULL,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, [CONTROL_RESULT_STATUS])
|
||||||
|
|
||||||
|
# Represents a control produced
|
||||||
|
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 of this control"),
|
||||||
|
"title" => { "type" => %w{string null} }, # Nullable string
|
||||||
|
"desc" => { "type" => %w{string null} },
|
||||||
|
"descriptions" => Primitives.array(CONTROL_DESCRIPTION.ref),
|
||||||
|
"impact" => Primitives::IMPACT,
|
||||||
|
"refs" => Primitives.array(Primitives::REFERENCE.ref),
|
||||||
|
"tags" => Primitives::TAGS,
|
||||||
|
"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::SOURCE_LOCATION.ref,
|
||||||
|
"results" => Primitives.desc(Primitives.array(CONTROL_RESULT.ref), %q{
|
||||||
|
A list of all results of the controls describe blocks.
|
||||||
|
|
||||||
|
For instance, if in the controls code we had the following:
|
||||||
|
describe sshd_config do
|
||||||
|
its('Port') { should cmp 22 }
|
||||||
|
end
|
||||||
|
The result of this block as a ControlResult would be appended to the results list.
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}, [CONTROL_DESCRIPTION, Primitives::REFERENCE, Primitives::SOURCE_LOCATION, CONTROL_RESULT])
|
||||||
|
|
||||||
|
# Based loosely on https://www.inspec.io/docs/reference/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
|
||||||
|
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, skip_message
|
||||||
|
"properties" => {
|
||||||
|
# These are provided in inspec.yml
|
||||||
|
"name" => Primitives::STRING,
|
||||||
|
"title" => Primitives::STRING,
|
||||||
|
"maintainer" => Primitives::STRING,
|
||||||
|
"copyright" => Primitives::STRING,
|
||||||
|
"copyright_email" => Primitives::STRING,
|
||||||
|
"depends" => Primitives.array(Primitives::DEPENDENCY.ref),
|
||||||
|
"parent_profile" => Primitives::STRING,
|
||||||
|
"license" => Primitives::STRING,
|
||||||
|
"summary" => Primitives::STRING,
|
||||||
|
"version" => Primitives::STRING,
|
||||||
|
"supports" => Primitives.array(Primitives::SUPPORT.ref),
|
||||||
|
"description" => Primitives::STRING,
|
||||||
|
"inspec_version" => Primitives::STRING,
|
||||||
|
|
||||||
|
# These are generated at runtime, and all except skip_message are guaranteed
|
||||||
|
"sha256" => Primitives::STRING,
|
||||||
|
"status" => Primitives::STRING,
|
||||||
|
"skip_message" => Primitives::STRING, # If skipped, why
|
||||||
|
"controls" => Primitives.array(CONTROL.ref),
|
||||||
|
"groups" => Primitives.array(Primitives::CONTROL_GROUP.ref),
|
||||||
|
"attributes" => Primitives.array(Primitives::INPUT),
|
||||||
|
},
|
||||||
|
}, [CONTROL, Primitives::CONTROL_GROUP, Primitives::DEPENDENCY, Primitives::SUPPORT])
|
||||||
|
|
||||||
|
# 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", {
|
||||||
|
"type" => "object",
|
||||||
|
"additionalProperties" => true,
|
||||||
|
"required" => %w{platform profiles statistics version},
|
||||||
|
"properties" => {
|
||||||
|
"platform" => Primitives::PLATFORM.ref,
|
||||||
|
"profiles" => Primitives.array(PROFILE.ref),
|
||||||
|
"statistics" => Primitives::STATISTICS.ref,
|
||||||
|
"version" => Primitives::STRING,
|
||||||
|
},
|
||||||
|
}, [Primitives::PLATFORM, PROFILE, Primitives::STATISTICS])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
40
lib/inspec/schema/exec_json_min.rb
Normal file
40
lib/inspec/schema/exec_json_min.rb
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
require "inspec/schema/primitives"
|
||||||
|
|
||||||
|
# These type occur only when running "exec --reporter json-min <file>".
|
||||||
|
|
||||||
|
module Inspec
|
||||||
|
module Schema
|
||||||
|
module ExecJsonMin
|
||||||
|
# Represents a subset of the information about a control, designed for conciseness
|
||||||
|
CONTROL = Primitives::SchemaType.new("Exec JSON-MIN Control", {
|
||||||
|
"type" => "object",
|
||||||
|
"additionalProperties" => true,
|
||||||
|
"required" => %w{id profile_id profile_sha256 status code_desc},
|
||||||
|
"properties" => {
|
||||||
|
"id" => Primitives::STRING,
|
||||||
|
"profile_id" => { "type" => %w{string null} },
|
||||||
|
"profile_sha256" => Primitives::STRING,
|
||||||
|
"status" => Primitives::STRING,
|
||||||
|
"code_desc" => Primitives::STRING,
|
||||||
|
"skip_message" => Primitives::STRING,
|
||||||
|
"resource" => Primitives::STRING,
|
||||||
|
"message" => Primitives::STRING,
|
||||||
|
"exception" => Primitives::STRING,
|
||||||
|
"backtrace" => Primitives.array(Primitives::STRING),
|
||||||
|
},
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
# Result of exec jsonmin. Top level value
|
||||||
|
OUTPUT = Primitives::SchemaType.new("Exec JSON-MIN output", {
|
||||||
|
"type" => "object",
|
||||||
|
"additionalProperties" => true,
|
||||||
|
"required" => %w{statistics controls version},
|
||||||
|
"properties" => {
|
||||||
|
"statistics" => Primitives::STATISTICS.ref,
|
||||||
|
"version" => Primitives::STRING,
|
||||||
|
"controls" => Primitives.array(CONTROL.ref),
|
||||||
|
},
|
||||||
|
}, [Primitives::STATISTICS, CONTROL])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
53
lib/inspec/schema/output_schema.rb
Normal file
53
lib/inspec/schema/output_schema.rb
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
require "json"
|
||||||
|
require "inspec/schema/primitives"
|
||||||
|
require "inspec/schema/exec_json"
|
||||||
|
require "inspec/schema/exec_json_min"
|
||||||
|
require "inspec/schema/profile_json"
|
||||||
|
|
||||||
|
module Inspec
|
||||||
|
module Schema
|
||||||
|
module OutputSchema
|
||||||
|
# Build our definitions
|
||||||
|
def self.build_definitions(schema_type)
|
||||||
|
{
|
||||||
|
"definitions" => schema_type.all_depends.map { |t| [t.ref_name, t.body] }.to_h,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
# Helper function to automatically bundle a type with its dependencies
|
||||||
|
def self.finalize(schema_type)
|
||||||
|
schema_type.body.merge(OutputSchema.build_definitions(schema_type))
|
||||||
|
end
|
||||||
|
|
||||||
|
# using a proc here so we can lazy load it when we need
|
||||||
|
PLATFORMS = lambda do
|
||||||
|
require "train"
|
||||||
|
Train.create("mock").connection
|
||||||
|
Train::Platforms.export
|
||||||
|
end
|
||||||
|
|
||||||
|
LIST = {
|
||||||
|
"profile-json" => OutputSchema.finalize(Schema::ProfileJson::PROFILE),
|
||||||
|
"exec-json" => OutputSchema.finalize(Schema::ExecJson::OUTPUT),
|
||||||
|
"exec-jsonmin" => OutputSchema.finalize(Schema::ExecJsonMin::OUTPUT),
|
||||||
|
"platforms" => PLATFORMS,
|
||||||
|
}.freeze
|
||||||
|
|
||||||
|
def self.names
|
||||||
|
LIST.keys
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.json(name)
|
||||||
|
if !LIST.key?(name)
|
||||||
|
raise("Cannot find schema #{name.inspect}.")
|
||||||
|
elsif LIST[name].is_a?(Proc)
|
||||||
|
v = LIST[name].call
|
||||||
|
else
|
||||||
|
v = LIST[name]
|
||||||
|
end
|
||||||
|
|
||||||
|
JSON.dump(v)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
266
lib/inspec/schema/primitives.rb
Normal file
266
lib/inspec/schema/primitives.rb
Normal file
|
@ -0,0 +1,266 @@
|
||||||
|
require "set"
|
||||||
|
|
||||||
|
# These elements are shared between more than one output type
|
||||||
|
|
||||||
|
module Inspec
|
||||||
|
module Schema
|
||||||
|
module Primitives
|
||||||
|
######################### Establish simple helpers for this schema ########################################
|
||||||
|
# Use this function to easily make described types
|
||||||
|
def self.desc(obj, description)
|
||||||
|
obj.merge({ "description" => description })
|
||||||
|
end
|
||||||
|
|
||||||
|
# Use this function to easily make an array of a type
|
||||||
|
def self.array(of_type)
|
||||||
|
{
|
||||||
|
"type" => "array",
|
||||||
|
"items" => of_type,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
# This function performs some simple validation on schemas, to catch obvious missed requirements
|
||||||
|
def self.validate_schema(schema)
|
||||||
|
return if schema["type"] != "object"
|
||||||
|
raise "Objects in schema must have a \"required\" property, even if it is empty." unless schema.key?("required")
|
||||||
|
|
||||||
|
return if schema["required"].empty?
|
||||||
|
raise "An object with required properties must have a properties hash." unless schema.key?("properties")
|
||||||
|
|
||||||
|
return if Set.new(schema["required"]) <= Set.new(schema["properties"].keys)
|
||||||
|
|
||||||
|
raise "Not all required properties are present."
|
||||||
|
end
|
||||||
|
|
||||||
|
# Use this class to quickly add/use object types to/in a definition block
|
||||||
|
class SchemaType
|
||||||
|
attr_accessor :name, :depends
|
||||||
|
def initialize(name, body, dependencies)
|
||||||
|
# Validate the schema
|
||||||
|
Primitives.validate_schema(body)
|
||||||
|
# The title of the type
|
||||||
|
@name = name
|
||||||
|
# The body of the type
|
||||||
|
@body = body
|
||||||
|
# What SchemaType[]s it depends on. In essence, any thing that you .ref in the body
|
||||||
|
@depends = Set.new(dependencies)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Produce this schema types generated body.
|
||||||
|
# Use to actually define the ref!
|
||||||
|
def body
|
||||||
|
@body.merge({
|
||||||
|
"title" => @name,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
# Formats this to have a JSON pointer compatible title
|
||||||
|
def ref_name
|
||||||
|
@name.gsub(/\s+/, "_")
|
||||||
|
end
|
||||||
|
|
||||||
|
# Yields this type as a json schema ref
|
||||||
|
def ref
|
||||||
|
{ "$ref" => "#/definitions/#{ref_name}" }
|
||||||
|
end
|
||||||
|
|
||||||
|
# Recursively acquire all depends for this schema. Return them sorted by name
|
||||||
|
def all_depends
|
||||||
|
result = @depends
|
||||||
|
# Fetch all from children
|
||||||
|
@depends.each do |nested_type|
|
||||||
|
# Yes, converting back to set here does some duplicate sorting.
|
||||||
|
# But here, performance really isn't our concern.
|
||||||
|
result += Set.new(nested_type.all_depends)
|
||||||
|
end
|
||||||
|
# Return the results as a sorted array
|
||||||
|
Array(result).sort_by(&:name)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
######################### Establish basic type shorthands for this schema ########################################
|
||||||
|
# These three are essentially primitives, used as shorthand
|
||||||
|
OBJECT = { "type" => "object", "additionalProperties" => true }.freeze
|
||||||
|
NUMBER = { "type" => "number" }.freeze
|
||||||
|
STRING = { "type" => "string" }.freeze
|
||||||
|
NULL = { "type" => "null" }.freeze
|
||||||
|
|
||||||
|
# We might eventually enforce string format stuff on this
|
||||||
|
URL = { "type" => "string" }.freeze
|
||||||
|
|
||||||
|
# A controls tags can have any number of properties, and any sorts of values
|
||||||
|
TAGS = { "type" => "object", "additionalProperties" => true }.freeze
|
||||||
|
|
||||||
|
# Attributes/Inputs specify the inputs to a profile.
|
||||||
|
INPUT = { "type" => "object", "additionalProperties" => true }.freeze
|
||||||
|
|
||||||
|
# Within a control file, impacts can be numeric 0-1 or string in [none,low,medium,high,critical]
|
||||||
|
# However, when they are output, the string types are automatically converted as follows:
|
||||||
|
# none -> 0 to 0.01
|
||||||
|
# low -> 0.01 to 0.4
|
||||||
|
# medium -> 0.4 to 0.7
|
||||||
|
# high -> 0.7 to 0.9
|
||||||
|
# Critical -> 0.9 to 1.0 (inclusive)
|
||||||
|
IMPACT = {
|
||||||
|
"type" => "number",
|
||||||
|
"minimum" => 0.0,
|
||||||
|
"maximum" => 1.0,
|
||||||
|
}.freeze
|
||||||
|
|
||||||
|
# A status for a control
|
||||||
|
STATUS = {
|
||||||
|
"type" => "string",
|
||||||
|
"enum" => %w{passed failed skipped error},
|
||||||
|
}.freeze
|
||||||
|
|
||||||
|
######################### Establish inspec types common to several schemas helpers for this schema #######################################
|
||||||
|
|
||||||
|
# We use "title" to name the type.
|
||||||
|
# and "description" (via the describe function) to describe its particular usage
|
||||||
|
# Summary item containing run statistics about a subset of the controls
|
||||||
|
STATISTIC_ITEM = SchemaType.new("Statistic Block", {
|
||||||
|
"type" => "object",
|
||||||
|
"additionalProperties" => true,
|
||||||
|
"required" => ["total"],
|
||||||
|
"properties" => {
|
||||||
|
"total" => desc(NUMBER, "Total number of controls (in this category) for this inspec execution."),
|
||||||
|
},
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
# Bundles several results statistics, representing distinct groups of controls
|
||||||
|
STATISTIC_GROUPING = SchemaType.new("Statistic Hash", {
|
||||||
|
"type" => "object",
|
||||||
|
"additionalProperties" => true,
|
||||||
|
"required" => [],
|
||||||
|
"properties" => {
|
||||||
|
"passed" => STATISTIC_ITEM.ref,
|
||||||
|
"skipped" => STATISTIC_ITEM.ref,
|
||||||
|
"failed" => STATISTIC_ITEM.ref,
|
||||||
|
},
|
||||||
|
}, [STATISTIC_ITEM])
|
||||||
|
|
||||||
|
# Contains statistics of an exec run.
|
||||||
|
STATISTICS = SchemaType.new("Statistics", {
|
||||||
|
"type" => "object",
|
||||||
|
"additionalProperties" => true,
|
||||||
|
"required" => ["duration"],
|
||||||
|
"properties" => {
|
||||||
|
"duration" => desc(NUMBER, "How long (in seconds) this inspec exec ran for."),
|
||||||
|
"controls" => desc(STATISTIC_GROUPING.ref, "Breakdowns of control statistics by result"),
|
||||||
|
},
|
||||||
|
}, [STATISTIC_GROUPING])
|
||||||
|
|
||||||
|
# Profile dependencies
|
||||||
|
DEPENDENCY = SchemaType.new("Dependency", {
|
||||||
|
"type" => "object",
|
||||||
|
"additionalProperties" => true, # Weird stuff shows up here sometimes
|
||||||
|
"required" => [], # Mysteriously even in a run profile we can have no status
|
||||||
|
"properties" => {
|
||||||
|
"name" => STRING,
|
||||||
|
"url" => URL,
|
||||||
|
"branch" => STRING,
|
||||||
|
"path" => STRING,
|
||||||
|
"skip_message" => STRING,
|
||||||
|
"status" => STRING,
|
||||||
|
"git" => URL,
|
||||||
|
"supermarket" => STRING,
|
||||||
|
"compliance" => STRING,
|
||||||
|
},
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
# Represents the platform the test was run on
|
||||||
|
PLATFORM = SchemaType.new("Platform", {
|
||||||
|
"type" => "object",
|
||||||
|
"additionalProperties" => true,
|
||||||
|
"required" => %w{name release},
|
||||||
|
"properties" => {
|
||||||
|
"name" => desc(STRING, "The name of the platform this was run on."),
|
||||||
|
"release" => desc(STRING, "The version of the platform this was run on."),
|
||||||
|
"target_id" => STRING,
|
||||||
|
},
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
# Explains what software ran the inspec profile/made this file. Typically inspec but could in theory be a different software
|
||||||
|
GENERATOR = SchemaType.new("Generator", {
|
||||||
|
"type" => "object",
|
||||||
|
"additionalProperties" => true,
|
||||||
|
"required" => %w{name version},
|
||||||
|
"properties" => {
|
||||||
|
"name" => desc(STRING, "The name of the software that generated this report."),
|
||||||
|
"version" => desc(STRING, "The version of the software that generated this report."),
|
||||||
|
},
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
# Occurs from "exec --reporter json" and "inspec json"
|
||||||
|
# Denotes what file this control comes from, and where within
|
||||||
|
SOURCE_LOCATION = SchemaType.new("Source Location", {
|
||||||
|
"type" => "object",
|
||||||
|
"additionalProperties" => true,
|
||||||
|
"properties" => {
|
||||||
|
"ref" => desc(STRING, "Path to the file that this statement originates from"),
|
||||||
|
"line" => desc(NUMBER, "The line at which this statement is located in the file"),
|
||||||
|
},
|
||||||
|
"required" => %w{ref line},
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
# References an external document
|
||||||
|
REFERENCE = SchemaType.new("Reference", {
|
||||||
|
# Needs at least one of title (ref), url, or uri.
|
||||||
|
"anyOf" => [
|
||||||
|
{
|
||||||
|
"type" => "object",
|
||||||
|
"required" => ["ref"],
|
||||||
|
"properties" => { "ref" => STRING },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type" => "object",
|
||||||
|
"required" => ["url"],
|
||||||
|
"properties" => { "url" => STRING },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type" => "object",
|
||||||
|
"required" => ["uri"],
|
||||||
|
"properties" => { "uri" => STRING },
|
||||||
|
},
|
||||||
|
# I am of the opinion that this is just an error in the codebase itself. See profiles/wrapper-override to uncover new mysteries!
|
||||||
|
{
|
||||||
|
"type" => "object",
|
||||||
|
"required" => ["ref"],
|
||||||
|
"properties" => { "ref" => array(OBJECT) },
|
||||||
|
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
# Represents a group of controls within a profile/.rb file
|
||||||
|
CONTROL_GROUP = SchemaType.new("Control Group", {
|
||||||
|
"type" => "object",
|
||||||
|
"additionalProperties" => true,
|
||||||
|
"required" => %w{id controls},
|
||||||
|
"properties" => {
|
||||||
|
"id" => desc(STRING, "The unique identifier of the group"),
|
||||||
|
"title" => desc({ type: %w{string null} }, "The name of the group"),
|
||||||
|
"controls" => desc(array(STRING), "The control IDs in this group"),
|
||||||
|
},
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
# Occurs from "inspec exec --reporter json" and "inspec json"
|
||||||
|
# Represents a platfrom or group of platforms that this profile supports
|
||||||
|
SUPPORT = SchemaType.new("Supported Platform", {
|
||||||
|
"type" => "object",
|
||||||
|
"additionalProperties" => true, # NOTE: This should really be false, and inspec should validate profiles to ensure people don't make dumb mistakes like platform_family
|
||||||
|
"required" => [],
|
||||||
|
"properties" => {
|
||||||
|
"platform-family" => STRING,
|
||||||
|
"platform-name" => STRING,
|
||||||
|
"platform" => STRING,
|
||||||
|
"release" => STRING,
|
||||||
|
# os-* supports are being deprecated
|
||||||
|
"os-family" => STRING,
|
||||||
|
"os-name" => STRING,
|
||||||
|
},
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
60
lib/inspec/schema/profile_json.rb
Normal file
60
lib/inspec/schema/profile_json.rb
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
require "inspec/schema/primitives"
|
||||||
|
|
||||||
|
# These type occur only when running "inspec json <file>".
|
||||||
|
|
||||||
|
module Inspec
|
||||||
|
module Schema
|
||||||
|
module ProfileJson
|
||||||
|
# Represents descriptions. Can have any string => string pairing
|
||||||
|
CONTROL_DESCRIPTIONS = Primitives::SchemaType.new("Profile JSON Control Descriptions", {
|
||||||
|
"type" => "object",
|
||||||
|
"aditionalProperties" => Primitives::STRING,
|
||||||
|
"required" => [],
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
# Represents a control that hasn't been run
|
||||||
|
# Differs slightly from a normal control, in that it lacks results, and its descriptions are different
|
||||||
|
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 of this control"),
|
||||||
|
"title" => { "type" => %w{string null} },
|
||||||
|
"desc" => { "type" => %w{string null} },
|
||||||
|
"descriptions" => CONTROL_DESCRIPTIONS.ref,
|
||||||
|
"impact" => Primitives::IMPACT,
|
||||||
|
"refs" => Primitives.array(Primitives::REFERENCE.ref),
|
||||||
|
"tags" => Primitives::TAGS,
|
||||||
|
"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::SOURCE_LOCATION.ref,
|
||||||
|
},
|
||||||
|
}, [CONTROL_DESCRIPTIONS, Primitives::REFERENCE, Primitives::SOURCE_LOCATION])
|
||||||
|
|
||||||
|
# A profile that has not been run.
|
||||||
|
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::STRING,
|
||||||
|
"supports" => Primitives.array(Primitives::SUPPORT.ref),
|
||||||
|
"controls" => Primitives.array(CONTROL.ref),
|
||||||
|
"groups" => Primitives.array(Primitives::CONTROL_GROUP.ref),
|
||||||
|
"inputs" => Primitives.array(Primitives::INPUT),
|
||||||
|
"sha256" => Primitives::STRING,
|
||||||
|
"status" => Primitives::STRING,
|
||||||
|
"generator" => Primitives::GENERATOR.ref,
|
||||||
|
"version" => Primitives::STRING,
|
||||||
|
|
||||||
|
# Other properties possible in inspec docs, but that aren"t guaranteed
|
||||||
|
"title" => Primitives::STRING,
|
||||||
|
"maintainer" => Primitives::STRING,
|
||||||
|
"copyright" => Primitives::STRING,
|
||||||
|
"copyright_email" => Primitives::STRING,
|
||||||
|
"depends" => Primitives.array(Primitives::DEPENDENCY.ref), # Can have depends, but NOT a parentprofile
|
||||||
|
},
|
||||||
|
}, [Primitives::SUPPORT, CONTROL, Primitives::CONTROL_GROUP, Primitives::DEPENDENCY, Primitives::GENERATOR])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,35 +0,0 @@
|
||||||
# copyright: 2016, Chef Software, Inc.
|
|
||||||
|
|
||||||
title 'Example Config Checks'
|
|
||||||
|
|
||||||
# To pass the test, create the following file
|
|
||||||
# ```bash
|
|
||||||
# mkdir -p /tmp/example
|
|
||||||
# cat <<EOF > /tmp/example/config.yaml
|
|
||||||
# version: '1.0'
|
|
||||||
# EOF
|
|
||||||
# ```
|
|
||||||
control 'example-1.0' do
|
|
||||||
impact 'critical'
|
|
||||||
title 'Verify the version number of Example'
|
|
||||||
desc 'An optional description...'
|
|
||||||
tag 'example'
|
|
||||||
ref 'Example Requirements 1.0', uri: 'http://...'
|
|
||||||
|
|
||||||
# Test using the custom example_config InSpec resource
|
|
||||||
# Find the resource content here: ../libraries/
|
|
||||||
describe example_config do
|
|
||||||
it { should exist }
|
|
||||||
its('version') { should eq('1.0') }
|
|
||||||
its('file_size') { should <= 20 }
|
|
||||||
its('comma_count') { should eq 0 }
|
|
||||||
end
|
|
||||||
|
|
||||||
# Test the version again to showcase variables
|
|
||||||
g = example_config
|
|
||||||
g_path = g.file_path
|
|
||||||
g_version = g.version
|
|
||||||
describe file(g_path) do
|
|
||||||
its('content') { should match g_version }
|
|
||||||
end
|
|
||||||
end
|
|
10
test/fixtures/profiles/old-examples/profile/controls/minimal.rb
vendored
Normal file
10
test/fixtures/profiles/old-examples/profile/controls/minimal.rb
vendored
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
title 'Minimal control'
|
||||||
|
|
||||||
|
# you add controls here
|
||||||
|
control 'minimalist' do # A unique ID for this control
|
||||||
|
impact 0.7 # The criticality, if this control fails.
|
||||||
|
|
||||||
|
describe file('/') do # The actual test
|
||||||
|
it { should be_directory }
|
||||||
|
end
|
||||||
|
end
|
|
@ -25,6 +25,11 @@ module FunctionalHelper
|
||||||
let(:mock_path) { File.join(repo_path, "test", "fixtures") }
|
let(:mock_path) { File.join(repo_path, "test", "fixtures") }
|
||||||
let(:profile_path) { File.join(mock_path, "profiles") }
|
let(:profile_path) { File.join(mock_path, "profiles") }
|
||||||
let(:examples_path) { File.join(profile_path, "old-examples") }
|
let(:examples_path) { File.join(profile_path, "old-examples") }
|
||||||
|
let(:integration_test_path) { File.join(repo_path, "test", "integration", "default") }
|
||||||
|
let(:all_profiles) { Dir.glob("#{profile_path}/**/inspec.yml") }
|
||||||
|
let(:all_profile_folders) { all_profiles.map { |path| File.dirname(path) } }
|
||||||
|
|
||||||
|
let(:complete_profile) { "#{profile_path}/complete-profile" }
|
||||||
let(:example_profile) { File.join(examples_path, "profile") }
|
let(:example_profile) { File.join(examples_path, "profile") }
|
||||||
let(:meta_profile) { File.join(examples_path, "meta-profile") }
|
let(:meta_profile) { File.join(examples_path, "meta-profile") }
|
||||||
let(:example_control) { File.join(example_profile, "controls", "example-tmp.rb") }
|
let(:example_control) { File.join(example_profile, "controls", "example-tmp.rb") }
|
||||||
|
|
|
@ -55,7 +55,7 @@ describe "example inheritance profile" do
|
||||||
s = out.stdout
|
s = out.stdout
|
||||||
hm = JSON.load(s)
|
hm = JSON.load(s)
|
||||||
_(hm["name"]).must_equal "inheritance"
|
_(hm["name"]).must_equal "inheritance"
|
||||||
_(hm["controls"].length).must_equal 5
|
_(hm["controls"].length).must_equal 6
|
||||||
assert_exit_code 0, out
|
assert_exit_code 0, out
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -66,15 +66,17 @@ describe "example inheritance profile" do
|
||||||
s = out.stdout
|
s = out.stdout
|
||||||
hm = JSON.load(s)
|
hm = JSON.load(s)
|
||||||
_(hm["name"]).must_equal "inheritance"
|
_(hm["name"]).must_equal "inheritance"
|
||||||
_(hm["controls"].length).must_equal 5
|
_(hm["controls"].length).must_equal 6
|
||||||
assert_exit_code 0, out
|
assert_exit_code 0, out
|
||||||
end
|
end
|
||||||
|
|
||||||
it "can execute a profile inheritance" do
|
it "can execute a profile inheritance" do
|
||||||
|
# TODO: the inheritence profile uses here fails on windows.
|
||||||
|
skip_windows!
|
||||||
out = inspec("exec " + path + " --reporter json --no-create-lockfile --input-file " + input_file)
|
out = inspec("exec " + path + " --reporter json --no-create-lockfile --input-file " + input_file)
|
||||||
|
|
||||||
_(out.stderr).must_equal ""
|
_(out.stderr).must_equal ""
|
||||||
_(JSON.load(out.stdout)).must_be_kind_of Hash
|
_(JSON.load(out.stdout)).must_be_kind_of Hash
|
||||||
assert_exit_code 101, out
|
assert_exit_code 0, out
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -16,7 +16,7 @@ describe "inspec exec automate" do
|
||||||
end
|
end
|
||||||
|
|
||||||
let(:invocation) do
|
let(:invocation) do
|
||||||
"exec #{example_profile} --config #{config_path}"
|
"exec #{complete_profile} --config #{config_path}"
|
||||||
end
|
end
|
||||||
|
|
||||||
let(:run_result) { run_inspec_process(invocation) }
|
let(:run_result) { run_inspec_process(invocation) }
|
||||||
|
@ -95,7 +95,7 @@ describe "inspec exec automate" do
|
||||||
_(json["passthrough"].keys.sort).must_equal %w{another_tramp_datum projects}
|
_(json["passthrough"].keys.sort).must_equal %w{another_tramp_datum projects}
|
||||||
_(json["passthrough"]["projects"]).must_equal %w{alpha beta}
|
_(json["passthrough"]["projects"]).must_equal %w{alpha beta}
|
||||||
|
|
||||||
assert_exit_code 101, run_result
|
assert_exit_code 0, run_result
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
require "functional/helper"
|
require "functional/helper"
|
||||||
require "json-schema"
|
require "json_schemer"
|
||||||
require "inspec/schema"
|
|
||||||
|
|
||||||
describe "inspec exec with json formatter" do
|
describe "inspec exec with json formatter" do
|
||||||
include FunctionalHelper
|
include FunctionalHelper
|
||||||
|
@ -11,32 +10,70 @@ describe "inspec exec with json formatter" do
|
||||||
it "can execute a simple file and validate the json schema" do
|
it "can execute a simple file and validate the json schema" do
|
||||||
out = inspec("exec " + example_control + " --reporter json --no-create-lockfile")
|
out = inspec("exec " + example_control + " --reporter json --no-create-lockfile")
|
||||||
data = JSON.parse(out.stdout)
|
data = JSON.parse(out.stdout)
|
||||||
_(JSON::Validator.validate(schema, data)).wont_equal false
|
sout = inspec("schema exec-json")
|
||||||
|
schema = JSONSchemer.schema(sout.stdout)
|
||||||
|
_(schema.validate(data).to_a).must_equal []
|
||||||
|
|
||||||
|
_(out.stderr).must_be_empty
|
||||||
|
|
||||||
|
skip_windows!
|
||||||
|
assert_exit_code 0, out
|
||||||
|
end
|
||||||
|
|
||||||
|
it "can execute a profile and validate the json schema" do
|
||||||
|
out = inspec("exec " + complete_profile + " --reporter json --no-create-lockfile")
|
||||||
|
data = JSON.parse(out.stdout)
|
||||||
|
sout = inspec("schema exec-json")
|
||||||
|
schema = JSONSchemer.schema(sout.stdout)
|
||||||
|
_(schema.validate(data).to_a).must_equal []
|
||||||
|
|
||||||
_(out.stderr).must_equal ""
|
_(out.stderr).must_equal ""
|
||||||
|
|
||||||
assert_exit_code 0, out
|
assert_exit_code 0, out
|
||||||
end
|
end
|
||||||
|
|
||||||
it "can execute a profile and validate the json schema" do
|
it "can execute a simple file while using end of options after reporter cli option" do
|
||||||
out = inspec("exec " + example_profile + " --reporter json --no-create-lockfile")
|
out = inspec("exec --no-create-lockfile --reporter json -- " + example_control)
|
||||||
|
_(out.stderr).must_equal ""
|
||||||
|
_(out.exit_status).must_equal 0
|
||||||
data = JSON.parse(out.stdout)
|
data = JSON.parse(out.stdout)
|
||||||
_(JSON::Validator.validate(schema, data)).wont_equal false
|
sout = inspec("schema exec-json")
|
||||||
|
schema = JSONSchemer.schema(sout.stdout)
|
||||||
|
_(schema.validate(data).to_a).must_equal []
|
||||||
|
|
||||||
_(out.stderr).must_equal ""
|
_(out.stderr).must_equal ""
|
||||||
|
|
||||||
assert_exit_code 101, out
|
skip_windows!
|
||||||
|
assert_exit_code 0, out
|
||||||
end
|
end
|
||||||
|
|
||||||
it "can execute a profile and validate the json schema with target_id" do
|
it "can execute a profile and validate the json schema with target_id" do
|
||||||
out = inspec("exec " + example_profile + " --reporter json --no-create-lockfile --target-id 1d3e399f-4d71-4863-ac54-84d437fbc444")
|
out = inspec("exec " + complete_profile + " --reporter json --no-create-lockfile --target-id 1d3e399f-4d71-4863-ac54-84d437fbc444")
|
||||||
data = JSON.parse(out.stdout)
|
data = JSON.parse(out.stdout)
|
||||||
_(data["platform"]["target_id"]).must_equal "1d3e399f-4d71-4863-ac54-84d437fbc444"
|
_(data["platform"]["target_id"]).must_equal "1d3e399f-4d71-4863-ac54-84d437fbc444"
|
||||||
_(JSON::Validator.validate(schema, data)).wont_equal false
|
sout = inspec("schema exec-json")
|
||||||
|
schema = JSONSchemer.schema(sout.stdout)
|
||||||
|
_(schema.validate(data).to_a).must_equal []
|
||||||
|
|
||||||
_(out.stderr).must_equal ""
|
_(out.stderr).must_equal ""
|
||||||
|
|
||||||
assert_exit_code 101, out
|
assert_exit_code 0, out
|
||||||
|
end
|
||||||
|
|
||||||
|
it "properly validates all (valid) unit tests against the schema" do
|
||||||
|
schema = JSONSchemer.schema(JSON.parse(inspec("schema exec-json").stdout))
|
||||||
|
all_profile_folders.first(1).each do |folder|
|
||||||
|
begin
|
||||||
|
out = inspec("exec " + folder + " --reporter json --no-create-lockfile")
|
||||||
|
# Ensure it parses properly
|
||||||
|
out = JSON.parse(out.stdout)
|
||||||
|
failures = schema.validate(out).to_a
|
||||||
|
_(failures).must_equal []
|
||||||
|
rescue JSON::ParserError
|
||||||
|
# We don't actually care about these; cannot validate if parsing fails!
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
it "does not report skipped dependent profiles" do
|
it "does not report skipped dependent profiles" do
|
||||||
|
@ -110,7 +147,6 @@ describe "inspec exec with json formatter" do
|
||||||
let(:controls) { profile["controls"] }
|
let(:controls) { profile["controls"] }
|
||||||
let(:ex1) { controls.find { |x| x["id"] == "tmp-1.0" } }
|
let(:ex1) { controls.find { |x| x["id"] == "tmp-1.0" } }
|
||||||
let(:ex2) { controls.find { |x| x["id"] =~ /generated/ } }
|
let(:ex2) { controls.find { |x| x["id"] =~ /generated/ } }
|
||||||
let(:ex3) { profile["controls"].find { |x| x["id"] == "example-1.0" } }
|
|
||||||
let(:check_result) do
|
let(:check_result) do
|
||||||
ex3["results"].find { |x| x["resource"] == "example_config" }
|
ex3["results"].find { |x| x["resource"] == "example_config" }
|
||||||
end
|
end
|
||||||
|
@ -121,7 +157,7 @@ describe "inspec exec with json formatter" do
|
||||||
end
|
end
|
||||||
|
|
||||||
it "maps impact symbols to numbers" do
|
it "maps impact symbols to numbers" do
|
||||||
_(ex3["impact"]).must_equal 0.9
|
_(ex1["impact"]).must_equal 0.7
|
||||||
end
|
end
|
||||||
|
|
||||||
it "has all the metadata" do
|
it "has all the metadata" do
|
||||||
|
@ -148,12 +184,12 @@ describe "inspec exec with json formatter" do
|
||||||
{ "id" => "controls/example-tmp.rb",
|
{ "id" => "controls/example-tmp.rb",
|
||||||
"title" => "/ profile",
|
"title" => "/ profile",
|
||||||
"controls" => ["tmp-1.0", key] },
|
"controls" => ["tmp-1.0", key] },
|
||||||
{ "id" => "controls/example.rb",
|
|
||||||
"title" => "Example Config Checks",
|
|
||||||
"controls" => ["example-1.0"] },
|
|
||||||
{ "id" => "controls/meta.rb",
|
{ "id" => "controls/meta.rb",
|
||||||
"title" => "SSH Server Configuration",
|
"title" => "SSH Server Configuration",
|
||||||
"controls" => ["ssh-1"] },
|
"controls" => ["ssh-1"] },
|
||||||
|
{ "id" => "controls/minimal.rb",
|
||||||
|
"title" => "Minimal control",
|
||||||
|
"controls" => ["minimalist"] },
|
||||||
])
|
])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -168,7 +204,6 @@ describe "inspec exec with json formatter" do
|
||||||
it "has results for every control" do
|
it "has results for every control" do
|
||||||
_(ex1["results"].length).must_equal 1
|
_(ex1["results"].length).must_equal 1
|
||||||
_(ex2["results"].length).must_equal 1
|
_(ex2["results"].length).must_equal 1
|
||||||
_(ex3["results"].length).must_equal 2
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it "has the right result for tmp-1.0" do
|
it "has the right result for tmp-1.0" do
|
||||||
|
|
|
@ -1,27 +1,57 @@
|
||||||
require "functional/helper"
|
require "functional/helper"
|
||||||
require "json-schema"
|
require "json_schemer"
|
||||||
require "inspec/schema"
|
|
||||||
|
|
||||||
describe "inspec exec" do
|
describe "inspec exec" do
|
||||||
include FunctionalHelper
|
include FunctionalHelper
|
||||||
|
|
||||||
parallelize_me!
|
parallelize_me!
|
||||||
|
|
||||||
let(:out) { inspec("exec " + example_profile + " --reporter json-min --no-create-lockfile") }
|
let(:out) { inspec("exec " + complete_profile + " --reporter json-min --no-create-lockfile") }
|
||||||
let(:json) { JSON.load(out.stdout) }
|
let(:json) { JSON.load(out.stdout) }
|
||||||
|
|
||||||
it "can execute a profile with the mini json formatter and validate its schema" do
|
it "can execute a profile with the mini json formatter and validate its schema" do
|
||||||
data = JSON.parse(out.stdout)
|
data = JSON.parse(out.stdout)
|
||||||
sout = Inspec::Schema.json("exec-jsonmin")
|
sout = inspec("schema exec-jsonmin")
|
||||||
schema = JSON.parse(sout)
|
schema = JSONSchemer.schema(sout.stdout)
|
||||||
_(JSON::Validator.validate(schema, data)).wont_equal false
|
_(schema.validate(data).to_a).must_equal []
|
||||||
|
|
||||||
_(out.stderr).must_equal ""
|
_(out.stderr).must_equal ""
|
||||||
|
|
||||||
assert_exit_code 101, out
|
assert_exit_code 0, out
|
||||||
end
|
end
|
||||||
|
|
||||||
it "does not contain any duplicate results with describe.one" do
|
it "can execute a simple file with the mini json formatter and validate its schema" do
|
||||||
|
out = inspec("exec " + example_control + " --reporter json-min --no-create-lockfile")
|
||||||
|
_(out.stderr).must_equal ""
|
||||||
|
_(out.exit_status).must_equal 0
|
||||||
|
data = JSON.parse(out.stdout)
|
||||||
|
sout = inspec("schema exec-jsonmin")
|
||||||
|
schema = JSONSchemer.schema(sout.stdout)
|
||||||
|
_(schema.validate(data).to_a).must_equal []
|
||||||
|
|
||||||
|
_(out.stderr).must_equal ""
|
||||||
|
|
||||||
|
skip_windows!
|
||||||
|
assert_exit_code 0, out
|
||||||
|
end
|
||||||
|
|
||||||
|
it "properly validates all (valid) unit tests against the schema" do
|
||||||
|
schema = JSONSchemer.schema(JSON.parse(inspec("schema exec-jsonmin").stdout))
|
||||||
|
all_profile_folders.first(1).each do |folder|
|
||||||
|
begin
|
||||||
|
out = inspec("exec " + folder + " --reporter json-min --no-create-lockfile")
|
||||||
|
# Ensure it parses properly; discard the result
|
||||||
|
out = JSON.parse(out.stdout)
|
||||||
|
failures = schema.validate(out).to_a
|
||||||
|
_(failures).must_equal []
|
||||||
|
rescue JSON::ParserError
|
||||||
|
# We don't actually care about these; cannot validate if parsing fails!
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it "does not contain any dupilcate results with describe.one" do
|
||||||
out = inspec("shell -c 'describe.one do describe 1 do it { should cmp 2 } end end' --reporter=json-min")
|
out = inspec("shell -c 'describe.one do describe 1 do it { should cmp 2 } end end' --reporter=json-min")
|
||||||
data = JSON.parse(out.stdout)
|
data = JSON.parse(out.stdout)
|
||||||
_(data["controls"].length).must_equal 1
|
_(data["controls"].length).must_equal 1
|
||||||
|
@ -34,17 +64,15 @@ describe "inspec exec" do
|
||||||
|
|
||||||
describe "execute a profile with mini json formatting" do
|
describe "execute a profile with mini json formatting" do
|
||||||
let(:controls) { json["controls"] }
|
let(:controls) { json["controls"] }
|
||||||
let(:ex1) { controls.find { |x| x["id"] == "tmp-1.0" } }
|
let(:ex1) { controls.find { |x| x["id"] == "test01" } }
|
||||||
let(:ex2) { controls.find { |x| x["id"] =~ /generated/ } }
|
|
||||||
let(:ex3) { controls.find { |x| x["id"] == "example-1.0" } }
|
|
||||||
|
|
||||||
before do
|
before do
|
||||||
# doesn't make sense on windows TODO: change the profile so it does?
|
# doesn't make sense on windows TODO: change the profile so it does?
|
||||||
skip if windows?
|
skip if windows?
|
||||||
end
|
end
|
||||||
|
|
||||||
it "must have 5 examples" do
|
it "must have 1 example" do
|
||||||
_(json["controls"].length).must_equal 5
|
_(json["controls"].length).must_equal 1
|
||||||
end
|
end
|
||||||
|
|
||||||
it "has an id" do
|
it "has an id" do
|
||||||
|
@ -56,19 +84,12 @@ describe "inspec exec" do
|
||||||
end
|
end
|
||||||
|
|
||||||
it "has a code_desc" do
|
it "has a code_desc" do
|
||||||
_(ex1["code_desc"]).must_equal "File / is expected to be directory"
|
_(ex1["code_desc"]).must_equal "Host example.com is expected to be resolvable"
|
||||||
_(controls.find { |ex| !ex.key? "code_desc" }).must_be :nil?
|
_(controls.find { |ex| !ex.key? "code_desc" }).must_be :nil?
|
||||||
end
|
end
|
||||||
|
|
||||||
it "has a status" do
|
it "has a status" do
|
||||||
skip_windows!
|
|
||||||
_(ex1["status"]).must_equal "passed"
|
_(ex1["status"]).must_equal "passed"
|
||||||
_(ex3["status"]).must_equal "skipped"
|
|
||||||
end
|
|
||||||
|
|
||||||
it "has a skip_message" do
|
|
||||||
_(ex1["skip_message"]).must_be :nil?
|
|
||||||
_(ex3["skip_message"]).must_equal "Can't find file `/tmp/example/config.yaml`"
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,7 @@ describe "inspec exec with junit formatter" do
|
||||||
end
|
end
|
||||||
|
|
||||||
it "can execute the profile with the junit formatter" do
|
it "can execute the profile with the junit formatter" do
|
||||||
out = inspec("exec " + example_profile + " --reporter junit --no-create-lockfile")
|
out = inspec("exec " + complete_profile + " --reporter junit --no-create-lockfile")
|
||||||
|
|
||||||
# TODO: _never_ use rexml. Anything else is guaranteed faster
|
# TODO: _never_ use rexml. Anything else is guaranteed faster
|
||||||
doc = REXML::Document.new(out.stdout)
|
doc = REXML::Document.new(out.stdout)
|
||||||
|
@ -28,7 +28,7 @@ describe "inspec exec with junit formatter" do
|
||||||
|
|
||||||
_(out.stderr).must_equal ""
|
_(out.stderr).must_equal ""
|
||||||
|
|
||||||
assert_exit_code 101, out
|
assert_exit_code 0, out
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "execute a profile with junit formatting" do
|
describe "execute a profile with junit formatting" do
|
||||||
|
@ -42,12 +42,12 @@ describe "inspec exec with junit formatter" do
|
||||||
describe "the test suite" do
|
describe "the test suite" do
|
||||||
let(:suite) { doc.elements.to_a("//testsuites/testsuite").first }
|
let(:suite) { doc.elements.to_a("//testsuites/testsuite").first }
|
||||||
|
|
||||||
it "must have 5 testcase children" do
|
it "must have 6 testcase children" do
|
||||||
_(suite.elements.to_a("//testcase").length).must_equal 5
|
_(suite.elements.to_a("//testcase").length).must_equal 4
|
||||||
end
|
end
|
||||||
|
|
||||||
it "has the tests attribute with 5 total tests" do
|
it "has the tests attribute with 5 total tests" do
|
||||||
_(suite.attribute("tests").value).must_equal "5"
|
_(suite.attribute("tests").value).must_equal "4"
|
||||||
end
|
end
|
||||||
|
|
||||||
it "has the failures attribute with 0 total tests" do
|
it "has the failures attribute with 0 total tests" do
|
||||||
|
@ -56,7 +56,7 @@ describe "inspec exec with junit formatter" do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'has 2 elements named "File / should be directory"' do
|
it 'has 2 elements named "File / should be directory"' do
|
||||||
_(REXML::XPath.match(suite, "//testcase[@name='File / is expected to be directory']").length).must_equal 2
|
_(REXML::XPath.match(suite, "//testcase[@name='File / is expected to be directory']").length).must_equal 3
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'the testcase named "example_config Can\'t find file ..."' do
|
describe 'the testcase named "example_config Can\'t find file ..."' do
|
||||||
|
@ -64,10 +64,12 @@ describe "inspec exec with junit formatter" do
|
||||||
let(:first_example_test) { example_yml_tests.first }
|
let(:first_example_test) { example_yml_tests.first }
|
||||||
|
|
||||||
it "should be unique" do
|
it "should be unique" do
|
||||||
|
skip
|
||||||
_(example_yml_tests.length).must_equal 1
|
_(example_yml_tests.length).must_equal 1
|
||||||
end
|
end
|
||||||
|
|
||||||
it "should be skipped" do
|
it "should be skipped" do
|
||||||
|
skip
|
||||||
if is_windows?
|
if is_windows?
|
||||||
_(first_example_test.elements.to_a("//skipped").length).must_equal 2
|
_(first_example_test.elements.to_a("//skipped").length).must_equal 2
|
||||||
else
|
else
|
||||||
|
|
|
@ -41,26 +41,14 @@ describe "inspec exec" do
|
||||||
end
|
end
|
||||||
|
|
||||||
it "can execute the profile" do
|
it "can execute the profile" do
|
||||||
inspec("exec " + example_profile + " --no-create-lockfile")
|
inspec("exec " + complete_profile + " --no-create-lockfile")
|
||||||
|
|
||||||
_(stdout).must_include " ✔ tmp-1.0: Create / directory\n"
|
_(stdout).must_include "Host example.com"
|
||||||
_(stdout).must_include "
|
_(stdout).must_include "1 successful control, "\
|
||||||
↺ example-1.0: Verify the version number of Example (1 skipped)
|
"0 control failures, 0 controls skipped"
|
||||||
↺ Can't find file `/tmp/example/config.yaml`
|
_(stderr).must_be_empty
|
||||||
"
|
|
||||||
if is_windows?
|
|
||||||
_(stdout).must_include " ↺ ssh-1: Allow only SSH Protocol 2\n"
|
|
||||||
_(stdout).must_include "\nProfile Summary: 1 successful control, 0 control failures, 2 controls skipped\n"
|
|
||||||
_(stdout).must_include "\nTest Summary: 3 successful, 0 failures, 2 skipped\n"
|
|
||||||
else
|
|
||||||
_(stdout).must_include " ✔ ssh-1: Allow only SSH Protocol 2\n"
|
|
||||||
_(stdout).must_include "\nProfile Summary: 2 successful controls, 0 control failures, 1 control skipped\n"
|
|
||||||
_(stdout).must_include "\nTest Summary: 4 successful, 0 failures, 1 skipped\n"
|
|
||||||
end
|
|
||||||
|
|
||||||
_(stderr).must_equal ""
|
assert_exit_code 0, out
|
||||||
|
|
||||||
assert_exit_code 101, out
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it "executes a minimum metadata-only profile" do
|
it "executes a minimum metadata-only profile" do
|
||||||
|
@ -82,14 +70,14 @@ Test Summary: 0 successful, 0 failures, 0 skipped
|
||||||
|
|
||||||
it "can execute the profile and write to directory" do
|
it "can execute the profile and write to directory" do
|
||||||
outpath = Dir.tmpdir
|
outpath = Dir.tmpdir
|
||||||
inspec("exec #{example_profile} --no-create-lockfile --reporter json:#{outpath}/foo/bar/test.json")
|
inspec("exec #{complete_profile} --no-create-lockfile --reporter json:#{outpath}/foo/bar/test.json")
|
||||||
|
|
||||||
_(File.exist?("#{outpath}/foo/bar/test.json")).must_equal true
|
_(File.exist?("#{outpath}/foo/bar/test.json")).must_equal true
|
||||||
_(File.stat("#{outpath}/foo/bar/test.json").size).must_be :>, 0
|
_(File.stat("#{outpath}/foo/bar/test.json").size).must_be :>, 0
|
||||||
|
|
||||||
_(stderr).must_equal ""
|
_(stderr).must_equal ""
|
||||||
|
|
||||||
assert_exit_code 101, out
|
assert_exit_code 0, out
|
||||||
end
|
end
|
||||||
|
|
||||||
it "can execute --help after exec command" do
|
it "can execute --help after exec command" do
|
||||||
|
@ -123,13 +111,13 @@ Test Summary: 0 successful, 0 failures, 0 skipped
|
||||||
end
|
end
|
||||||
|
|
||||||
it "can execute the profile with a target_id passthrough" do
|
it "can execute the profile with a target_id passthrough" do
|
||||||
inspec("exec #{example_profile} --no-create-lockfile --target-id 1d3e399f-4d71-4863-ac54-84d437fbc444")
|
inspec("exec #{complete_profile} --no-create-lockfile --target-id 1d3e399f-4d71-4863-ac54-84d437fbc444")
|
||||||
|
|
||||||
_(stdout).must_include "Target ID: 1d3e399f-4d71-4863-ac54-84d437fbc444"
|
_(stdout).must_include "Target ID: 1d3e399f-4d71-4863-ac54-84d437fbc444"
|
||||||
|
|
||||||
_(stderr).must_equal ""
|
_(stderr).must_equal ""
|
||||||
|
|
||||||
assert_exit_code 101, out
|
assert_exit_code 0, out
|
||||||
end
|
end
|
||||||
|
|
||||||
it "executes a metadata-only profile" do
|
it "executes a metadata-only profile" do
|
||||||
|
@ -228,15 +216,17 @@ Test Summary: 0 successful, 0 failures, 0 skipped
|
||||||
|
|
||||||
it "does not vendor profiles when using the a local path dependecy" do
|
it "does not vendor profiles when using the a local path dependecy" do
|
||||||
Dir.mktmpdir do |tmpdir|
|
Dir.mktmpdir do |tmpdir|
|
||||||
command = "exec " + inheritance_profile + " --no-create-lockfile"
|
command = "exec " + inheritance_profile + " --no-create-lockfile " \
|
||||||
|
"--input-file=#{examples_path}/profile-attribute.yml"
|
||||||
inspec_with_env(command, INSPEC_CONFIG_DIR: tmpdir)
|
inspec_with_env(command, INSPEC_CONFIG_DIR: tmpdir)
|
||||||
|
|
||||||
if is_windows?
|
if is_windows?
|
||||||
_(stdout).must_include "Profile Summary: 0 successful controls, 0 control failures, 2 controls skipped\n"
|
_(stdout).must_include "No tests executed."
|
||||||
_(stdout).must_include "Test Summary: 2 successful, 1 failure, 3 skipped\n"
|
assert_exit_code 1, out
|
||||||
else
|
else
|
||||||
_(stdout).must_include "Profile Summary: 1 successful control, 0 control failures, 1 control skipped\n"
|
_(stdout).must_include "Profile Summary: 2 successful controls, 0 control failures, 0 controls skipped\n"
|
||||||
_(stdout).must_include "Test Summary: 3 successful, 1 failure, 2 skipped\n"
|
_(stdout).must_include "Test Summary: 5 successful, 0 failures, 0 skipped\n"
|
||||||
|
assert_exit_code 0, out
|
||||||
end
|
end
|
||||||
|
|
||||||
cache_dir = File.join(tmpdir, "cache")
|
cache_dir = File.join(tmpdir, "cache")
|
||||||
|
@ -244,8 +234,6 @@ Test Summary: 0 successful, 0 failures, 0 skipped
|
||||||
_(Dir.glob(File.join(cache_dir, "**", "*"))).must_be_empty
|
_(Dir.glob(File.join(cache_dir, "**", "*"))).must_be_empty
|
||||||
|
|
||||||
_(stderr).must_equal ""
|
_(stderr).must_equal ""
|
||||||
|
|
||||||
assert_exit_code 100, out
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -525,7 +513,7 @@ Test Summary: 2 successful, 0 failures, 0 skipped\n"
|
||||||
|
|
||||||
describe "when --password is used" do
|
describe "when --password is used" do
|
||||||
it "raises an exception if no password is provided" do
|
it "raises an exception if no password is provided" do
|
||||||
inspec("exec " + example_profile + " --password")
|
inspec("exec " + complete_profile + " --password")
|
||||||
|
|
||||||
_(stderr).must_include "Please provide a value for --password. For example: --password=hello."
|
_(stderr).must_include "Please provide a value for --password. For example: --password=hello."
|
||||||
|
|
||||||
|
@ -535,7 +523,7 @@ Test Summary: 2 successful, 0 failures, 0 skipped\n"
|
||||||
|
|
||||||
describe "when --sudo-password is used" do
|
describe "when --sudo-password is used" do
|
||||||
it "raises an exception if no sudo password is provided" do
|
it "raises an exception if no sudo password is provided" do
|
||||||
inspec("exec " + example_profile + " --sudo-password")
|
inspec("exec " + complete_profile + " --sudo-password")
|
||||||
|
|
||||||
_(stderr).must_include "Please provide a value for --sudo-password. For example: --sudo-password=hello."
|
_(stderr).must_include "Please provide a value for --sudo-password. For example: --sudo-password=hello."
|
||||||
|
|
||||||
|
@ -545,7 +533,7 @@ Test Summary: 2 successful, 0 failures, 0 skipped\n"
|
||||||
|
|
||||||
describe "when --bastion-host and --proxy_command is used" do
|
describe "when --bastion-host and --proxy_command is used" do
|
||||||
it "raises an exception when both flags are provided" do
|
it "raises an exception when both flags are provided" do
|
||||||
inspec("exec " + example_profile + " -t ssh://dummy@dummy --password dummy --proxy_command dummy --bastion_host dummy")
|
inspec("exec " + complete_profile + " -t ssh://dummy@dummy --password dummy --proxy_command dummy --bastion_host dummy")
|
||||||
|
|
||||||
_(stderr).must_include "Client error, can't connect to 'ssh' backend: Only one of proxy_command or bastion_host needs to be specified"
|
_(stderr).must_include "Client error, can't connect to 'ssh' backend: Only one of proxy_command or bastion_host needs to be specified"
|
||||||
|
|
||||||
|
@ -555,7 +543,7 @@ Test Summary: 2 successful, 0 failures, 0 skipped\n"
|
||||||
|
|
||||||
describe "when --winrm-transport is used" do
|
describe "when --winrm-transport is used" do
|
||||||
it "raises an exception when an invalid transport is given" do
|
it "raises an exception when an invalid transport is given" do
|
||||||
inspec("exec " + example_profile + " -t winrm://administrator@dummy --password dummy --winrm-transport nonesuch")
|
inspec("exec " + complete_profile + " -t winrm://administrator@dummy --password dummy --winrm-transport nonesuch")
|
||||||
|
|
||||||
_(stderr).must_include "Client error, can't connect to 'winrm' backend: Unsupported transport type: :nonesuch\n"
|
_(stderr).must_include "Client error, can't connect to 'winrm' backend: Unsupported transport type: :nonesuch\n"
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
require "functional/helper"
|
require "functional/helper"
|
||||||
require "mixlib/shellout"
|
require "mixlib/shellout"
|
||||||
|
require "json_schemer"
|
||||||
|
|
||||||
describe "inspec json" do
|
describe "inspec json" do
|
||||||
include FunctionalHelper
|
include FunctionalHelper
|
||||||
|
@ -178,4 +179,33 @@ describe "inspec json" do
|
||||||
assert_exit_code 0, out
|
assert_exit_code 0, out
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it "can format a profile and validate the json schema" do
|
||||||
|
out = inspec("json " + example_profile)
|
||||||
|
|
||||||
|
data = JSON.parse(out.stdout)
|
||||||
|
sout = inspec("schema profile-json")
|
||||||
|
schema = JSONSchemer.schema(sout.stdout)
|
||||||
|
_(schema.validate(data).to_a).must_equal []
|
||||||
|
|
||||||
|
_(out.stderr).must_equal ""
|
||||||
|
|
||||||
|
assert_exit_code 0, out
|
||||||
|
end
|
||||||
|
|
||||||
|
it "properly validates all (valid) unit tests against the schema" do
|
||||||
|
schema = JSONSchemer.schema(JSON.parse(inspec("schema profile-json").stdout))
|
||||||
|
all_profile_folders.first(1).each do |folder|
|
||||||
|
begin
|
||||||
|
out = inspec("json " + folder)
|
||||||
|
# Ensure it parses properly; discard the result
|
||||||
|
out = JSON.parse(out.stdout)
|
||||||
|
failures = schema.validate(out).to_a
|
||||||
|
_(failures).must_equal []
|
||||||
|
rescue JSON::ParserError
|
||||||
|
# We don't actually care about these; cannot validate if parsing fails!
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
71
test/unit/schema/primivites_test.rb
Normal file
71
test/unit/schema/primivites_test.rb
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
# Tests that the components of the schema generation framework are behaving properly
|
||||||
|
require "helper"
|
||||||
|
require "inspec/schema/primitives"
|
||||||
|
|
||||||
|
describe Inspec::Schema::Primitives do
|
||||||
|
describe "title construction" do
|
||||||
|
let(:schema_alpha) do
|
||||||
|
Inspec::Schema::Primitives::SchemaType.new("Alpha", {
|
||||||
|
"type" => "string",
|
||||||
|
}, [])
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:schema_beta) do
|
||||||
|
Inspec::Schema::Primitives::SchemaType.new("Beta", {
|
||||||
|
"type" => "object",
|
||||||
|
"required" => [],
|
||||||
|
"properties" => {
|
||||||
|
"param1" => { "type" => "number" },
|
||||||
|
},
|
||||||
|
}, [])
|
||||||
|
end
|
||||||
|
|
||||||
|
# Omega nests alpha and beta
|
||||||
|
let(:schema_omega) do
|
||||||
|
Inspec::Schema::Primitives::SchemaType.new("Omega", {
|
||||||
|
"type" => "object",
|
||||||
|
"required" => ["my_beta"],
|
||||||
|
"properties" => {
|
||||||
|
"my_alpha" => schema_alpha.ref,
|
||||||
|
"my_beta" => schema_beta.ref,
|
||||||
|
},
|
||||||
|
}, [schema_alpha, schema_beta])
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should add the title to schema bodies" do
|
||||||
|
_(schema_alpha.body["title"]).must_equal "Alpha"
|
||||||
|
_(schema_beta.body["title"]).must_equal "Beta"
|
||||||
|
_(schema_omega.body["title"]).must_equal "Omega"
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should properly generate ref keys" do
|
||||||
|
skip "Test Unimplemented"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "property validation" do
|
||||||
|
it "detects if an object does not define required properties" do
|
||||||
|
_ {
|
||||||
|
Inspec::Schema::Primitives::SchemaType.new("Test1", {
|
||||||
|
"type" => "object",
|
||||||
|
"properties" => {
|
||||||
|
"hello" => { "type" => "string" },
|
||||||
|
},
|
||||||
|
}, [])
|
||||||
|
}.must_raise RuntimeError
|
||||||
|
end
|
||||||
|
|
||||||
|
it "detects if a required property is missing" do
|
||||||
|
_ {
|
||||||
|
Inspec::Schema::Primitives::SchemaType.new("Test2", {
|
||||||
|
"type" => "object",
|
||||||
|
"required" => %w{alpha beta},
|
||||||
|
"properties" => {
|
||||||
|
"alpha" => { "type" => "number" },
|
||||||
|
"omega" => { "type" => "number" },
|
||||||
|
},
|
||||||
|
}, [])
|
||||||
|
}.must_raise RuntimeError
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in a new issue