Merge remote-tracking branch 'upstream/master' into add-startuser-systemd-service

Signed-off-by: Mendy Baitelman <mendy@baitelman.com>
This commit is contained in:
Mendy Baitelman 2019-09-24 22:17:07 -07:00
commit d44f9edc55
151 changed files with 3803 additions and 2327 deletions

View file

@ -4,13 +4,16 @@ set -ueo pipefail
export LANG=C.UTF-8 LANGUAGE=C.UTF-8
echo "--- updating rubygems"
gem update --system -N
echo "--- system details"
uname -a
ruby -v
gem env
bundle --version
echo "--- bundle install"
bundle install --jobs=7 --retry=3 --without tools maintenance deploy
echo "+++ bundle exec rake"
bundle exec rake
bundle exec rake ${RAKE_TASK:-}

View file

@ -30,6 +30,7 @@ pipelines:
- LANG: "C.UTF-8"
- SLOW: 1
- NO_AWS: 1
- N: 5
- coverage:
description: Generate test coverage report
public: true
@ -38,7 +39,7 @@ pipelines:
- LANG: "C.UTF-8"
- SLOW: 1
- www/build:
description: Gather documentation from related resource packs and build website.
description: Build website.
definition: .expeditor/wwwbuild.yml
- www/deploy:
description: Deploy website to inspec.io
@ -91,17 +92,38 @@ merge_actions:
ignore_labels:
- "Version: Skip Bump"
- "Expeditor: Skip All"
only_if_modified:
- docs/*
- etc/*
- habitat/*
- inspec-bin/*
- lib/*
- omnibus/*
- support/*
- tasks/*
- test/*
- Gemfile*
- LICENSE
- "*.gemspec"
- "*.md"
- bash:.expeditor/update_version.sh:
only_if: built_in:bump_version
- built_in:update_changelog:
ignore_labels:
- "Changelog: Skip Update"
- "Expeditor: Skip All"
- trigger_pipeline:omnibus/adhoc:
not_if: built_in:bump_version
ignore_labels:
- "Omnibus: Skip Build"
- "Expeditor: Skip All"
- trigger_pipeline:omnibus/release:
only_if: built_in:bump_version
ignore_labels:
- "Omnibus: Skip Build"
- "Expeditor: Skip All"
- trigger_pipeline:habitat/build:
only_if: built_in:bump_version
ignore_labels:
- "Habitat: Skip Build"
- "Expeditor: Skip All"
@ -126,9 +148,19 @@ subscriptions:
- built_in:tag_docker_image
- built_in:promote_habitat_packages
- bash:.expeditor/publish-release-notes.sh:
post_commit: true
post_commit: true
- bash:.expeditor/purge-cdn.sh:
post_commit: true
post_commit: true
- bash:.expeditor/announce-release.sh:
post_commit: true
post_commit: true
- built_in:notify_chefio_slack_channels
- workload: pull_request_opened:{{agent_id}}:*
actions:
- post_github_comment:.expeditor/templates/pull_request.mustache:
ignore_team_members:
- inspec/owners
- inspec/inspec-core-team
- built_in:github_auto_assign_author:
only_if_team_member:
- inspec/owners
- inspec/inspec-core-team

View file

@ -12,12 +12,10 @@ builder-to-testers-map:
el-7-x86_64:
- el-7-x86_64
- el-8-x86_64
mac_os_x-10.12-x86_64:
- mac_os_x-10.12-x86_64
mac_os_x-10.13-x86_64:
- mac_os_x-10.13-x86_64
- mac_os_x-10.14-x86_64
sles-11-x86_64:
- sles-11-x86_64
- mac_os_x-10.15-x86_64
sles-12-x86_64:
- sles-12-x86_64
- sles-15-x86_64

View file

@ -0,0 +1,11 @@
Hello {{author}}! Thanks for the pull request!
Here is what will happen next:
1. Your PR will be reviewed by the maintainers.
2. Possible Outcomes
a. If everything looks good, one of them will approve it, and your PR will be merged.
b. The maintainer may request follow-on work (e.g. code fix, linting, etc). We would encourage you to address this work in 2-3 business days to keep the conversation going and to get your contribution in sooner.
c. Cases exist where a PR is neither aligned to Chef InSpec's product roadmap, or something the team can own or maintain long-term. In these cases, the maintainer will provide justification and close out the PR.
Thank you for contributing!

View file

@ -10,4 +10,4 @@
set -evx
sed -i -r "s/^ARG VERSION=.*/ARG VERSION=${VERSION}/" Dockerfile
sed -i -r "s/^ARG VERSION=.*/ARG VERSION=${EXPEDITOR_VERSION}/" Dockerfile

View file

@ -3,6 +3,9 @@ expeditor:
defaults:
buildkite:
timeout_in_minutes: 45
retry:
automatic:
limit: 1
steps:
@ -30,6 +33,14 @@ steps:
docker:
image: ruby:2.6-stretch
- label: isolated-tests-ruby-2.6
command:
- RAKE_TASK=test:isolated /workdir/.expeditor/buildkite/verify.sh
expeditor:
executor:
docker:
image: ruby:2.6-stretch
- label: run-tests-ruby-2.6-windows
command:
- /workdir/.expeditor/buildkite/verify.ps1

7
.github/CODEOWNERS vendored
View file

@ -1,6 +1,5 @@
# Order is important. The last matching pattern has the most precedence.
* @chef/inspec-team
docs/** @chef/docs-team @chef/inspec-team
.expeditor/** @chef/jex-team
omnibus/** @chef/ben-team
* @inspec/inspec-core-team
docs/** @chef/docs-team @inspec/inspec-core-team
*.md @chef/docs-team @inspec/inspec-core-team

1
.gitignore vendored
View file

@ -1,4 +1,5 @@
*.gem
*.swp
.attribute.yml
.bundle
.delivery/cli.toml

View file

@ -1,12 +1,95 @@
# Change Log
<!-- usage documentation: http://expeditor-docs.es.chef.io/configuration/changelog/ -->
<!-- latest_release -->
<!-- latest_release 4.17.5 -->
## [v4.17.5](https://github.com/inspec/inspec/tree/v4.17.5) (2019-09-25)
#### Merged Pull Requests
- Split and parallelize some tests to make them faster [#4490](https://github.com/inspec/inspec/pull/4490) ([zenspider](https://github.com/zenspider))
<!-- latest_release -->
<!-- release_rollup -->
<!-- release_rollup since=4.16.0 -->
### Changes since 4.16.0 release
#### Bug Fixes
- Use File.realpath in Loader#plugin_gem_path to resolve all symlinks. [#4476](https://github.com/inspec/inspec/pull/4476) ([zenspider](https://github.com/zenspider)) <!-- 4.17.3 -->
- Resolve issue where the Inspec::Tag to_ruby method outputs invalid Ruby [#4434](https://github.com/inspec/inspec/pull/4434) ([irvingpop](https://github.com/irvingpop)) <!-- 4.17.2 -->
- Add to_s definitions to several resources [#4478](https://github.com/inspec/inspec/pull/4478) ([clintoncwolfe](https://github.com/clintoncwolfe)) <!-- 4.16.13 -->
- Fix broken unit test by adding require [#4469](https://github.com/inspec/inspec/pull/4469) ([clintoncwolfe](https://github.com/clintoncwolfe)) <!-- 4.16.12 -->
- Fixed file resource raising UndefinedMethod on source_path [#4214](https://github.com/inspec/inspec/pull/4214) ([zenspider](https://github.com/zenspider)) <!-- 4.16.1 -->
#### Merged Pull Requests
- Split and parallelize some tests to make them faster [#4490](https://github.com/inspec/inspec/pull/4490) ([zenspider](https://github.com/zenspider)) <!-- 4.17.5 -->
- Fixed MssqlSession.query not escaping double quote correctly [#4393](https://github.com/inspec/inspec/pull/4393) ([dalee-bis](https://github.com/dalee-bis)) <!-- 4.17.4 -->
- Waivers Phase 3 [#4493](https://github.com/inspec/inspec/pull/4493) ([miah](https://github.com/miah)) <!-- 4.17.1 -->
- Expand sys_info resource functionality [#4388](https://github.com/inspec/inspec/pull/4388) ([Vancelot11](https://github.com/Vancelot11)) <!-- 4.17.0 -->
- Split out Inspec::Input functional code from the code generation code. [#4485](https://github.com/inspec/inspec/pull/4485) ([zenspider](https://github.com/zenspider)) <!-- 4.16.15 -->
- Added test:isolate task that runs tests isolated but in parallel. [#4480](https://github.com/inspec/inspec/pull/4480) ([zenspider](https://github.com/zenspider)) <!-- 4.16.14 -->
- Fix simple typo: becuase -&gt; because [#4484](https://github.com/inspec/inspec/pull/4484) ([timgates42](https://github.com/timgates42)) <!-- 4.16.14 -->
- Update rubygems and use gem env for system details. [#4475](https://github.com/inspec/inspec/pull/4475) ([zenspider](https://github.com/zenspider)) <!-- 4.16.13 -->
- json resource: Add handling for `command:` error [#3844](https://github.com/inspec/inspec/pull/3844) ([jerryaldrichiii](https://github.com/jerryaldrichiii)) <!-- 4.16.11 -->
- Rough draft on waiver input [#4437](https://github.com/inspec/inspec/pull/4437) ([zenspider](https://github.com/zenspider)) <!-- 4.16.10 -->
- waivers: functional test fixture and eval-time skipping [#4427](https://github.com/inspec/inspec/pull/4427) ([clintoncwolfe](https://github.com/clintoncwolfe)) <!-- 4.16.9 -->
- Make inspec much faster for most commands. [#4365](https://github.com/inspec/inspec/pull/4365) ([zenspider](https://github.com/zenspider)) <!-- 4.16.8 -->
- Try to decode archive contents to UTF-8 to avoid encoding corruption. [#4451](https://github.com/inspec/inspec/pull/4451) ([zenspider](https://github.com/zenspider)) <!-- 4.16.7 -->
- Fix for postfix_conf when using a non-standard config location [#4443](https://github.com/inspec/inspec/pull/4443) ([ramereth](https://github.com/ramereth)) <!-- 4.16.6 -->
- Rework activator plugin to be more idiomatic. [#4446](https://github.com/inspec/inspec/pull/4446) ([zenspider](https://github.com/zenspider)) <!-- 4.16.5 -->
- Clean up our use of SecureRandom in Rakefile. [#4447](https://github.com/inspec/inspec/pull/4447) ([zenspider](https://github.com/zenspider)) <!-- 4.16.4 -->
- Guard against nil in apt repo parser. [#4435](https://github.com/inspec/inspec/pull/4435) ([zenspider](https://github.com/zenspider)) <!-- 4.16.4 -->
- Fixed the formatting of `inspec exec -h` by not using long_desc. [#4436](https://github.com/inspec/inspec/pull/4436) ([zenspider](https://github.com/zenspider)) <!-- 4.16.3 -->
- Extended skip_windows to the end of September. [#4441](https://github.com/inspec/inspec/pull/4441) ([zenspider](https://github.com/zenspider)) <!-- 4.16.2 -->
- update LCR Roadtrip announcement to 13 cities [#4440](https://github.com/inspec/inspec/pull/4440) ([shaunyap](https://github.com/shaunyap)) <!-- 4.16.1 -->
- Use new logo branding [#4433](https://github.com/inspec/inspec/pull/4433) ([btm](https://github.com/btm)) <!-- 4.16.0 -->
<!-- release_rollup -->
<!-- latest_stable_release -->
## [v4.16.0](https://github.com/inspec/inspec/tree/v4.16.0) (2019-08-29)
#### New Resources
- Add a Postfix-specific configuration testing resource [#4378](https://github.com/inspec/inspec/pull/4378) ([dmgasper](https://github.com/dmgasper))
#### New Features
- Accept input keys via the Runner API [#4398](https://github.com/inspec/inspec/pull/4398) ([clintoncwolfe](https://github.com/clintoncwolfe))
- Add plugin config settings to Config [#4406](https://github.com/inspec/inspec/pull/4406) ([clintoncwolfe](https://github.com/clintoncwolfe))
- inputs: Accept bare input from the command line [#4401](https://github.com/inspec/inspec/pull/4401) ([clintoncwolfe](https://github.com/clintoncwolfe))
#### Enhancements
- Ignore .swp files [#4389](https://github.com/inspec/inspec/pull/4389) ([Vancelot11](https://github.com/Vancelot11))
- inspec check: warn if inspec_version is not supported by current inspec [#4419](https://github.com/inspec/inspec/pull/4419) ([clintoncwolfe](https://github.com/clintoncwolfe))
#### Bug Fixes
- Ensure that resources are always initialized in all cases [#4366](https://github.com/inspec/inspec/pull/4366) ([zenspider](https://github.com/zenspider))
- inspec plugins: List system plugins and other UX improvements [#4387](https://github.com/inspec/inspec/pull/4387) ([clintoncwolfe](https://github.com/clintoncwolfe))
#### Merged Pull Requests
- Refactor lib/source_readers/inspec.rb [#4376](https://github.com/inspec/inspec/pull/4376) ([zenspider](https://github.com/zenspider))
- Remove inspec-vault from the plugin exclusion list [#4411](https://github.com/inspec/inspec/pull/4411) ([clintoncwolfe](https://github.com/clintoncwolfe))
- Let expeditor respond to pull requests. [#4430](https://github.com/inspec/inspec/pull/4430) ([miah](https://github.com/miah))
- Deprecate macOS 10.12 and add macOS 10.15 support [#4421](https://github.com/inspec/inspec/pull/4421) ([jaymalasinha](https://github.com/jaymalasinha))
<!-- latest_stable_release -->
## [v4.12.0](https://github.com/inspec/inspec/tree/v4.12.0) (2019-08-15)
#### New Features
- Add startuser windows service [#4363](https://github.com/inspec/inspec/pull/4363) ([mbaitelman](https://github.com/mbaitelman))
#### Bug Fixes
- Fixed syntax errors in wmi doco. [#4370](https://github.com/inspec/inspec/pull/4370) ([zenspider](https://github.com/zenspider))
- group resource: Modified DarwinGroup to collect all users properly on macos [#4343](https://github.com/inspec/inspec/pull/4343) ([zenspider](https://github.com/zenspider))
#### Merged Pull Requests
- Only bump versions when we modify InSpec. [#4348](https://github.com/inspec/inspec/pull/4348) ([miah](https://github.com/miah))
- Move some resources into Inspec::Resources module [#4361](https://github.com/inspec/inspec/pull/4361) ([KrisShannon](https://github.com/KrisShannon))
- Add a retry to expeditor defaults, per shain&#39;s advice. [#4369](https://github.com/inspec/inspec/pull/4369) ([zenspider](https://github.com/zenspider))
- Remove timebombs on skip_until and licensing tests. [#4351](https://github.com/inspec/inspec/pull/4351) ([zenspider](https://github.com/zenspider))
- Stop building Chef InSpec on SLES 11 [#4374](https://github.com/inspec/inspec/pull/4374) ([schisamo](https://github.com/schisamo))
- Fixed typo in umask example [#4360](https://github.com/inspec/inspec/pull/4360) ([kclinden](https://github.com/kclinden))
- README: Add platform detail to support info in runtime list [#4375](https://github.com/inspec/inspec/pull/4375) ([clintoncwolfe](https://github.com/clintoncwolfe))
- I didn&#39;t get the BK setup right, nor did the verification check this. [#4380](https://github.com/inspec/inspec/pull/4380) ([zenspider](https://github.com/zenspider))
- change announcement to LCR Road Trip [#4379](https://github.com/inspec/inspec/pull/4379) ([shaunyap](https://github.com/shaunyap))
- Moved the osx? guard below minitest/autorun. [#4381](https://github.com/inspec/inspec/pull/4381) ([zenspider](https://github.com/zenspider))
- Begin signing MSI&#39;s with renewed Windows Signing Cert [#4386](https://github.com/inspec/inspec/pull/4386) ([schisamo](https://github.com/schisamo))
- Add deps on train 3 and train-winrm 0.2 [#4355](https://github.com/inspec/inspec/pull/4355) ([clintoncwolfe](https://github.com/clintoncwolfe))
## [v4.10.4](https://github.com/inspec/inspec/tree/v4.10.4) (2019-08-01)
#### New Features
@ -28,7 +111,6 @@
- point badge to master [#4347](https://github.com/inspec/inspec/pull/4347) ([miah](https://github.com/miah))
- changed legal refs to point to chef.io proper [#4345](https://github.com/inspec/inspec/pull/4345) ([kekaichinose](https://github.com/kekaichinose))
- Bump skipped license tests by 2 weeks. [#4352](https://github.com/inspec/inspec/pull/4352) ([zenspider](https://github.com/zenspider))
<!-- latest_stable_release -->
## [v4.7.24](https://github.com/inspec/inspec/tree/v4.7.24) (2019-07-26)

View file

@ -1,9 +1,13 @@
FROM ruby:alpine
MAINTAINER Chef Software, Inc. <docker@chef.io>
LABEL maintainer="Chef Software, Inc. <docker@chef.io>"
ARG VERSION=
ARG EXPEDITOR_VERSION
ARG VERSION=4.16.0
ARG GEM_SOURCE=https://rubygems.org
# Allow VERSION below to be controlled by either VERSION or EXPEDITOR_VERSION build arguments
ENV VERSION ${EXPEDITOR_VERSION:-${VERSION}}
RUN mkdir -p /share
RUN apk add --update build-base libxml2-dev libffi-dev git openssh-client
RUN gem install --no-document --source ${GEM_SOURCE} --version ${VERSION} inspec

View file

@ -327,13 +327,13 @@ Remote Targets
In addition, runtime support is provided for:
| Platform | Versions |
| -------- | -------- |
| Debian | 8, 9 |
| RHEL | 6, 7 |
| Ubuntu | 12.04+ |
| Windows | 7+ |
| Windows | 2012+ |
| Platform | Versions | Arch |
| -------- | -------- | ------ |
| Debian | 8, 9 | x86_64 |
| RHEL | 6, 7 | x86_64 |
| Ubuntu | 12.04+ | x86_64 |
| Windows | 7+ | x86_64 |
| Windows | 2012+ | x86_64 |
## Documentation

View file

@ -80,22 +80,83 @@ namespace :test do
missing.reject! { |f| ! File.file? f }
missing.reject! { |f| f =~ %r{test/(integration|cookbooks)} }
missing.reject! { |f| f =~ %r{test/unit/mock} }
missing.reject! { |f| f =~ %r{test.*helper} }
missing.reject! { |f| f =~ /test.*helper/ }
missing.reject! { |f| f =~ %r{test/docker} }
puts missing.sort
end
task :isolated do
failures = Dir[*GLOBS]
failures.reject! do |file|
system(Gem.ruby, "-Ilib:test", file)
# rubocop:disable Style/BlockDelimiters,Layout/ExtraSpacing,Lint/AssignmentInCondition
def n_threads_run(n_workers, jobs)
queue = Queue.new
jobs.each do |job|
queue << job
end
unless failures.empty?
puts "These test files failed:\n"
puts failures
raise "broken tests..."
n_workers.times.map {
queue << nil # 1 quit value per thread
Thread.new do
while job = queue.pop # go until quit value
yield job
end
end
}.each(&:join)
end
task :isolated do
require "fileutils"
require "thread"
# Only needed for local runs, not CI?
FileUtils.rm_rf File.expand_path "~/.inspec"
FileUtils.rm_rf File.expand_path "~/.chef"
# 3 seems to be the magic number... (tho not by that much)
bad, good, n = {}, [], (ENV.delete("N") || 3).to_i
t0 = Time.now
srand 42
tests = Dir[*GLOBS].sort
n_threads_run n, tests do |path|
output = `bundle exec ruby -Ilib:test #{path} 2>&1`
if $?.success?
$stderr.print "."
good << path
else
$stderr.print "x"
bad[path] = output
end
end
puts "done"
puts "Ran in %d seconds" % [ Time.now - t0 ]
unless good.empty?
puts
puts "# Good tests:"
good.sort.each do |path|
puts path
end
end
unless bad.empty?
puts
puts "# Bad tests:"
bad.keys.each do |path|
puts path
end
puts
puts "# Bad Test Output:"
bad.each do |path, output|
puts
puts "# #{path}:"
puts output
end
exit 1
end
end
@ -265,8 +326,8 @@ namespace :test do
# Determine the storage account name and the admin password
require "securerandom"
sa_name = (0...15).map { (65 + rand(26)).chr }.join.downcase
admin_password = SecureRandom.alphanumeric(72)
sa_name = ("a".."z").to_a.sample(15).join
admin_password = SecureRandom.alphanumeric 72
# Use the first 4 characters of the storage account to create a suffix
suffix = sa_name[0..3]

View file

@ -1 +1 @@
4.10.4
4.17.5

View file

@ -1,6 +1,6 @@
# The Chef InSpec Configuration File
This documents the Chef InSpec configuration file format introduced in version 3.5 of InSpec.
This documents the Chef InSpec configuration file format introduced in version 3.5 of InSpec and extended in later versions.
## Config File Location
@ -83,3 +83,30 @@ Credential sets are intended to work hand-in-hand with the underlying credential
### reporter
You may also set output (reporter) options in the config file. See the [Reporters Page](https://www.inspec.io/docs/reference/reporters/) for details.
## Version 1.2
Version 1.2 adds a top-level field, "plugins".
### plugins
Use the `plugins` top-level configuration field to provide configuration settings to plugins that you use with Chef InSpec. Refer to the documentation of the plugin you are using for details regarding what settings are available.
To use this new feature, add a new top-level key in your config file named `plugins`. Then create a sub-key named for each plugin you wish to configure. Each plugin will have a key-value are that it may use as it sees fit - Chef Inspec does not specify the structure. Here is an example, using contrived plugins:
```
{
"version":"1.2",
"plugins": {
"inspec-training-wheels": {
"diameter": "4 inches"
},
"inspec-input-secrets": {
"security-tokens: [
"123456789".
"abcdef252875"
]
}
}
}
```

View file

@ -68,6 +68,12 @@ Putting this all together, here is a plugins.json file from the Chef InSpec test
}
```
### Plugin Runtime Configuration
You can read runtime configuration data from your user using `Inspec::Config.cached.fetch_plugin_config("your-plugin-name")`, which will return a hash with indifferent access representing the user's config file `plugins` section pertaining to your plugin. See [the config file](https://www.inspec.io/docs/reference/config/) for more information.
Do not store or read configuration information from plugins.json.
## Plugin Parts
### A Typical Plugin File Layout

View file

@ -44,13 +44,9 @@ When the above profile is executed by using `inspec exec rock_critic`, you would
Test Summary: 0 successful, 1 failure, 0 skipped
```
That result clearly won't do. Let's override the input's default value. Create a file, `custom_amps.yml`:
That result clearly won't do. Let's override the input's default value.
```yaml
amplifier_max_volume: 11
```
We can now run that profile with `inspec exec rock_critic --input-file custom_amps.yaml`:
We can now run that profile with `inspec exec rock_critic --input amplifier_max_volume=11`:
```
11
@ -67,11 +63,12 @@ That said, any profile that uses the DSL keyword `input()` (or the deprecated `a
### How can I set Inputs?
As installed (without specialized plugins), Chef InSpec supports five ways of setting inputs:
As installed (without specialized plugins), Chef InSpec supports several ways of setting inputs:
* Inline in control code, using `input('input_name', value: 42)`.
* In profile `inspec.yml` metadata files
* Using the CLI option `--input-file somefile.yaml`
* Using the CLI option `--input name1=value1 name2=value2...` to read directly from the command line
* Using the CLI option `--input-file somefile.yaml` to read inputs from files
* In kitchen-inspec, using the `verifier/inputs` settings
* In the Audit Cookbook, using the `node[:audit][:inputs]`
@ -83,7 +80,7 @@ In addition, Chef InSpec supports Input Plugins, which can provide optional inte
Briefly:
inline DSL < metadata < ( cli-input-file or kitchen-inspec or audit-cookbook )
inline DSL < metadata < ( cli-input-file or kitchen-inspec or audit-cookbook ) < cli --input
In addition, for inherited profiles:
@ -122,7 +119,7 @@ As packaged, Chef InSpec uses the following priority values:
| CLI `--input-file` option | 40 | No |
| inspec-kitchen `inputs:` section | 40 | No |
| audit cookbook `node[:audit][:inputs]` | 40 | No |
| CLI `--input` option | 50 | No |
### What happened to "Attributes"?
@ -246,13 +243,35 @@ an_input: a_value
another_input: another_value
```
CLI-set inputs have a priority of 40.
CLI-input-file-set inputs have a priority of 40.
As of Chef InSpec 4.3.2, this mechanism has the following limitations:
1. No [input options](#input-options-reference) may be set - only the name and value.
2. Because the CLI is outside the scope of any individual profile and the inputs don't take options, the inputs are clumsily copied into every profile, effectively making the CLI mechanism global.
## Setting Input values using `--input`
You may also provide inputs and values directly on the command line:
```yaml
inspec exec my_profile --input input_name=input_value
```
To set multiple inputs, say:
```yaml
inspec exec my_profile --input input_name1=input_value1 name2=value2
```
Do not repeat the `--input` flag; that will override the previous setting.
CLI-set inputs have a priority of 50.
As of Chef InSpec 4.12, this mechanism has the following limitations:
1. No [input options](#input-options-reference) may be set - only the name and value.
2. Because the CLI is outside the scope of any individual profile and the inputs don't take options, the inputs are clumsily copied into every profile, effectively making the CLI mechanism global.
## Input Options Reference
### Name

View file

@ -67,7 +67,7 @@ The `name` matcher tests the value of `name` as read from `login.defs` versus th
### Test umask setting
describe login_def do
describe login_defs do
its('UMASK') { should eq '077' }
its('PASS_MAX_DAYS') { should eq '90' }
end

View file

@ -0,0 +1,76 @@
---
title: About the postfix_conf Resource
platform: os
---
# postfix_conf
Use the `postfix_conf` Chef InSpec audit resource to test the main configuration of the Postfix Mail Transfer Agent.
<br>
## Availability
### Installation
This resource is distributed along with Chef InSpec itself. You can use it automatically.
### Version
## Syntax
A `postfix_conf` resource block declares the configuration settings to be tested:
describe postfix_conf do
its('setting_name') { should cmp 'value' }
end
where
* `'setting_name'` is a setting key defined in main.cf
* `{ should cmp 'value' }` is the value to be expected
When using `postfix_conf` with a custom configuration directory, the following syntax can be used:
describe postfix_conf('path') do
...
end
where
* `'path'` is the path to your Postfix configuration (ex. '/etc/my/postfix/path/main.cf')
<br>
## Properties
This resource supports any of the settings listed in the main.cf file as properties.
<br>
## Examples
The following examples show how to use this Chef InSpec audit resource.
### Test basic Postfix configuration settings in the main.cf file
For example, the following Postfix configuration:
/etc/postfix/main.cf:
myorigin = $myhostname
myhostname = host.local.domain
mynetworks = 127.0.0.0/8
can be tested like this:
describe postfix_conf do
its('myorigin') { should cmp '$myhostname' }
its('myhostname') { should cmp 'host.local.domain' }
its('mynetworks') { should cmp '127.0.0.0/8' }
end
<br>
## Matchers
For a full list of available matchers, please visit our [matchers page](https://www.inspec.io/docs/reference/matchers/).

View file

@ -41,6 +41,24 @@ The following examples show how to use this Chef InSpec audit resource.
<br>
### Compare content to hostname
describe file('/path/to/some/file') do
its('content') { should match sys_info.hostname }
end
<br>
Options can be passed as arguments to hostname as well.
describe file('/path/to/some/file') do
its('content') { should match sys_info.hostname('full') }
end
<br>
Currently supported arguments to `hostname` on Linux platforms are 'full'|'f'|'fqdn'|'long', 'domain'|'d', 'ip_address'|'i', and 'short'|'s'. Mac currently supports 'full'|'f'|'fqdn'|'long' and 'short'|'s'
## Matchers
For a full list of available matchers, please visit our [matchers page](https://www.inspec.io/docs/reference/matchers/).
@ -51,6 +69,31 @@ The `hostname` matcher tests the host for which standard output is returned:
its('hostname') { should eq 'value' }
### fqdn
The `fqdn` property tests the 'fully qualified domain name' of the system:
its('fqdn') { should eq 'value' }
### domain
The `domain` property tests the name of the DNS domain:
its('domain') { should eq 'value' }
### ip-address
The `ip-address` property tests all network addresses of the host:
its('ip-address') { should eq 'value' }
### short
The `short` property tests the host name cut at the first dot:
its('short') { should eq 'value' }
### manufacturer
The `manufacturer` matcher tests the host for which standard output is returned:

View file

@ -24,9 +24,9 @@ This resource first became available in v1.0.0 of InSpec.
A `wmi` resource block tests WMI settings on the Windows platform:
describe wmi({
class: 'class_name'
namespace: 'path\\to\\setting'
filter: 'filter'
class: 'class_name',
namespace: 'path\\to\\setting',
filter: 'filter',
query: 'query'
}) do
its('setting_name') { should eq '' }
@ -78,7 +78,7 @@ The following examples show how to use this Chef InSpec audit resource.
### Test if an anonymous user can query the Local Security Authority (LSA)
describe wmi({
namespace: 'root\rsop\computer',
namespace: 'root\\rsop\\computer',
query: "SELECT Setting FROM RSOP_SecuritySettingBoolean WHERE KeyName='LSAAnonymousNameLookup' AND Precedence=1"
}) do
its('Setting') { should eq false }

View file

@ -25,10 +25,6 @@
"plugin_name": "inspec-release",
"rationale": "This gem is currently only a placeholder, waiting to be built."
},
{
"plugin_name": "inspec-vault",
"rationale": "This gem is currently only a placeholder, waiting to be built."
},
{
"plugin_name": "train-vault",
"rationale": "This gem is currently only a placeholder, waiting to be built."

View file

@ -1,5 +1,5 @@
# This file managed by automation - do not edit manually
module InspecBin
INSPECBIN_ROOT = File.expand_path("../..", __FILE__)
VERSION = "4.10.4".freeze
VERSION = "4.17.5".freeze
end

View file

@ -19,7 +19,7 @@ Gem::Specification.new do |spec|
spec.required_ruby_version = ">= 2.4"
spec.add_dependency "train-core", "~> 2.0"
spec.add_dependency "train-core", "~> 3.0"
spec.add_dependency "license-acceptance", ">= 0.2.13", "< 2.0"
spec.add_dependency "thor", "~> 0.20"
spec.add_dependency "json-schema", "~> 2.8"

View file

@ -23,10 +23,11 @@ Gem::Specification.new do |spec|
spec.required_ruby_version = ">= 2.4"
spec.add_dependency "train", "~> 2.0" # Inspec 4 must have train 2+
spec.add_dependency "train", "~> 3.0" # Inspec 4 must have train 2+; 3+ if we include train-winrm
# Train plugins we ship with InSpec
spec.add_dependency "train-habitat", "~> 0.1"
spec.add_dependency "train-aws", "~> 0.1"
spec.add_dependency "train-winrm", "~> 0.2" # Requires train 3+
# Implementation dependencies
spec.add_dependency "license-acceptance", ">= 0.2.13", "< 2.0"

View file

@ -52,6 +52,7 @@ module Fetchers
# processing, but then again, if you passed a relative path
# to an on-disk repo, you probably expect it to exist.
return url_or_file_path unless File.exist?(url_or_file_path)
# It's important to expand this path, because it may be specified
# locally in the metadata files, and when we clone, we will be
# in a temp dir.
@ -97,6 +98,7 @@ module Fetchers
def cache_key
return resolved_ref unless @relative_path
OpenSSL::Digest::SHA256.hexdigest(resolved_ref + @relative_path)
end

View file

@ -28,4 +28,3 @@ require "inspec/base_cli"
require "inspec/fetcher"
require "inspec/source_reader"
require "inspec/resource"
require "inspec/resources"

View file

@ -4,6 +4,7 @@ require "train"
require "inspec/config"
require "inspec/version"
require "inspec/resource"
require "inspec/dsl" # for method_missing_resource
module Inspec
module Backend
@ -77,6 +78,12 @@ module Inspec
connection
end
def method_missing(id, *args, &blk)
Inspec::DSL.method_missing_resource(self, id, *args)
rescue LoadError
super
end
Inspec::Resource.registry.each do |id, r|
define_method id.to_sym do |*args|
r.new(self, id.to_s, *args)

View file

@ -133,8 +133,12 @@ module Inspec
option :reporter, type: :array,
banner: "one two:/output/file/path",
desc: "Enable one or more output reporters: cli, documentation, html, progress, json, json-min, json-rspec, junit, yaml"
option :input, type: :array, banner: "name1=value1 name2=value2",
desc: "Specify one or more inputs directly on the command line, as --input NAME=VALUE"
option :input_file, type: :array,
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 :attrs, type: :array,
desc: "Legacy name for --input-file - deprecated."
option :create_lockfile, type: :boolean,

View file

@ -64,7 +64,6 @@ class Inspec::InspecCLI < Inspec::BaseCLI
desc: "A list of controls to include. Ignore all other tests."
profile_options
def json(target)
require "inspec/resources"
require "json"
o = config
@ -103,8 +102,6 @@ class Inspec::InspecCLI < Inspec::BaseCLI
option :format, type: :string
profile_options
def check(path) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
require "inspec/resources"
o = config
diagnose(o)
o["log_location"] ||= STDERR if o["format"] == "json"
@ -124,8 +121,8 @@ class Inspec::InspecCLI < Inspec::BaseCLI
else
%w{location profile controls timestamp valid}.each do |item|
prepared_string = format("%-12s %s",
"#{item.to_s.capitalize} :",
result[:summary][item.to_sym])
"#{item.to_s.capitalize} :",
result[:summary][item.to_sym])
ui.plain_line(prepared_string)
end
puts
@ -157,8 +154,6 @@ class Inspec::InspecCLI < Inspec::BaseCLI
option :overwrite, type: :boolean, default: false,
desc: "Overwrite existing vendored dependencies and lockfile."
def vendor(path = nil)
require "inspec/resources"
o = config
configure_logger(o)
o[:logger] = Logger.new($stdout)
@ -180,8 +175,6 @@ class Inspec::InspecCLI < Inspec::BaseCLI
option :ignore_errors, type: :boolean, default: false,
desc: "Ignore profile warnings."
def archive(path)
require "inspec/resources"
o = config
diagnose(o)
@ -208,9 +201,9 @@ class Inspec::InspecCLI < Inspec::BaseCLI
pretty_handle_exception(e)
end
desc "exec LOCATIONS", "run all test files at the specified LOCATIONS."
# TODO: find a way for Thor not to butcher the formatting of this
long_desc <<~EOT
desc "exec LOCATIONS", <<~EOT
Run all test files at the specified LOCATIONS.
Loads the given profile(s) and fetches their dependencies if needed. Then
connects to the target and executes any controls contained in the profiles.
One or more reporters are used to generate output.

View file

@ -7,9 +7,12 @@ require "forwardable"
require "thor"
require "base64"
require "inspec/base_cli"
require "inspec/plugin/v2/filter"
module Inspec
class Config
include Inspec::Plugin::V2::FilterPredicates
# These are options that apply to any transport
GENERIC_CREDENTIALS = %w{
backend
@ -23,6 +26,11 @@ module Inspec
shell_command
}.freeze
KNOWN_VERSIONS = [
"1.1",
"1.2",
].freeze
extend Forwardable
# Many parts of InSpec expect to treat the Config as a Hash
@ -48,6 +56,7 @@ module Inspec
def initialize(cli_opts = {}, cfg_io = nil, command_name = nil)
@command_name = command_name || (ARGV.empty? ? nil : ARGV[0].to_sym)
@defaults = Defaults.for_command(@command_name)
@plugin_cfg = {}
@cli_opts = cli_opts.dup
cfg_io = resolve_cfg_io(@cli_opts, cfg_io)
@ -119,6 +128,13 @@ module Inspec
end
end
#-----------------------------------------------------------------------#
# Fetching Plugin Data
#-----------------------------------------------------------------------#
def fetch_plugin_config(plugin_name)
Thor::CoreExt::HashWithIndifferentAccess.new(@plugin_cfg[plugin_name] || {})
end
private
def _utc_merge_transport_options(credentials, transport_name)
@ -187,8 +203,7 @@ module Inspec
def _utc_find_credset_name(_credentials, transport_name)
return nil unless final_options[:target]
match = final_options[:target].match(%r{^#{transport_name}://(?<credset_name>[\w\d\-]+)$})
match = final_options[:target].match(%r{^#{transport_name}://(?<credset_name>[\w\-]+)$})
match ? match[:credset_name] : nil
end
@ -205,8 +220,8 @@ module Inspec
path = determine_cfg_path(cli_opts)
cfg_io = File.open(path) if path
cfg_io || StringIO.new('{ "version": "1.1" }')
ver = KNOWN_VERSIONS.max
path ? File.open(path) : StringIO.new({ "version" => ver }.to_json)
end
def check_for_piped_config(cli_opts)
@ -285,16 +300,24 @@ module Inspec
# Assume legacy format, which is unconstrained
return unless version
unless version == "1.1"
raise Inspec::ConfigError::Invalid, "Unsupported config file version '#{version}' - currently supported versions: 1.1"
unless KNOWN_VERSIONS.include?(version)
raise Inspec::ConfigError::Invalid, "Unsupported config file version '#{version}' - currently supported versions: #{KNOWN_VERSIONS.join(",")}"
end
# Use Gem::Version for comparision operators
cfg_version = Gem::Version.new(version)
version_1_2 = Gem::Version.new("1.2")
# TODO: proper schema version loading and validation
valid_fields = %w{version cli_options credentials compliance reporter}.sort
valid_fields << "plugins" if cfg_version >= version_1_2
@cfg_file_contents.keys.each do |seen_field|
unless valid_fields.include?(seen_field)
raise Inspec::ConfigError::Invalid, "Unrecognized top-level configuration field #{seen_field}. Recognized fields: #{valid_fields.join(", ")}"
end
end
validate_plugins! if cfg_version >= version_1_2
end
def validate_reporters!(reporters)
@ -334,6 +357,29 @@ module Inspec
raise ArgumentError, "The option --reporter can only have a single report outputting to stdout." if stdout_reporters > 1
end
def validate_plugins!
return unless @cfg_file_contents.key? "plugins"
data = @cfg_file_contents["plugins"]
unless data.is_a?(Hash)
raise Inspec::ConfigError::Invalid, "The 'plugin' field in your config file must be a hash (key-value list), not an array."
end
data.each do |plugin_name, plugin_settings|
# Enforce that every key is a valid inspec or train plugin name
unless valid_plugin_name?(plugin_name)
raise Inspec::ConfigError::Invalid, "Plugin settings should ne named after the the InSpec or Train plugin. Valid names must begin with inspec- or train-, not '#{plugin_name}' "
end
# Enforce that every entry is hash-valued
unless plugin_settings.is_a?(Hash)
raise Inspec::ConfigError::Invalid, "The plugin settings for '#{plugin_name}' in your config file should be a Hash (key-value list)."
end
end
@plugin_cfg = data
end
#-----------------------------------------------------------------------#
# Merging Options
#-----------------------------------------------------------------------#

View file

@ -66,9 +66,11 @@ module Inspec
# We still haven't called it, so do so now.
send(method_name, *arguments, &block)
else
# If we couldn't find a plugin to match, maybe something up above has it,
# or maybe it is just a unknown method error.
super
begin
Inspec::DSL.method_missing_resource(inspec, method_name, *arguments)
rescue LoadError
super
end
end
end

View file

@ -3,6 +3,8 @@ require "inspec/log"
require "inspec/plugin/v2"
module Inspec::DSL
attr_accessor :backend
def require_controls(id, &block)
opts = { profile_id: id, include_all: false, backend: @backend, conf: @conf, dependencies: @dependencies }
::Inspec::DSL.load_spec_files_for_profile(self, opts, &block)
@ -25,6 +27,23 @@ module Inspec::DSL
add_resource(target_name, res)
end
##
# Try to load and instantiate a missing resource or raise LoadError
# if unable. Requiring the resource registers it and generates a
# method for it so you should only hit this once per missing
# resource.
def self.method_missing_resource(backend, id, *arguments)
begin
require "inspec/resources/#{id}"
rescue LoadError
require "resources/aws/#{id}"
end
klass = Inspec::Resource.registry[id.to_s]
klass.new(backend, id, *arguments)
end
# Support for Outer Profile DSL plugins
# This is called when an unknown method is encountered
# "bare" in a control file - outside of a control or describe block.
@ -44,7 +63,11 @@ module Inspec::DSL
# We still haven't called it, so do so now.
send(method_name, *arguments, &block)
else
super
begin
Inspec::DSL.method_missing_resource(backend, method_name, *arguments)
rescue LoadError
super
end
end
end

View file

@ -14,31 +14,5 @@ module Inspec
class ConfigError::MalformedJson < ConfigError; end
class ConfigError::Invalid < ConfigError; end
class Input
class Error < Inspec::Error; end
class ValidationError < Error
attr_accessor :input_name
attr_accessor :input_value
attr_accessor :input_type
end
class TypeError < Error
attr_accessor :input_type
end
class RequiredError < Error
attr_accessor :input_name
end
end
class InputRegistry
class Error < Inspec::Error; end
class ProfileLookupError < Error
attr_accessor :profile_name
end
class InputLookupError < Error
attr_accessor :profile_name
attr_accessor :input_name
end
end
class UserInteractionRequired < Error; end
end

View file

@ -51,7 +51,7 @@ module Inspec
def relative_provider
RelativeFileProvider.new(self)
end
end
end # class FileProvider
class MockProvider < FileProvider
attr_reader :files
@ -89,7 +89,7 @@ module Inspec
File.binread(file)
end
end
end # class DirProvider
class ZipProvider < FileProvider
attr_reader :files
@ -146,7 +146,7 @@ module Inspec
end
res
end
end
end # class ZipProvider
class TarProvider < FileProvider
attr_reader :files
@ -154,42 +154,48 @@ module Inspec
def initialize(path)
@path = path
@contents = {}
@files = []
walk_tar(@path) do |tar|
@files = tar.find_all(&:file?)
# delete all entries with no name
@files = @files.find_all { |x| !x.full_name.empty? && x.full_name.squeeze("/") !~ %r{\.{2}(?:/|\z)} }
here = Pathname.new(".")
# delete all entries that have a PaxHeader
@files = @files.delete_if { |x| x.full_name.include?("PaxHeader/") }
walk_tar(@path) do |entries|
entries.each do |entry|
name = entry.full_name
# replace all items of the array simply with the relative filename of the file
@files.map! { |x| Pathname.new(x.full_name).relative_path_from(Pathname.new(".")).to_s }
# rubocop:disable Layout/MultilineOperationIndentation
# rubocop:disable Style/ParenthesesAroundCondition
next unless (entry.file? && # duh
!name.empty? && # for empty filenames?
name !~ %r{\.\.(?:/|\z)} && # .. (to avoid attacks?)
!name.include?("PaxHeader/"))
path = Pathname.new(name).relative_path_from(here).to_s
@contents[path] = begin # not ||= in a tarball, last one wins
res = entry.read
try = res.dup
try.force_encoding Encoding::UTF_8
res = try if try.valid_encoding?
res
end
end
@files = @contents.keys
end
end
def extract(destination_path = ".")
FileUtils.mkdir_p(destination_path)
walk_tar(@path) do |files|
files.each do |file|
next unless @files.include?(file.full_name)
@contents.each do |path, body|
full_path = File.join(destination_path, path)
final_path = File.join(destination_path, file.full_name)
# This removes the top level directory (and any other files) to ensure
# extracted files do not conflict.
FileUtils.remove_entry(final_path) if File.exist?(final_path)
FileUtils.mkdir_p(File.dirname(final_path))
File.open(final_path, "wb") { |f| f.write(file.read) }
end
FileUtils.mkdir_p(File.dirname(full_path))
File.open(full_path, "wb") { |f| f.write(body) }
end
end
def read(file)
@contents[file] ||= read_from_tar(file)
@contents[file]
end
private
@ -200,23 +206,7 @@ module Inspec
ensure
tar_file.close
end
def read_from_tar(file)
return nil unless @files.include?(file)
res = nil
# NB `TarReader` includes `Enumerable` beginning with Ruby 2.x
walk_tar(@path) do |tar|
tar.each do |entry|
next unless entry.file? && [file, "./#{file}"].include?(entry.full_name)
res = entry.read
break
end
end
res
end
end
end # class TarProvider
class RelativeFileProvider
BLACKLIST_FILES = [
@ -318,5 +308,5 @@ module Inspec
b = File.dirname(new_pre + "b")
get_prefix([a, b])
end
end
end # class RelativeFileProvider
end

View file

@ -158,6 +158,7 @@ module Inspec::Formatters
start_time: example.execution_result.started_at.to_datetime.rfc3339.to_s,
resource_title: example.metadata[:described_class] || example.metadata[:example_group][:description],
expectation_message: format_expectation_message(example),
waiver_data: example.metadata[:waiver_data],
}
unless (pid = example.metadata[:profile_id]).nil?

View file

@ -1,3 +1,5 @@
require "inspec/errors"
# Impact scores based off CVSS 3.0
module Inspec::Impact
IMPACT_SCORES = {

410
lib/inspec/input.rb Normal file
View file

@ -0,0 +1,410 @@
require "inspec/utils/deprecation"
# For backwards compatibility during the rename (see #3802),
# maintain the Inspec::Attribute namespace for people checking for
# Inspec::Attribute::DEFAULT_ATTRIBUTE
module Inspec
class Attribute
# This only exists to create the Inspec::Attribute::DEFAULT_ATTRIBUTE symbol with a class
class DEFAULT_ATTRIBUTE; end # rubocop: disable Naming/ClassAndModuleCamelCase
end
end
module Inspec
class Input
class Error < Inspec::Error; end
class ValidationError < Error
attr_accessor :input_name
attr_accessor :input_value
attr_accessor :input_type
end
class TypeError < Error
attr_accessor :input_type
end
class RequiredError < Error
attr_accessor :input_name
end
#===========================================================================#
# Class Input::Event
#===========================================================================#
# TODO: break this out to its own file under inspec/input?
# Information about how the input obtained its value.
# Each time it changes, an Input::Event is added to the #events array.
class Event
EVENT_PROPERTIES = [
:action, # :create, :set, :fetch
:provider, # Name of the plugin
:priority, # Priority of this plugin for resolving conflicts. 1-100, higher numbers win.
:value, # New value, if provided.
:file, # File containing the input-changing action, if known
:line, # Line in file containing the input-changing action, if known
:hit, # if action is :fetch, true if the remote source had the input
].freeze
# Value has a special handler
EVENT_PROPERTIES.reject { |p| p == :value }.each do |prop|
attr_accessor prop
end
attr_reader :value
def initialize(properties = {})
@value_has_been_set = false
properties.each do |prop_name, prop_value|
if EVENT_PROPERTIES.include? prop_name
# OK, save the property
send((prop_name.to_s + "=").to_sym, prop_value)
else
raise "Unrecognized property to Input::Event: #{prop_name}"
end
end
end
def value=(the_val)
# Even if set to nil or false, it has indeed been set; note that fact.
@value_has_been_set = true
@value = the_val
end
def value_has_been_set?
@value_has_been_set
end
def diagnostic_string
to_h.reject { |_, val| val.nil? }.to_a.map { |pair| "#{pair[0]}: '#{pair[1]}'" }.join(", ")
end
def to_h
EVENT_PROPERTIES.each_with_object({}) do |prop, hash|
hash[prop] = send(prop)
end
end
def self.probe_stack
frames = caller_locations(2, 40)
frames.reject! { |f| f.path && f.path.include?("/lib/inspec/") }
frames.first
end
end # class Event
#===========================================================================#
# Class NO_VALUE_SET
#===========================================================================#
# This special class is used to represent the value when an input has
# not been assigned a value. This allows a user to explicitly assign nil
# to an input.
class NO_VALUE_SET # rubocop: disable Naming/ClassAndModuleCamelCase
def initialize(name)
@name = name
# output warn message if we are in a exec call
if Inspec::BaseCLI.inspec_cli_command == :exec
Inspec::Log.warn(
"Input '#{@name}' does not have a value. "\
"Use --input-file to provide a value for '#{@name}' or specify a "\
"value with `attribute('#{@name}', value: 'somevalue', ...)`."
)
end
end
def method_missing(*_)
self
end
def respond_to_missing?(_, _)
true
end
def to_s
"Input '#{@name}' does not have a value. Skipping test."
end
def is_a?(klass)
if klass == Inspec::Attribute::DEFAULT_ATTRIBUTE
Inspec.deprecate(:rename_attributes_to_inputs, "Don't check for `is_a?(Inspec::Attribute::DEFAULT_ATTRIBUTE)`, check for `Inspec::Input::NO_VALUE_SET")
true # lie for backward compatibility
else
super(klass)
end
end
def kind_of?(klass)
if klass == Inspec::Attribute::DEFAULT_ATTRIBUTE
Inspec.deprecate(:rename_attributes_to_inputs, "Don't check for `kind_of?(Inspec::Attribute::DEFAULT_ATTRIBUTE)`, check for `Inspec::Input::NO_VALUE_SET")
true # lie for backward compatibility
else
super(klass)
end
end
end # class NO_VALUE_SET
#===========================================================================#
# Class Inspec::Input
#===========================================================================#
# Validation types for input values
VALID_TYPES = %w{
String
Numeric
Regexp
Array
Hash
Boolean
Any
}.freeze
# TODO: this is not used anywhere?
# If you call `input` in a control file, the input will receive this priority.
# You can override that with a :priority option.
DEFAULT_PRIORITY_FOR_DSL_ATTRIBUTES = 20
# If you somehow manage to initialize an Input outside of the DSL,
# AND you don't provide an Input::Event, this is the priority you get.
DEFAULT_PRIORITY_FOR_UNKNOWN_CALLER = 10
# If you directly call value=, this is the priority assigned.
# This is the highest priority within InSpec core; though plugins
# are free to go higher.
DEFAULT_PRIORITY_FOR_VALUE_SET = 60
attr_reader :description, :events, :identifier, :name, :required, :title, :type
def initialize(name, options = {})
@name = name
@opts = options
if @opts.key?(:default)
Inspec.deprecate(:attrs_value_replaces_default, "input name: '#{name}'")
if @opts.key?(:value)
Inspec::Log.warn "Input #{@name} created using both :default and :value options - ignoring :default"
@opts.delete(:default)
end
end
# Array of Input::Event objects. These compete with one another to determine
# the value of the input when value() is called, as well as providing a
# debugging record of when and how the value changed.
@events = []
events.push make_creation_event(options)
update(options)
end
# TODO: is this here just for testing?
def set_events
events.select { |e| e.action == :set }
end
def diagnostic_string
"Input #{name}, with history:\n" +
events.map(&:diagnostic_string).map { |line| " #{line}" }.join("\n")
end
#--------------------------------------------------------------------------#
# Managing Value
#--------------------------------------------------------------------------#
def update(options)
_update_set_metadata(options)
normalize_type_restriction!
# Values are set by passing events in; but we can also infer an event.
if options.key?(:value) || options.key?(:default)
if options.key?(:event)
if options.key?(:value) || options.key?(:default)
Inspec::Log.warn "Do not provide both an Event and a value as an option to attribute('#{name}') - using value from event"
end
else
self.class.infer_event(options) # Sets options[:event]
end
end
events << options[:event] if options.key? :event
enforce_type_restriction!
end
# We can determine a value:
# 1. By event.value (preferred)
# 2. By options[:value]
# 3. By options[:default] (deprecated)
def self.infer_event(options)
# Don't rely on this working; you really should be passing a proper Input::Event
# with the context information you have.
location = Input::Event.probe_stack
event = Input::Event.new(
action: :set,
provider: options[:provider] || :unknown,
priority: options[:priority] || Inspec::Input::DEFAULT_PRIORITY_FOR_UNKNOWN_CALLER,
file: location.path,
line: location.lineno
)
if options.key?(:default)
Inspec.deprecate(:attrs_value_replaces_default, "attribute name: '#{name}'")
if options.key?(:value)
Inspec::Log.warn "Input #{@name} created using both :default and :value options - ignoring :default"
options.delete(:default)
else
options[:value] = options.delete(:default)
end
end
event.value = options[:value] if options.key?(:value)
options[:event] = event
end
private
def _update_set_metadata(options)
# Basic metadata
@title = options[:title] if options.key?(:title)
@description = options[:description] if options.key?(:description)
@required = options[:required] if options.key?(:required)
@identifier = options[:identifier] if options.key?(:identifier) # TODO: determine if this is ever used
@type = options[:type] if options.key?(:type)
end
def make_creation_event(options)
loc = options[:location] || Event.probe_stack
Input::Event.new(
action: :create,
provider: options[:provider],
file: loc.path,
line: loc.lineno
)
end
# Determine the current winning value, but don't validate it
def current_value
# Examine the events to determine highest-priority value. Tie-break
# by using the last one set.
events_that_set_a_value = events.select(&:value_has_been_set?)
winning_priority = events_that_set_a_value.map(&:priority).max
winning_events = events_that_set_a_value.select { |e| e.priority == winning_priority }
winning_event = winning_events.last # Last for tie-break
if winning_event.nil?
# No value has been set - return special no value object
NO_VALUE_SET.new(name)
else
winning_event.value # May still be nil
end
end
public
def value=(new_value, priority = DEFAULT_PRIORITY_FOR_VALUE_SET)
# Inject a new Event with the new value.
location = Event.probe_stack
events << Event.new(
action: :set,
provider: :value_setter,
priority: priority,
value: new_value,
file: location.path,
line: location.lineno
)
enforce_type_restriction!
end
def value
enforce_required_validation!
current_value
end
def has_value?
!current_value.is_a? NO_VALUE_SET
end
#--------------------------------------------------------------------------#
# Value Type Coercion
#--------------------------------------------------------------------------#
def to_s
"Input #{name} with #{current_value}"
end
#--------------------------------------------------------------------------#
# Validation
#--------------------------------------------------------------------------#
private
def enforce_required_validation!
return unless required
# skip if we are not doing an exec call (archive/vendor/check)
return unless Inspec::BaseCLI.inspec_cli_command == :exec
proposed_value = current_value
if proposed_value.nil? || proposed_value.is_a?(NO_VALUE_SET)
error = Inspec::Input::RequiredError.new
error.input_name = name
raise error, "Input '#{error.input_name}' is required and does not have a value."
end
end
def enforce_type_restriction! # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
return unless type
return unless has_value?
type_req = type
return if type_req == "Any"
proposed_value = current_value
invalid_type = false
if type_req == "Regexp"
invalid_type = true unless valid_regexp?(proposed_value)
elsif type_req == "Numeric"
invalid_type = true unless valid_numeric?(proposed_value)
elsif type_req == "Boolean"
invalid_type = true unless [true, false].include?(proposed_value)
elsif proposed_value.is_a?(Module.const_get(type_req)) == false
# TODO: why is this case here?
invalid_type = true
end
if invalid_type == true
error = Inspec::Input::ValidationError.new
error.input_name = @name
error.input_value = proposed_value
error.input_type = type_req
raise error, "Input '#{error.input_name}' with value '#{error.input_value}' does not validate to type '#{error.input_type}'."
end
end
def normalize_type_restriction!
return unless type
type_req = type.capitalize
abbreviations = {
"Num" => "Numeric",
"Regex" => "Regexp",
}
type_req = abbreviations[type_req] if abbreviations.key?(type_req)
unless VALID_TYPES.include?(type_req)
error = Inspec::Input::TypeError.new
error.input_type = type_req
raise error, "Type '#{error.input_type}' is not a valid input type."
end
@type = type_req
end
def valid_numeric?(value)
Float(value)
true
rescue
false
end
def valid_regexp?(value)
# check for invalid regex syntax
Regexp.new(value)
true
rescue
false
end
end
end

View file

@ -1,6 +1,6 @@
require "forwardable"
require "singleton"
require "inspec/objects/input"
require "inspec/input"
require "inspec/secrets"
require "inspec/exceptions"
require "inspec/plugin/v2"
@ -13,6 +13,15 @@ module Inspec
include Singleton
extend Forwardable
class Error < Inspec::Error; end
class ProfileLookupError < Error
attr_accessor :profile_name
end
class InputLookupError < Error
attr_accessor :profile_name
attr_accessor :input_name
end
attr_reader :inputs_by_profile, :profile_aliases, :plugins
def_delegator :inputs_by_profile, :each
def_delegator :inputs_by_profile, :[]
@ -64,6 +73,9 @@ module Inspec
#-------------------------------------------------------------#
def find_or_register_input(input_name, profile_name, options = {})
input_name = input_name.to_s
profile_name = profile_name.to_s
if profile_alias?(profile_name) && !profile_aliases[profile_name].nil?
alias_name = profile_name
profile_name = profile_aliases[profile_name]
@ -132,10 +144,37 @@ module Inspec
bind_inputs_from_metadata(profile_name, sources[:profile_metadata])
bind_inputs_from_input_files(profile_name, sources[:cli_input_files])
bind_inputs_from_runner_api(profile_name, sources[:runner_api])
bind_inputs_from_cli_args(profile_name, sources[:cli_input_arg])
end
private
def bind_inputs_from_cli_args(profile_name, input_list)
# TODO: move this into a core plugin
return if input_list.nil?
return if input_list.empty?
# These arrive as an array of "name=value" strings
# If the user used a comma, we'll see unfortunately see it as "name=value," pairs
input_list.each do |pair|
unless pair.include?("=")
if pair.end_with?(".yaml")
raise ArgumentError, "ERROR: --input is used for individual input values, as --input name=value. Use --input-file to load a YAML file."
else
raise ArgumentError, "ERROR: An '=' is required when using --input. Usage: --input input_name1=input_value1 input2=value2"
end
end
input_name, input_value = pair.split("=")
evt = Inspec::Input::Event.new(
value: input_value.chomp(","), # Trim trailing comma if any
provider: :cli,
priority: 50
)
find_or_register_input(input_name, profile_name, event: evt)
end
end
def bind_inputs_from_runner_api(profile_name, input_hash)
# TODO: move this into a core plugin

View file

@ -88,6 +88,10 @@ module Inspec
errors.push("Version needs to be in SemVer format")
end
unless supports_runtime?
warnings.push("The current inspec version #{Inspec::VERSION} cannot satisfy profile inspec_version constraint #{params[:inspec_version]}")
end
%w{title summary maintainer copyright license}.each do |field|
next unless params[field.to_sym].nil?

View file

@ -1,5 +1,5 @@
module Inspec
autoload :Input, "inspec/objects/input"
# TODO: these should be namespaced in Objects
autoload :Tag, "inspec/objects/tag"
autoload :Control, "inspec/objects/control"
autoload :Describe, "inspec/objects/describe"
@ -10,3 +10,5 @@ module Inspec
autoload :Test, "inspec/objects/test"
autoload :Value, "inspec/objects/value"
end
require "inspec/objects/input" # already defined so you can't autoload

View file

@ -1,307 +1,14 @@
require "inspec/utils/deprecation"
# For backwards compatibility during the rename (see #3802),
# maintain the Inspec::Attribute namespace for people checking for
# Inspec::Attribute::DEFAULT_ATTRIBUTE
module Inspec
class Attribute
# This only exists to create the Inspec::Attribute::DEFAULT_ATTRIBUTE symbol with a class
class DEFAULT_ATTRIBUTE; end # rubocop: disable Naming/ClassAndModuleCamelCase
end
end
require "inspec/input"
module Inspec
class Input
#===========================================================================#
# Class Input::Event
#===========================================================================#
# Information about how the input obtained its value.
# Each time it changes, an Input::Event is added to the #events array.
class Event
EVENT_PROPERTIES = [
:action, # :create, :set, :fetch
:provider, # Name of the plugin
:priority, # Priority of this plugin for resolving conflicts. 1-100, higher numbers win.
:value, # New value, if provided.
:file, # File containing the input-changing action, if known
:line, # Line in file containing the input-changing action, if known
:hit, # if action is :fetch, true if the remote source had the input
].freeze
# Value has a special handler
EVENT_PROPERTIES.reject { |p| p == :value }.each do |prop|
attr_accessor prop
end
attr_reader :value
def initialize(properties = {})
@value_has_been_set = false
properties.each do |prop_name, prop_value|
if EVENT_PROPERTIES.include? prop_name
# OK, save the property
send((prop_name.to_s + "=").to_sym, prop_value)
else
raise "Unrecognized property to Input::Event: #{prop_name}"
end
end
end
def value=(the_val)
# Even if set to nil or false, it has indeed been set; note that fact.
@value_has_been_set = true
@value = the_val
end
def value_has_been_set?
@value_has_been_set
end
def diagnostic_string
to_h.reject { |_, val| val.nil? }.to_a.map { |pair| "#{pair[0]}: '#{pair[1]}'" }.join(", ")
end
def to_h
EVENT_PROPERTIES.each_with_object({}) do |prop, hash|
hash[prop] = send(prop)
end
end
def self.probe_stack
frames = caller_locations(2, 40)
frames.reject! { |f| f.path && f.path.include?("/lib/inspec/") }
frames.first
end
end
#===========================================================================#
# Class NO_VALUE_SET
#===========================================================================#
# This special class is used to represent the value when an input has
# not been assigned a value. This allows a user to explicitly assign nil
# to an input.
class NO_VALUE_SET # rubocop: disable Naming/ClassAndModuleCamelCase
def initialize(name)
@name = name
# output warn message if we are in a exec call
if Inspec::BaseCLI.inspec_cli_command == :exec
Inspec::Log.warn(
"Input '#{@name}' does not have a value. "\
"Use --input-file to provide a value for '#{@name}' or specify a "\
"value with `attribute('#{@name}', value: 'somevalue', ...)`."
)
end
end
def method_missing(*_)
self
end
def respond_to_missing?(_, _)
true
end
def to_s
"Input '#{@name}' does not have a value. Skipping test."
end
def is_a?(klass)
if klass == Inspec::Attribute::DEFAULT_ATTRIBUTE
Inspec.deprecate(:rename_attributes_to_inputs, "Don't check for `is_a?(Inspec::Attribute::DEFAULT_ATTRIBUTE)`, check for `Inspec::Input::NO_VALUE_SET")
true # lie for backward compatibility
else
super(klass)
end
end
def kind_of?(klass)
if klass == Inspec::Attribute::DEFAULT_ATTRIBUTE
Inspec.deprecate(:rename_attributes_to_inputs, "Don't check for `kind_of?(Inspec::Attribute::DEFAULT_ATTRIBUTE)`, check for `Inspec::Input::NO_VALUE_SET")
true # lie for backward compatibility
else
super(klass)
end
end
end
end
# NOTE: due to namespacing, this reopens and extends the existing
# Inspec::Input. This should be under Inspec::Objects but that ship
# has sailed.
class Input
#===========================================================================#
# Class Inspec::Input
#===========================================================================#
# Validation types for input values
VALID_TYPES = %w{
String
Numeric
Regexp
Array
Hash
Boolean
Any
}.freeze
# If you call `input` in a control file, the input will receive this priority.
# You can override that with a :priority option.
DEFAULT_PRIORITY_FOR_DSL_ATTRIBUTES = 20
# If you somehow manage to initialize an Input outside of the DSL,
# AND you don't provide an Input::Event, this is the priority you get.
DEFAULT_PRIORITY_FOR_UNKNOWN_CALLER = 10
# If you directly call value=, this is the priority assigned.
# This is the highest priority within InSpec core; though plugins
# are free to go higher.
DEFAULT_PRIORITY_FOR_VALUE_SET = 60
attr_reader :description, :events, :identifier, :name, :required, :title, :type
def initialize(name, options = {})
@name = name
@opts = options
if @opts.key?(:default)
Inspec.deprecate(:attrs_value_replaces_default, "input name: '#{name}'")
if @opts.key?(:value)
Inspec::Log.warn "Input #{@name} created using both :default and :value options - ignoring :default"
@opts.delete(:default)
end
end
# Array of Input::Event objects. These compete with one another to determine
# the value of the input when value() is called, as well as providing a
# debugging record of when and how the value changed.
@events = []
events.push make_creation_event(options)
update(options)
end
def set_events
events.select { |e| e.action == :set }
end
def diagnostic_string
"Input #{name}, with history:\n" +
events.map(&:diagnostic_string).map { |line| " #{line}" }.join("\n")
end
#--------------------------------------------------------------------------#
# Managing Value
#--------------------------------------------------------------------------#
def update(options)
_update_set_metadata(options)
normalize_type_restriction!
# Values are set by passing events in; but we can also infer an event.
if options.key?(:value) || options.key?(:default)
if options.key?(:event)
if options.key?(:value) || options.key?(:default)
Inspec::Log.warn "Do not provide both an Event and a value as an option to attribute('#{name}') - using value from event"
end
else
self.class.infer_event(options) # Sets options[:event]
end
end
events << options[:event] if options.key? :event
enforce_type_restriction!
end
# We can determine a value:
# 1. By event.value (preferred)
# 2. By options[:value]
# 3. By options[:default] (deprecated)
def self.infer_event(options)
# Don't rely on this working; you really should be passing a proper Input::Event
# with the context information you have.
location = Input::Event.probe_stack
event = Input::Event.new(
action: :set,
provider: options[:provider] || :unknown,
priority: options[:priority] || Inspec::Input::DEFAULT_PRIORITY_FOR_UNKNOWN_CALLER,
file: location.path,
line: location.lineno
)
if options.key?(:default)
Inspec.deprecate(:attrs_value_replaces_default, "attribute name: '#{name}'")
if options.key?(:value)
Inspec::Log.warn "Input #{@name} created using both :default and :value options - ignoring :default"
options.delete(:default)
else
options[:value] = options.delete(:default)
end
end
event.value = options[:value] if options.key?(:value)
options[:event] = event
end
private
def _update_set_metadata(options)
# Basic metadata
@title = options[:title] if options.key?(:title)
@description = options[:description] if options.key?(:description)
@required = options[:required] if options.key?(:required)
@identifier = options[:identifier] if options.key?(:identifier) # TODO: determine if this is ever used
@type = options[:type] if options.key?(:type)
end
def make_creation_event(options)
loc = options[:location] || Event.probe_stack
Input::Event.new(
action: :create,
provider: options[:provider],
file: loc.path,
line: loc.lineno
)
end
# Determine the current winning value, but don't validate it
def current_value
# Examine the events to determine highest-priority value. Tie-break
# by using the last one set.
events_that_set_a_value = events.select(&:value_has_been_set?)
winning_priority = events_that_set_a_value.map(&:priority).max
winning_events = events_that_set_a_value.select { |e| e.priority == winning_priority }
winning_event = winning_events.last # Last for tie-break
if winning_event.nil?
# No value has been set - return special no value object
NO_VALUE_SET.new(name)
else
winning_event.value # May still be nil
end
end
public
def value=(new_value, priority = DEFAULT_PRIORITY_FOR_VALUE_SET)
# Inject a new Event with the new value.
location = Event.probe_stack
events << Event.new(
action: :set,
provider: :value_setter,
priority: priority,
value: new_value,
file: location.path,
line: location.lineno
)
enforce_type_restriction!
end
def value
enforce_required_validation!
current_value
end
def has_value?
!current_value.is_a? NO_VALUE_SET
end
# NOTE: No initialize method or accessors for the reasons listed above
#--------------------------------------------------------------------------#
# Marshalling
@ -334,94 +41,5 @@ module Inspec
res.push "})"
res.join("\n")
end
#--------------------------------------------------------------------------#
# Value Type Coercion
#--------------------------------------------------------------------------#
def to_s
"Input #{name} with #{current_value}"
end
#--------------------------------------------------------------------------#
# Validation
#--------------------------------------------------------------------------#
private
def enforce_required_validation!
return unless required
# skip if we are not doing an exec call (archive/vendor/check)
return unless Inspec::BaseCLI.inspec_cli_command == :exec
proposed_value = current_value
if proposed_value.nil? || proposed_value.is_a?(NO_VALUE_SET)
error = Inspec::Input::RequiredError.new
error.input_name = name
raise error, "Input '#{error.input_name}' is required and does not have a value."
end
end
def enforce_type_restriction! # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
return unless type
return unless has_value?
type_req = type
return if type_req == "Any"
proposed_value = current_value
invalid_type = false
if type_req == "Regexp"
invalid_type = true unless valid_regexp?(proposed_value)
elsif type_req == "Numeric"
invalid_type = true unless valid_numeric?(proposed_value)
elsif type_req == "Boolean"
invalid_type = true unless [true, false].include?(proposed_value)
elsif proposed_value.is_a?(Module.const_get(type_req)) == false
# TODO: why is this case here?
invalid_type = true
end
if invalid_type == true
error = Inspec::Input::ValidationError.new
error.input_name = @name
error.input_value = proposed_value
error.input_type = type_req
raise error, "Input '#{error.input_name}' with value '#{error.input_value}' does not validate to type '#{error.input_type}'."
end
end
def normalize_type_restriction!
return unless type
type_req = type.capitalize
abbreviations = {
"Num" => "Numeric",
"Regex" => "Regexp",
}
type_req = abbreviations[type_req] if abbreviations.key?(type_req)
unless VALID_TYPES.include?(type_req)
error = Inspec::Input::TypeError.new
error.input_type = type_req
raise error, "Type '#{error.input_type}' is not a valid input type."
end
@type = type_req
end
def valid_numeric?(value)
Float(value)
true
rescue
false
end
def valid_regexp?(value)
# check for invalid regex syntax
Regexp.new(value)
true
rescue
false
end
end
end

View file

@ -15,7 +15,7 @@ module Inspec
end
def to_ruby
"tag #{key.inspect}: #{value.inspect}"
"tag #{key}: #{value.inspect}"
end
def to_s

View file

@ -31,8 +31,10 @@ module Inspec
def supports(criteria = nil)
return if criteria.nil?
Inspec::Resource.supports[@name] ||= []
Inspec::Resource.supports[@name].push(criteria)
key = @name.to_sym
Inspec::Resource.supports[key] ||= []
Inspec::Resource.supports[key].push(criteria)
end
def example(example = nil)
@ -80,18 +82,22 @@ module Inspec
def initialize(backend, name, *args)
@resource_skipped = false
@resource_failed = false
@supports = Inspec::Resource.supports[name]
@supports = Inspec::Resource.supports[name.to_sym]
@resource_exception_message = nil
# attach the backend to this instance
@__backend_runner__ = backend
@__resource_name__ = name
# check resource supports
supported = true
supported = check_supports unless @supports.nil?
supported = @supports ? check_supports : true # check_supports has side effects!
test_backend = defined?(Train::Transports::Mock::Connection) && backend.backend.class == Train::Transports::Mock::Connection
# do not return if we are supported, or for tests
return unless supported || test_backend
# raise unless we are supported or in test
unless supported || test_backend
msg = "Unsupported resource/backend combination: %s / %s. Exiting." %
[name, backend.platform.name]
raise ArgumentError, msg
end
# call the resource initializer
begin
@ -100,12 +106,10 @@ module Inspec
skip_resource(e.message)
rescue Inspec::Exceptions::ResourceFailed => e
fail_resource(e.message)
rescue NotImplementedError => e
fail_resource(e.message) unless @resource_failed
rescue NoMethodError => e
# The new platform resources have methods generated on the fly
# for inspec check to work we need to skip these train errors
raise unless test_backend && e.receiver.class == Train::Transports::Mock::Connection
skip_resource(e.message)
skip_resource(e.message) unless @resource_failed
end
end
@ -122,6 +126,7 @@ module Inspec
end
def check_supports
require "inspec/resources/platform"
status = inspec.platform.supported?(@supports)
fail_msg = "Resource `#{@__resource_name__}` is not supported on platform #{inspec.platform.name}/#{inspec.platform.release}."
fail_resource(fail_msg) unless status
@ -163,7 +168,7 @@ module Inspec
end
module Plugins
class Resource
class Resource # TODO: possibly push up to inspec/resource.rb
extend Inspec::ResourceDSL
include Inspec::ResourceBehaviors
end

View file

@ -3,21 +3,17 @@ module Inspec::Plugin::V2
:plugin_name,
:plugin_type,
:activator_name,
:activated?,
:activated,
:exception,
:activation_proc,
:implementation_class
) do
def initialize(*)
super
self[:'activated?'] = false
self[:activated] = false
end
def activated?(new_value = nil)
return self[:activated?] if new_value.nil?
self[:activated?] = new_value
end
alias activated? activated
# Load a plugin, but if an error is encountered, store it and continue
def activate
@ -26,7 +22,7 @@ module Inspec::Plugin::V2
# rubocop: disable Lint/RescueException
begin
impl_class = self[:activation_proc].call
self[:activated?] = true
self.activated = true
self[:implementation_class] = impl_class
rescue Exception => ex
self[:exception] = ex

View file

@ -2,6 +2,8 @@ require "singleton"
require "json"
require "inspec/globals"
module Inspec::Plugin; end
module Inspec::Plugin::V2
Exclusion = Struct.new(:plugin_name, :rationale)
@ -60,4 +62,35 @@ module Inspec::Plugin::V2
end
end
end
# To be a valid plugin name, the plugin must beign with either
# inspec- or train-, AND ALSO not be on the exclusion list.
# We maintain this exclusion list to avoid confusing users.
# For example, we want to have a real gem named inspec-test-fixture,
# but we don't want the users to see that.
module FilterPredicates
def train_plugin_name?(name)
valid_plugin_name?(name, :train)
end
def inspec_plugin_name?(name)
valid_plugin_name?(name, :inspec)
end
def valid_plugin_name?(name, kind = :either)
# Must have a permitted prefix.
return false unless case kind
when :inspec
name.to_s.start_with?("inspec-")
when :train
name.to_s.start_with?("train-")
when :either
name.to_s.match(/^(inspec|train)-/)
else false
end # rubocop: disable Layout/EndAlignment
# And must not be on the exclusion list.
! Inspec::Plugin::V2::PluginFilter.exclude?(name)
end
end
end

View file

@ -60,14 +60,15 @@ module Inspec::Plugin::V2
# TODO: - check plugins.json for validity before trying anything that needs to modify it.
validate_installation_opts(plugin_name, opts)
# TODO: change all of these to return installed spec/gem/thingy
# TODO: return installed thingy
if opts[:path]
install_from_path(plugin_name, opts)
elsif opts[:gem_file]
install_from_gem_file(plugin_name, opts)
gem_version = install_from_gem_file(plugin_name, opts)
opts[:version] = gem_version.to_s
else
install_from_remote_gems(plugin_name, opts)
gem_version = install_from_remote_gems(plugin_name, opts)
opts[:version] = gem_version.to_s
end
update_plugin_config_file(plugin_name, opts.merge({ action: :install }))
@ -88,9 +89,9 @@ module Inspec::Plugin::V2
# TODO: Handle installing from a local file
# TODO: Perform dependency checks to make sure the new solution is valid
install_from_remote_gems(plugin_name, opts)
gem_version = install_from_remote_gems(plugin_name, opts)
update_plugin_config_file(plugin_name, opts.merge({ action: :update }))
update_plugin_config_file(plugin_name, opts.merge({ action: :update, version: gem_version.to_s }))
end
# Uninstalls (removes) a plugin. Refers to plugin.json to determine if it
@ -335,13 +336,15 @@ module Inspec::Plugin::V2
# not obliged to during packaging.)
# So, after each install, run a scan for all gem(specs) we manage, and copy in their gemspec file
# into the exploded gem source area if absent.
loader.list_managed_gems.each do |spec|
path_inside_source = File.join(spec.gem_dir, "#{spec.name}.gemspec")
unless File.exist?(path_inside_source)
File.write(path_inside_source, spec.to_ruby)
end
end
# Locate the GemVersion for the new dependency and return it
solution.detect { |g| g.name == new_plugin_dependency.name }.version
end
#===================================================================#
@ -365,7 +368,7 @@ module Inspec::Plugin::V2
# excluding any that are path-or-core-based, excluding the gem to be removed
plugin_deps_we_still_must_satisfy = registry.plugin_statuses
plugin_deps_we_still_must_satisfy = plugin_deps_we_still_must_satisfy.select do |status|
status.installation_type == :gem && status.name != plugin_name_to_be_removed.to_sym
status.installation_type == :user_gem && status.name != plugin_name_to_be_removed.to_sym
end
plugin_deps_we_still_must_satisfy = plugin_deps_we_still_must_satisfy.map do |status|
constraint = status.version || "> 0"

View file

@ -1,5 +1,6 @@
require "inspec/log"
require "inspec/plugin/v2/config_file"
require "inspec/plugin/v2/filter"
# Add the current directory of the process to the load path
$LOAD_PATH.unshift(".") unless $LOAD_PATH.include?(".")
@ -11,9 +12,16 @@ module Inspec::Plugin::V2
class Loader
attr_reader :conf_file, :registry, :options
# For {inspec|train}_plugin_name?
include Inspec::Plugin::V2::FilterPredicates
extend Inspec::Plugin::V2::FilterPredicates
def initialize(options = {})
@options = options
@registry = Inspec::Plugin::V2::Registry.instance
# User plugins are those installed by the user via `inspec plugin install`
# and are installed under ~/.inspec/gems
unless options[:omit_user_plugins]
@conf_file = Inspec::Plugin::V2::ConfigFile.new
read_conf_file_into_registry
@ -27,9 +35,8 @@ module Inspec::Plugin::V2
# and may be safely loaded
detect_core_plugins unless options[:omit_core_plugins]
# Train plugins aren't InSpec plugins (they don't use our API)
# but InSpec CLI manages them. So, we have to wrap them a bit.
accommodate_train_plugins
# Identify plugins that inspec is co-installed with
detect_system_plugins unless options[:omit_sys_plugins]
end
def load_all
@ -46,7 +53,7 @@ module Inspec::Plugin::V2
begin
# We could use require, but under testing, we need to repeatedly reload the same
# plugin. However, gems only work with require (rubygems dooes not overload `load`)
if plugin_details.installation_type == :gem
if plugin_details.installation_type == :user_gem
activate_managed_gems_for_plugin(plugin_name)
require plugin_details.entry_point
else
@ -116,7 +123,9 @@ module Inspec::Plugin::V2
require "rbconfig"
ruby_abi_version = RbConfig::CONFIG["ruby_version"]
# TODO: why are we installing under the api directory for plugins?
File.join(Inspec.config_dir, "gems", ruby_abi_version)
base_dir = Inspec.config_dir
base_dir = File.realpath base_dir if File.exist? base_dir
File.join(base_dir, "gems", ruby_abi_version)
end
# Lists all gems found in the plugin_gem_path.
@ -130,10 +139,11 @@ module Inspec::Plugin::V2
end
# Lists all plugin gems found in the plugin_gem_path.
# This is simply all gems that begin with train- or inspec-.
# This is simply all gems that begin with train- or inspec-
# and are not on the exclusion list.
# @return [Array[Gem::Specification]] Specs of all gems found.
def self.list_installed_plugin_gems
list_managed_gems.select { |spec| spec.name.match(/^(inspec|train)-/) }
list_managed_gems.select { |spec| valid_plugin_name?(spec.name) }
end
def list_installed_plugin_gems
@ -193,7 +203,7 @@ module Inspec::Plugin::V2
status = registry[plugin_name]
status.api_generation = 0
act = Activator.new
act.activated?(true)
act.activated = true
act.plugin_type = :cli_command
act.plugin_name = plugin_name
act.activator_name = :default
@ -234,34 +244,70 @@ module Inspec::Plugin::V2
end
end
def accommodate_train_plugins
registry.plugin_names.map(&:to_s).grep(/^train-/).each do |train_plugin_name|
status = registry[train_plugin_name.to_sym]
status.api_generation = :'train-1'
if status.installation_type == :gem
# Activate the gem. This allows train to 'require' the gem later.
activate_managed_gems_for_plugin(train_plugin_name)
end
end
end
def read_conf_file_into_registry
conf_file.each do |plugin_entry|
status = Inspec::Plugin::V2::Status.new
status.name = plugin_entry[:name]
status.loaded = false
status.installation_type = (plugin_entry[:installation_type] || :gem)
status.installation_type = (plugin_entry[:installation_type] || :user_gem)
case status.installation_type
when :gem
when :user_gem
status.entry_point = status.name.to_s
status.version = plugin_entry[:version]
when :path
status.entry_point = plugin_entry[:installation_path]
end
# Train plugins are not true InSpec plugins; we need to decorate them a
# bit more to integrate them.
fixup_train_plugin_status(status) if train_plugin_name?(plugin_entry[:name])
registry[status.name] = status
end
end
def fixup_train_plugin_status(status)
status.api_generation = :'train-1'
if status.installation_type == :user_gem
# Activate the gem. This allows train to 'require' the gem later.
activate_managed_gems_for_plugin(status.entry_point)
end
end
def detect_system_plugins
# Find the gemspec for inspec
inspec_gemspec = Gem::Specification.find_by_name("inspec", "=#{Inspec::VERSION}")
# Make a RequestSet that represents the dependencies of inspec
inspec_deps_request_set = Gem::RequestSet.new(*inspec_gemspec.dependencies)
inspec_deps_request_set.remote = false
# Resolve the request against the installed gem universe
gem_resolver = Gem::Resolver::CurrentSet.new
runtime_solution = inspec_deps_request_set.resolve(gem_resolver)
inspec_gemspec.dependencies.each do |inspec_dep|
next unless inspec_plugin_name?(inspec_dep.name) || train_plugin_name?(inspec_dep.name)
plugin_spec = runtime_solution.detect { |s| s.name == inspec_dep.name }.spec
status = Inspec::Plugin::V2::Status.new
status.name = inspec_dep.name
status.entry_point = inspec_dep.name # gem-based, just 'require' the name
status.version = plugin_spec.version.to_s
status.loaded = false
status.installation_type = :system_gem
if train_plugin_name?(status[:name])
# Train plugins are not true InSpec plugins; we need to decorate them a
# bit more to integrate them.
fixup_train_plugin_status(status)
else
status.api_generation = 2
end
registry[status.name.to_sym] = status
end
end
end
end

View file

@ -68,7 +68,7 @@ module Inspec
end
def self.for_target(target, opts = {})
opts[:vendor_cache] = opts[:vendor_cache] || Cache.new
opts[:vendor_cache] ||= Cache.new
fetcher = resolve_target(target, opts[:vendor_cache])
for_fetcher(fetcher, opts)
end
@ -116,9 +116,19 @@ module Inspec
# we can create any inputs that were provided by various mechanisms.
options[:runner_conf] ||= Inspec::Config.cached
# Catch legacy CLI input option usage
if options[:runner_conf].key?(:attrs)
Inspec.deprecate(:rename_attributes_to_inputs, "Use --input-file on the command line instead of --attrs.")
options[:runner_conf][:input_file] = options[:runner_conf].delete(:attrs)
elsif options[:runner_conf].key?(:input_files)
# The kitchen-inspec docs say to use plural. Our CLI and internal expectations are singular.
options[:runner_conf][:input_file] = options[:runner_conf].delete(:input_files)
end
# Catch legacy kitchen-inspec input usage
if options[:runner_conf].key?(:attributes)
Inspec.deprecate(:rename_attributes_to_inputs, "Use :inputs in your kitchen.yml verifier config instead of :attributes.")
options[:runner_conf][:inputs] = options[:runner_conf].delete(:attributes)
end
Inspec::InputRegistry.bind_profile_inputs(
@ -127,8 +137,8 @@ module Inspec
# Remaining args are possible sources of inputs
cli_input_files: options[:runner_conf][:input_file], # From CLI --input-file
profile_metadata: metadata,
# TODO: deprecation checks here
runner_api: options[:runner_conf][:attributes] # This is the route the audit_cookbook and kitchen-inspec take
runner_api: options[:runner_conf][:inputs], # This is the route the audit_cookbook and kitchen-inspec take
cli_input_arg: options[:runner_conf][:input] # The --input name=value CLI option
)
@runner_context =

View file

@ -5,7 +5,7 @@ require "inspec/library_eval_context"
require "inspec/control_eval_context"
require "inspec/require_loader"
require "securerandom"
require "inspec/objects/input"
require "inspec/input_registry"
module Inspec
class ProfileContext

View file

@ -1,6 +1,7 @@
require "json"
module Inspec::Reporters
# rubocop:disable Layout/AlignHash, Style/BlockDelimiters
class Json < Base
def render
output(report.to_json, false)
@ -20,112 +21,93 @@ module Inspec::Reporters
private
def platform
platform = {
name: run_data[:platform][:name],
release: run_data[:platform][:release],
}
platform[:target_id] = @config["target_id"] if @config["target_id"]
platform
{
name: run_data[:platform][:name],
release: run_data[:platform][:release],
target_id: @config["target_id"],
}.reject { |_k, v| v.nil? }
end
def profile_results(control)
results = []
return results if control[:results].nil?
control[:results].each do |r|
result = {
status: r[:status],
code_desc: r[:code_desc],
run_time: r[:run_time],
start_time: r[:start_time],
}
result[:resource] = r[:resource] if r[:resource]
result[:skip_message] = r[:skip_message] if r[:skip_message]
result[:message] = r[:message] if r[:message]
result[:exception] = r[:exception] if r[:exception]
result[:backtrace] = r[:backtrace] if r[:backtrace]
results << result
end
results
end
def profile_controls(profile)
controls = []
return controls if profile[:controls].nil?
profile[:controls].each do |c|
control = {
id: c[:id],
title: c[:title],
desc: c.dig(:descriptions, :default),
descriptions: convert_descriptions(c[:descriptions]),
impact: c[:impact],
refs: c[:refs],
tags: c[:tags],
code: c[:code],
source_location: {
line: c[:source_location][:line],
ref: c[:source_location][:ref],
},
results: profile_results(c),
}
controls << control
end
controls
end
def profile_groups(profile)
groups = []
return groups if profile[:groups].nil?
profile[:groups].each do |g|
group = {
id: g[:id],
controls: g[:controls],
}
group[:title] = g[:title] if g[:title]
groups << group
end
groups
(control[:results] || []).map { |r|
{
status: r[:status],
code_desc: r[:code_desc],
run_time: r[:run_time],
start_time: r[:start_time],
resource: r[:resource],
skip_message: r[:skip_message],
message: r[:message],
exception: r[:exception],
backtrace: r[:backtrace],
waiver_data: r[:waiver_data],
}.reject { |_k, v| v.nil? }
}
end
def profiles
profiles = []
run_data[:profiles].each do |p|
profile = {
name: p[:name],
version: p[:version],
sha256: p[:sha256],
title: p[:title],
maintainer: p[:maintainer],
summary: p[:summary],
license: p[:license],
copyright: p[:copyright],
run_data[:profiles].map { |p|
{
name: p[:name],
version: p[:version],
sha256: p[:sha256],
title: p[:title],
maintainer: p[:maintainer],
summary: p[:summary],
license: p[:license],
copyright: p[:copyright],
copyright_email: p[:copyright_email],
supports: p[:supports],
attributes: (p[:inputs] ? p[:inputs] : p[:attributes]), # TODO: rename exposed field to inputs, see #3802
parent_profile: p[:parent_profile],
depends: p[:depends],
groups: profile_groups(p),
controls: profile_controls(p),
status: p[:status],
skip_message: p[:skip_message],
supports: p[:supports],
# TODO: rename exposed field to inputs, see #3802:
attributes: (p[:inputs] || p[:attributes]),
parent_profile: p[:parent_profile],
depends: p[:depends],
groups: profile_groups(p),
controls: profile_controls(p),
status: p[:status],
skip_message: p[:skip_message],
waiver_data: p[:waiver_data],
}.reject { |_k, v| v.nil? }
}
end
def profile_groups(profile)
(profile[:groups] || []).map { |g|
{
id: g[:id],
controls: g[:controls],
title: g[:title],
}.reject { |_k, v| v.nil? }
}
end
def profile_controls(profile)
(profile[:controls] || []).map { |c|
{
id: c[:id],
title: c[:title],
desc: c.dig(:descriptions, :default),
descriptions: convert_descriptions(c[:descriptions]),
impact: c[:impact],
refs: c[:refs],
tags: c[:tags],
code: c[:code],
source_location: {
line: c[:source_location][:line],
ref: c[:source_location][:ref],
},
results: profile_results(c),
}
profiles << profile.reject { |_k, v| v.nil? }
end
profiles
}
end
def convert_descriptions(data)
return [] if data.nil?
results = []
data.each do |label, text|
results.push({ label: label.to_s, data: text })
end
results
(data || []).map { |label, text|
{
label: label.to_s,
data: text,
}
}
end
end
end

View file

@ -74,6 +74,7 @@ module Inspec
# @return [Resource] base class for creating a new resource
def self.resource(version)
validate_resource_dsl_version!(version)
require "inspec/plugin/v1/plugin_types/resource"
Inspec::Plugins::Resource
end

View file

@ -1,11 +1,18 @@
##
# Now that resources are lazily loaded, this file is ONLY here for one
# reason at this point, to load all the resources in order to populate
# the registry for `inspec shell`'s `help commands`. There has to be a
# cheaper way to do this, but this will do for now.
#
# NOTE: I intentionally didn't convert this to a loop over a simple
# glob so this remains a sort of manifest for our resources.
require "inspec/resource"
# Detect if we are running the stripped-down inspec-core
# This relies on AWS being stripped from the inspec-core gem
inspec_core_only = ENV["NO_AWS"] || !File.exist?(File.join(File.dirname(__FILE__), "..", "resource_support", "aws.rb"))
require "rspec/matchers"
# Do not attempt to load cloud resources if we are in inspec-core mode
unless inspec_core_only
require "resource_support/aws"
@ -84,6 +91,7 @@ require "inspec/resources/passwd"
require "inspec/resources/pip"
require "inspec/resources/platform"
require "inspec/resources/port"
require "inspec/resources/postfix_conf"
require "inspec/resources/postgres"
require "inspec/resources/postgres_conf"
require "inspec/resources/postgres_hba_conf"

View file

@ -48,6 +48,10 @@ module Inspec::Resources
filter.install_filter_methods_on_resource(self, :params)
def to_s
"AIDE Config"
end
private
def read_content

View file

@ -71,9 +71,9 @@ module Inspec::Resources
read_debs.select { |repo| repo[:url] == @deb_url && repo[:type] == "deb" }
end
# TODO: remove this. just see if it is valid w/ URI.parse
HTTP_URL_RE = /\A#{URI::DEFAULT_PARSER.make_regexp(%w{http https})}\z/.freeze
# read
def read_debs
return @repo_cache if defined?(@repo_cache)
@ -81,32 +81,32 @@ module Inspec::Resources
cmd = inspec.command("find /etc/apt/ -name \*.list -exec sh -c 'cat {} || echo -n' \\;")
# @see https://help.ubuntu.com/community/Repositories/CommandLine#Explanation_of_the_Repository_Format
@repo_cache = cmd.stdout.chomp.split("\n").each_with_object([]) do |raw_line, lines|
active = true
@repo_cache = cmd.stdout.lines.map do |raw_line|
# detect if the repo is commented out
line = raw_line.gsub(/^(#\s*)*/, "")
active = false if raw_line != line
active = raw_line == line
# eg.: deb http://archive.ubuntu.com/ubuntu/ wily main restricted
# or : deb [trusted=yes] http://archive.ubuntu.com/ubuntu/ wily main restricted
parse_repo = /^\s*(\S+)\s+(?:\[\S+\])?\s*"?([^ "\t\r\n\f]+)"?\s+(\S+)\s+(.*)$/.match(line)
# formats:
# deb http://archive.ubuntu.com/ubuntu/ wily main restricted ...
# deb [trusted=yes] http://archive.ubuntu.com/ubuntu/ wily main restricted ...
# check if we got any result and the second param is an url
next if parse_repo.nil? || !parse_repo[2] =~ HTTP_URL_RE
words = line.split
words.delete 1 if words[1] && words[1].start_with?("[")
type, url, distro, *components = words
next if components.empty?
next unless URI::HTTP === URI.parse(url)
next unless %w{deb deb-src}.include? type
# map data
repo = {
type: parse_repo[1],
url: parse_repo[2],
distro: parse_repo[3],
components: parse_repo[4].chomp.split(" "),
{
type: type,
url: url,
distro: distro,
components: components,
active: active,
}
next unless %w{deb deb-src}.include? repo[:type]
lines.push(repo)
end
end.compact
end
# resolves ppa urls

View file

@ -1,6 +1,8 @@
# copyright: 2015, Vulcano Security GmbH
require "inspec/resource"
require "inspec/resources/platform"
require "inspec/resources/os"
module Inspec::Resources
class Cmd < Inspec.resource(1)

View file

@ -1,81 +1,83 @@
require "openssl"
require "inspec/utils/file_reader"
class DhParams < Inspec.resource(1)
name "dh_params"
supports platform: "unix"
desc '
Use the `dh_params` InSpec audit resource to test Diffie-Hellman (DH)
parameters.
'
module Inspec::Resources
class DhParams < Inspec.resource(1)
name "dh_params"
supports platform: "unix"
desc '
Use the `dh_params` InSpec audit resource to test Diffie-Hellman (DH)
parameters.
'
example <<~EXAMPLE
describe dh_params('/path/to/file.dh_pem') do
it { should be_dh_params }
it { should be_valid }
its('generator') { should eq 2 }
its('modulus') { should eq '00:91:a0:15:89:e5:bc:38:93:12:02:fc:...' }
its('prime_length') { should eq 2048 }
its('pem') { should eq '-----BEGIN DH PARAMETERS...' }
its('text') { should eq 'PKCS#3 DH Parameters: (2048 bit)...' }
example <<~EXAMPLE
describe dh_params('/path/to/file.dh_pem') do
it { should be_dh_params }
it { should be_valid }
its('generator') { should eq 2 }
its('modulus') { should eq '00:91:a0:15:89:e5:bc:38:93:12:02:fc:...' }
its('prime_length') { should eq 2048 }
its('pem') { should eq '-----BEGIN DH PARAMETERS...' }
its('text') { should eq 'PKCS#3 DH Parameters: (2048 bit)...' }
end
EXAMPLE
include FileReader
def initialize(filename)
@dh_params_path = filename
@dh_params = OpenSSL::PKey::DH.new read_file_content(@dh_params_path)
end
EXAMPLE
include FileReader
# it { should be_dh_params }
def dh_params?
!@dh_params.nil?
end
def initialize(filename)
@dh_params_path = filename
@dh_params = OpenSSL::PKey::DH.new read_file_content(@dh_params_path)
end
# its('generator') { should eq 2 }
def generator
return if @dh_params.nil?
# it { should be_dh_params }
def dh_params?
!@dh_params.nil?
end
@dh_params.g.to_i
end
# its('generator') { should eq 2 }
def generator
return if @dh_params.nil?
# its('modulus') { should eq '00:91:a0:15:89:e5:bc:38:93:12:02:fc:...' }
def modulus
return if @dh_params.nil?
@dh_params.g.to_i
end
"00:" + @dh_params.p.to_s(16).downcase.scan(/.{2}/).join(":")
end
# its('modulus') { should eq '00:91:a0:15:89:e5:bc:38:93:12:02:fc:...' }
def modulus
return if @dh_params.nil?
# its('pem') { should eq '-----BEGIN DH PARAMETERS...' }
def pem
return if @dh_params.nil?
"00:" + @dh_params.p.to_s(16).downcase.scan(/.{2}/).join(":")
end
@dh_params.to_pem
end
# its('pem') { should eq '-----BEGIN DH PARAMETERS...' }
def pem
return if @dh_params.nil?
# its('prime_length') { should be 2048 }
def prime_length
return if @dh_params.nil?
@dh_params.to_pem
end
@dh_params.p.num_bits
end
# its('prime_length') { should be 2048 }
def prime_length
return if @dh_params.nil?
# its('text') { should eq 'human-readable-text' }
def text
return if @dh_params.nil?
@dh_params.p.num_bits
end
@dh_params.to_text
end
# its('text') { should eq 'human-readable-text' }
def text
return if @dh_params.nil?
# it { should be_valid }
def valid?
return if @dh_params.nil?
@dh_params.to_text
end
@dh_params.params_ok?
end
# it { should be_valid }
def valid?
return if @dh_params.nil?
@dh_params.params_ok?
end
def to_s
"dh_params #{@dh_params_path}"
def to_s
"dh_params #{@dh_params_path}"
end
end
end

View file

@ -57,6 +57,10 @@ module Inspec::Resources
where { mount_point == "/home" }.entries[0].mount_options
end
def to_s
"File System Table File (fstab)"
end
private
def read_content

View file

@ -1,62 +1,68 @@
require "inspec/utils/parser"
require "inspec/utils/file_reader"
class EtcHosts < Inspec.resource(1)
name "etc_hosts"
supports platform: "linux"
supports platform: "bsd"
supports platform: "windows"
desc 'Use the etc_hosts InSpec audit resource to find an
ip_address and its associated hosts'
example <<~EXAMPLE
describe etc_hosts.where { ip_address == '127.0.0.1' } do
its('ip_address') { should cmp '127.0.0.1' }
its('primary_name') { should cmp 'localhost' }
its('all_host_names') { should eq [['localhost', 'localhost.localdomain', 'localhost4', 'localhost4.localdomain4']] }
module Inspec::Resources
class EtcHosts < Inspec.resource(1)
name "etc_hosts"
supports platform: "linux"
supports platform: "bsd"
supports platform: "windows"
desc 'Use the etc_hosts InSpec audit resource to find an
ip_address and its associated hosts'
example <<~EXAMPLE
describe etc_hosts.where { ip_address == '127.0.0.1' } do
its('ip_address') { should cmp '127.0.0.1' }
its('primary_name') { should cmp 'localhost' }
its('all_host_names') { should eq [['localhost', 'localhost.localdomain', 'localhost4', 'localhost4.localdomain4']] }
end
EXAMPLE
attr_reader :params
include CommentParser
include FileReader
DEFAULT_UNIX_PATH = "/etc/hosts".freeze
DEFAULT_WINDOWS_PATH = 'C:\windows\system32\drivers\etc\hosts'.freeze
def initialize(hosts_path = nil)
content = read_file_content(hosts_path || default_hosts_file_path)
@params = parse_conf(content.lines)
end
EXAMPLE
attr_reader :params
FilterTable.create
.register_column(:ip_address, field: "ip_address")
.register_column(:primary_name, field: "primary_name")
.register_column(:all_host_names, field: "all_host_names")
.install_filter_methods_on_resource(self, :params)
include CommentParser
include FileReader
def to_s
"Hosts File"
end
DEFAULT_UNIX_PATH = "/etc/hosts".freeze
DEFAULT_WINDOWS_PATH = 'C:\windows\system32\drivers\etc\hosts'.freeze
private
def initialize(hosts_path = nil)
content = read_file_content(hosts_path || default_hosts_file_path)
def default_hosts_file_path
inspec.os.windows? ? DEFAULT_WINDOWS_PATH : DEFAULT_UNIX_PATH
end
@params = parse_conf(content.lines)
end
def parse_conf(lines)
lines.reject(&:empty?).reject(&comment?).map(&parse_data).map(&format_data)
end
FilterTable.create
.register_column(:ip_address, field: "ip_address")
.register_column(:primary_name, field: "primary_name")
.register_column(:all_host_names, field: "all_host_names")
.install_filter_methods_on_resource(self, :params)
def comment?
parse_options = { comment_char: "#", standalone_comments: false }
private
->(data) { parse_comment_line(data, parse_options).first.empty? }
end
def default_hosts_file_path
inspec.os.windows? ? DEFAULT_WINDOWS_PATH : DEFAULT_UNIX_PATH
end
def parse_data
->(data) { [data.split[0], data.split[1], data.split[1..-1]] }
end
def parse_conf(lines)
lines.reject(&:empty?).reject(&comment?).map(&parse_data).map(&format_data)
end
def comment?
parse_options = { comment_char: "#", standalone_comments: false }
->(data) { parse_comment_line(data, parse_options).first.empty? }
end
def parse_data
->(data) { [data.split[0], data.split[1], data.split[1..-1]] }
end
def format_data
->(data) { %w{ip_address primary_name all_host_names}.zip(data).to_h }
def format_data
->(data) { %w{ip_address primary_name all_host_names}.zip(data).to_h }
end
end
end

View file

@ -88,6 +88,10 @@ module Inspec::Resources
firewalld_command("--zone=#{query_zone} --query-rich-rule='#{rule}'") == "yes"
end
def to_s
"Firewall Rules"
end
private
def active_zones

View file

@ -164,22 +164,40 @@ module Inspec::Resources
# OSX uses opendirectory for groups, so `/etc/group` may not be fully accurate
# This uses `dscacheutil` to get the group info instead of `etc_group`
class DarwinGroup < GroupInfo
def groups
group_info = inspec.command("dscacheutil -q group").stdout.split("\n\n")
def runmap(cmd, &blk)
hashmap(inspec.command(cmd).stdout.lines, &blk)
end
def hashmap(enum, &blk)
enum.map(&blk).to_h
end
def groups
group_by_id = runmap("dscl . -list /Groups PrimaryGroupID") { |l| name, id = l.split; [id.to_i, name] }
userss = runmap("dscl . -list /Users PrimaryGroupID") { |l| name, id = l.split; [name, id.to_i] }
membership = runmap("dscl . -list /Groups GroupMembership") { |l| key, *vs = l.split; [key, vs] }
membership.default_proc = ->(h, k) { h[k] = [] }
users_by_group = hashmap(userss.keys.group_by { |k| userss[k] }) { |k, vs| [group_by_id[k], vs] }
users_by_group.each do |name, users|
membership[name].concat users
end
group_info = inspec.command("dscacheutil -q group").stdout.split("\n\n").uniq
groups = []
regex = /^([^:]*?)\s*:\s(.*?)\s*$/
group_info.each do |data|
groups << inspec.parse_config(data, assignment_regex: regex).params
groups = group_info.map do |data|
inspec.parse_config(data, assignment_regex: regex).params
end
# Convert the `dscacheutil` groups to match `inspec.etc_group.entries`
groups.each { |g| g["gid"] = g["gid"].to_i }
groups.each do |g|
next if g["users"].nil?
g["members"] = g.delete("users")
g["members"].tr!(" ", ",")
users = g.delete("users") || ""
users = users.split
users += Array(users_by_group[g["name"]])
g["members"] = users
g["members"].sort.join ","
end
end
end

View file

@ -1,228 +1,230 @@
require "inspec/utils/simpleconfig"
require "inspec/utils/file_reader"
class GrubConfig < Inspec.resource(1)
name "grub_conf"
supports platform: "unix"
desc "Use the grub_conf InSpec audit resource to test the boot config of Linux systems that use Grub."
example <<~EXAMPLE
describe grub_conf('/etc/grub.conf', 'default') do
its('kernel') { should include '/vmlinuz-2.6.32-573.7.1.el6.x86_64' }
its('initrd') { should include '/initramfs-2.6.32-573.el6.x86_64.img=1' }
its('default') { should_not eq '1' }
its('timeout') { should eq '5' }
end
also check specific kernels
describe grub_conf('/etc/grub.conf', 'CentOS (2.6.32-573.12.1.el6.x86_64)') do
its('kernel') { should include 'audit=1' }
end
EXAMPLE
include FileReader
class UnknownGrubConfig < StandardError; end
def initialize(path = nil, kernel = nil)
config_for_platform(path)
@content = read_file(@conf_path)
@kernel = kernel || "default"
rescue UnknownGrubConfig
skip_resource "The `grub_config` resource is not supported on your OS yet."
end
def config_for_platform(path)
os = inspec.os
if os.redhat? || os[:name] == "fedora"
config_for_redhatish(path)
elsif os.debian?
@conf_path = path || "/boot/grub/grub.cfg"
@defaults_path = "/etc/default/grub"
@grubenv_path = "/boot/grub2/grubenv"
@version = "grub2"
elsif os[:name] == "amazon"
@conf_path = path || "/etc/grub.conf"
@version = "legacy"
else
raise UnknownGrubConfig
end
end
def config_for_redhatish(path)
if inspec.os[:release].to_f < 7
@conf_path = path || "/etc/grub.conf"
@version = "legacy"
else
@conf_path = path || "/boot/grub2/grub.cfg"
@defaults_path = "/etc/default/grub"
@grubenv_path = "/boot/grub2/grubenv"
@version = "grub2"
end
end
def method_missing(name)
read_params[name.to_s]
end
def to_s
"Grub Config"
end
private
######################################################################
# Grub2 This is used by all supported versions of Ubuntu and Rhel 7+ #
######################################################################
def grub2_parse_kernel_lines(content, conf)
menu_entries = extract_menu_entries(content)
if @kernel == "default"
default_menu_entry(menu_entries, conf["GRUB_DEFAULT"])
else
menu_entries.find { |entry| entry["name"] == @kernel }
end
end
def extract_menu_entries(content)
menu_entries = []
lines = content.split("\n")
lines.each_with_index do |line, index|
next unless line =~ /^menuentry\s+.*/
entry = {}
entry["insmod"] = []
# Extract name from menuentry line
capture_data = line.match(/(?:^|\s+).*menuentry\s*['|"](.*)['|"]\s*--/)
if capture_data.nil? || capture_data.captures[0].nil?
raise Inspec::Exceptions::ResourceFailed "Failed to extract menuentry name from #{line}"
module Inspec::Resources
class GrubConfig < Inspec.resource(1)
name "grub_conf"
supports platform: "unix"
desc "Use the grub_conf InSpec audit resource to test the boot config of Linux systems that use Grub."
example <<~EXAMPLE
describe grub_conf('/etc/grub.conf', 'default') do
its('kernel') { should include '/vmlinuz-2.6.32-573.7.1.el6.x86_64' }
its('initrd') { should include '/initramfs-2.6.32-573.el6.x86_64.img=1' }
its('default') { should_not eq '1' }
its('timeout') { should eq '5' }
end
entry["name"] = capture_data.captures[0]
also check specific kernels
describe grub_conf('/etc/grub.conf', 'CentOS (2.6.32-573.12.1.el6.x86_64)') do
its('kernel') { should include 'audit=1' }
end
EXAMPLE
# Begin processing from index forward until a `}` line is met
lines.drop(index + 1).each do |mline|
break if mline =~ /^\s*}\s*$/
include FileReader
case mline
when /(?:^|\s*)initrd.*/
entry["initrd"] = mline.split(" ")[1]
when /(?:^|\s*)linux.*/
entry["kernel"] = mline.split
when /(?:^|\s*)set root=.*/
entry["root"] = mline.split("=")[1].tr("'", "")
when /(?:^|\s*)insmod.*/
entry["insmod"] << mline.split(" ")[1]
class UnknownGrubConfig < StandardError; end
def initialize(path = nil, kernel = nil)
config_for_platform(path)
@content = read_file(@conf_path)
@kernel = kernel || "default"
rescue UnknownGrubConfig
skip_resource "The `grub_config` resource is not supported on your OS yet."
end
def config_for_platform(path)
os = inspec.os
if os.redhat? || os[:name] == "fedora"
config_for_redhatish(path)
elsif os.debian?
@conf_path = path || "/boot/grub/grub.cfg"
@defaults_path = "/etc/default/grub"
@grubenv_path = "/boot/grub2/grubenv"
@version = "grub2"
elsif os[:name] == "amazon"
@conf_path = path || "/etc/grub.conf"
@version = "legacy"
else
raise UnknownGrubConfig
end
end
def config_for_redhatish(path)
if inspec.os[:release].to_f < 7
@conf_path = path || "/etc/grub.conf"
@version = "legacy"
else
@conf_path = path || "/boot/grub2/grub.cfg"
@defaults_path = "/etc/default/grub"
@grubenv_path = "/boot/grub2/grubenv"
@version = "grub2"
end
end
def method_missing(name)
read_params[name.to_s]
end
def to_s
"Grub Config"
end
private
######################################################################
# Grub2 This is used by all supported versions of Ubuntu and Rhel 7+ #
######################################################################
def grub2_parse_kernel_lines(content, conf)
menu_entries = extract_menu_entries(content)
if @kernel == "default"
default_menu_entry(menu_entries, conf["GRUB_DEFAULT"])
else
menu_entries.find { |entry| entry["name"] == @kernel }
end
end
def extract_menu_entries(content)
menu_entries = []
lines = content.split("\n")
lines.each_with_index do |line, index|
next unless line =~ /^menuentry\s+.*/
entry = {}
entry["insmod"] = []
# Extract name from menuentry line
capture_data = line.match(/(?:^|\s+).*menuentry\s*['|"](.*)['|"]\s*--/)
if capture_data.nil? || capture_data.captures[0].nil?
raise Inspec::Exceptions::ResourceFailed "Failed to extract menuentry name from #{line}"
end
end
menu_entries << entry
end
entry["name"] = capture_data.captures[0]
menu_entries
end
# Begin processing from index forward until a `}` line is met
lines.drop(index + 1).each do |mline|
break if mline =~ /^\s*}\s*$/
def default_menu_entry(menu_entries, default)
# If the default entry isn't `saved` then a number is used as an index.
# By default this is `0`, which would be the first item in the list.
return menu_entries[default.to_i] unless default == "saved"
grubenv_contents = inspec.file(@grubenv_path).content
# The location of the grubenv file is not guaranteed. In the case that
# the file does not exist this will return the 0th entry. This will also
# return the 0th entry if InSpec lacks permission to read the file. Both
# of these reflect the default Grub2 behavior.
return menu_entries[0] if grubenv_contents.nil?
default_name = SimpleConfig.new(grubenv_contents).params["saved_entry"]
default_entry = menu_entries.select { |k| k["name"] == default_name }[0]
return default_entry unless default_entry.nil?
# It is possible for the saved entry to not be valid . For example, grubenv
# not being up to date. If so, the 0th entry is the default.
menu_entries[0]
end
###################################################################
# Grub1 aka legacy-grub config. Primarily used by Centos/Rhel 6.x #
###################################################################
def parse_kernel_lines(content, conf)
# Find all "title" lines and then parse them into arrays
menu_entry = 0
lines = content.split("\n")
kernel_opts = {}
lines.each_with_index do |file_line, index|
next unless file_line =~ /^title.*/
current_kernel = file_line.split(" ", 2)[1]
lines.drop(index + 1).each do |kernel_line|
if kernel_line =~ /^\s.*/
option_type = kernel_line.split(" ")[0]
line_options = kernel_line.split(" ").drop(1)
if (menu_entry == conf["default"].to_i && @kernel == "default") || current_kernel == @kernel
if option_type == "kernel"
kernel_opts["kernel"] = line_options
else
kernel_opts[option_type] = line_options[0]
end
case mline
when /(?:^|\s*)initrd.*/
entry["initrd"] = mline.split(" ")[1]
when /(?:^|\s*)linux.*/
entry["kernel"] = mline.split
when /(?:^|\s*)set root=.*/
entry["root"] = mline.split("=")[1].tr("'", "")
when /(?:^|\s*)insmod.*/
entry["insmod"] << mline.split(" ")[1]
end
else
menu_entry += 1
break
end
end
end
kernel_opts
end
def read_file(config_file)
read_file_content(config_file)
end
def read_params
return @params if defined?(@params)
content = read_file(@conf_path)
if @version == "legacy"
# parse the file
conf = SimpleConfig.new(
content,
multiple_values: true
).params
# convert single entry arrays into strings
conf.each do |key, value|
if value.size == 1
conf[key] = conf[key][0].to_s
end
end
kernel_opts = parse_kernel_lines(content, conf)
@params = conf.merge(kernel_opts)
end
if @version == "grub2"
# read defaults
defaults = read_file(@defaults_path)
conf = SimpleConfig.new(
defaults,
multiple_values: true
).params
# convert single entry arrays into strings
conf.each do |key, value|
if value.size == 1
conf[key] = conf[key][0].to_s
end
menu_entries << entry
end
kernel_opts = grub2_parse_kernel_lines(content, conf)
@params = conf.merge(kernel_opts)
menu_entries
end
def default_menu_entry(menu_entries, default)
# If the default entry isn't `saved` then a number is used as an index.
# By default this is `0`, which would be the first item in the list.
return menu_entries[default.to_i] unless default == "saved"
grubenv_contents = inspec.file(@grubenv_path).content
# The location of the grubenv file is not guaranteed. In the case that
# the file does not exist this will return the 0th entry. This will also
# return the 0th entry if InSpec lacks permission to read the file. Both
# of these reflect the default Grub2 behavior.
return menu_entries[0] if grubenv_contents.nil?
default_name = SimpleConfig.new(grubenv_contents).params["saved_entry"]
default_entry = menu_entries.select { |k| k["name"] == default_name }[0]
return default_entry unless default_entry.nil?
# It is possible for the saved entry to not be valid . For example, grubenv
# not being up to date. If so, the 0th entry is the default.
menu_entries[0]
end
###################################################################
# Grub1 aka legacy-grub config. Primarily used by Centos/Rhel 6.x #
###################################################################
def parse_kernel_lines(content, conf)
# Find all "title" lines and then parse them into arrays
menu_entry = 0
lines = content.split("\n")
kernel_opts = {}
lines.each_with_index do |file_line, index|
next unless file_line =~ /^title.*/
current_kernel = file_line.split(" ", 2)[1]
lines.drop(index + 1).each do |kernel_line|
if kernel_line =~ /^\s.*/
option_type = kernel_line.split(" ")[0]
line_options = kernel_line.split(" ").drop(1)
if (menu_entry == conf["default"].to_i && @kernel == "default") || current_kernel == @kernel
if option_type == "kernel"
kernel_opts["kernel"] = line_options
else
kernel_opts[option_type] = line_options[0]
end
end
else
menu_entry += 1
break
end
end
end
kernel_opts
end
def read_file(config_file)
read_file_content(config_file)
end
def read_params
return @params if defined?(@params)
content = read_file(@conf_path)
if @version == "legacy"
# parse the file
conf = SimpleConfig.new(
content,
multiple_values: true
).params
# convert single entry arrays into strings
conf.each do |key, value|
if value.size == 1
conf[key] = conf[key][0].to_s
end
end
kernel_opts = parse_kernel_lines(content, conf)
@params = conf.merge(kernel_opts)
end
if @version == "grub2"
# read defaults
defaults = read_file(@defaults_path)
conf = SimpleConfig.new(
defaults,
multiple_values: true
).params
# convert single entry arrays into strings
conf.each do |key, value|
if value.size == 1
conf[key] = conf[key][0].to_s
end
end
kernel_opts = grub2_parse_kernel_lines(content, conf)
@params = conf.merge(kernel_opts)
end
@params
end
@params
end
end

View file

@ -5,125 +5,127 @@ require "inspec/resources/powershell"
# check for web applications in IIS
# Note: this is only supported in windows 2012 and later
class IisAppPool < Inspec.resource(1)
name "iis_app_pool"
desc "Tests IIS application pool configuration on windows."
supports platform: "windows"
example <<~EXAMPLE
describe iis_app_pool('DefaultAppPool') do
it { should exist }
its('enable32bit') { should cmp 'True' }
its('runtime_version') { should eq 'v4.0' }
its('pipeline_mode') { should eq 'Integrated' }
module Inspec::Resources
class IisAppPool < Inspec.resource(1)
name "iis_app_pool"
desc "Tests IIS application pool configuration on windows."
supports platform: "windows"
example <<~EXAMPLE
describe iis_app_pool('DefaultAppPool') do
it { should exist }
its('enable32bit') { should cmp 'True' }
its('runtime_version') { should eq 'v4.0' }
its('pipeline_mode') { should eq 'Integrated' }
end
EXAMPLE
def initialize(pool_name)
@pool_name = pool_name
@pool_path = "IIS:\\AppPools\\#{@pool_name}"
@cache = nil
# verify that this resource is only supported on Windows
return skip_resource "The `iis_app_pool` resource is not supported on your OS." unless inspec.os.windows?
end
EXAMPLE
def initialize(pool_name)
@pool_name = pool_name
@pool_path = "IIS:\\AppPools\\#{@pool_name}"
@cache = nil
def pool_name
iis_app_pool[:pool_name]
end
# verify that this resource is only supported on Windows
return skip_resource "The `iis_app_pool` resource is not supported on your OS." unless inspec.os.windows?
end
def runtime_version
iis_app_pool[:version]
end
def pool_name
iis_app_pool[:pool_name]
end
def enable32bit
iis_app_pool[:e32b]
end
def runtime_version
iis_app_pool[:version]
end
def pipeline_mode
iis_app_pool[:mode]
end
def enable32bit
iis_app_pool[:e32b]
end
def max_processes
iis_app_pool[:processes]
end
def pipeline_mode
iis_app_pool[:mode]
end
def timeout
iis_app_pool[:timeout]
end
def max_processes
iis_app_pool[:processes]
end
def timeout_days
iis_app_pool[:timeout_days]
end
def timeout
iis_app_pool[:timeout]
end
def timeout_hours
iis_app_pool[:timeout_hours]
end
def timeout_days
iis_app_pool[:timeout_days]
end
def timeout_minutes
iis_app_pool[:timeout_minutes]
end
def timeout_hours
iis_app_pool[:timeout_hours]
end
def timeout_seconds
iis_app_pool[:timeout_seconds]
end
def timeout_minutes
iis_app_pool[:timeout_minutes]
end
def user_identity_type
iis_app_pool[:user_identity_type]
end
def timeout_seconds
iis_app_pool[:timeout_seconds]
end
def username
iis_app_pool[:username]
end
def user_identity_type
iis_app_pool[:user_identity_type]
end
def exists?
!iis_app_pool[:pool_name].empty?
end
def username
iis_app_pool[:username]
end
def to_s
"IIS App Pool '#{@pool_name}'"
end
def exists?
!iis_app_pool[:pool_name].empty?
end
private
def to_s
"IIS App Pool '#{@pool_name}'"
end
def iis_app_pool
return @cache unless @cache.nil?
private
# We use `-Compress` here to avoid a bug in PowerShell
# It does not affect validity of the output, only the representation
# See: https://github.com/inspec/inspec/pull/3842
script = <<~EOH
Import-Module WebAdministration
If (Test-Path '#{@pool_path}') {
Get-Item '#{@pool_path}' | Select-Object * | ConvertTo-Json -Compress
} Else {
Write-Host '{}'
}
EOH
cmd = inspec.powershell(script)
def iis_app_pool
return @cache unless @cache.nil?
begin
pool = JSON.parse(cmd.stdout)
rescue JSON::ParserError => _e
raise Inspec::Exceptions::ResourceFailed, "Unable to parse app pool JSON"
end
# We use `-Compress` here to avoid a bug in PowerShell
# It does not affect validity of the output, only the representation
# See: https://github.com/inspec/inspec/pull/3842
script = <<~EOH
Import-Module WebAdministration
If (Test-Path '#{@pool_path}') {
Get-Item '#{@pool_path}' | Select-Object * | ConvertTo-Json -Compress
} Else {
Write-Host '{}'
process_model = pool.fetch("processModel", {})
idle_timeout = process_model.fetch("idleTimeout", {})
# map our values to a hash table
@cache = {
pool_name: pool["name"],
version: pool["managedRuntimeVersion"],
e32b: pool["enable32BitAppOnWin64"],
mode: pool["managedPipelineMode"],
processes: process_model["maxProcesses"],
timeout: "#{idle_timeout["Hours"]}:#{idle_timeout["Minutes"]}:#{idle_timeout["Seconds"]}",
timeout_days: idle_timeout["Days"],
timeout_hours: idle_timeout["Hours"],
timeout_minutes: idle_timeout["Minutes"],
timeout_seconds: idle_timeout["Seconds"],
user_identity_type: process_model["identityType"],
username: process_model["userName"],
}
EOH
cmd = inspec.powershell(script)
begin
pool = JSON.parse(cmd.stdout)
rescue JSON::ParserError => _e
raise Inspec::Exceptions::ResourceFailed, "Unable to parse app pool JSON"
end
process_model = pool.fetch("processModel", {})
idle_timeout = process_model.fetch("idleTimeout", {})
# map our values to a hash table
@cache = {
pool_name: pool["name"],
version: pool["managedRuntimeVersion"],
e32b: pool["enable32BitAppOnWin64"],
mode: pool["managedPipelineMode"],
processes: process_model["maxProcesses"],
timeout: "#{idle_timeout["Hours"]}:#{idle_timeout["Minutes"]}:#{idle_timeout["Seconds"]}",
timeout_days: idle_timeout["Days"],
timeout_hours: idle_timeout["Hours"],
timeout_minutes: idle_timeout["Minutes"],
timeout_seconds: idle_timeout["Seconds"],
user_identity_type: process_model["identityType"],
username: process_model["userName"],
}
end
end

View file

@ -93,10 +93,17 @@ module Inspec::Resources
end
def load_raw_from_command(command)
command_output = inspec.command(command).stdout
raise Inspec::Exceptions::ResourceSkipped, "No output from command: #{command}" if command_output.nil? || command_output.empty?
result = inspec.command(command)
command_output
return result.stdout unless result.stdout.empty?
msg = if result.stderr.empty?
"No JSON output, STDERR was empty"
else
"No JSON output, STDERR:\n #{result.stderr}"
end
raise Inspec::Exceptions::ResourceFailed, msg
end
# for resources the subclass JsonConfig, this allows specification of the resource

View file

@ -53,7 +53,7 @@ module Inspec::Resources
end
def query(q) # rubocop:disable Metrics/PerceivedComplexity
escaped_query = q.gsub(/\\/, '\\\\').gsub(/"/, '\\"').gsub(/\$/, '\\$')
escaped_query = q.gsub(/\\/, '\\\\').gsub(/"/, '""').gsub(/\$/, '\\$')
# surpress 'x rows affected' in SQLCMD with 'set nocount on;'
cmd_string = "sqlcmd -Q \"set nocount on; #{escaped_query}\" -W -w 1024 -s ','"
cmd_string += " -U '#{@user}' -P '#{@password}'" unless @user.nil? || @password.nil?

View file

@ -1,3 +1,5 @@
require "inspec/resource"
module Inspec::Resources
class PlatformResource < Inspec.resource(1)
name "platform"
@ -67,19 +69,22 @@ module Inspec::Resources
return true if supports.nil? || supports.empty?
status = true
supports.each do |s|
s.each do |k, v|
if %i{os_family os-family platform_family platform-family}.include?(k)
status = in_family?(v)
elsif %i{os platform}.include?(k)
status = platform?(v)
elsif %i{os_name os-name platform_name platform-name}.include?(k)
status = name == v
elsif k == :release
status = check_release(v)
else
status = false
end
supports.each do |support|
support.each do |k, v|
status =
case k
when :os_family, :"os-family", :platform_family, :"platform-family" then
in_family?(v)
when :os, :platform then
platform?(v)
when :os_name, :"os-name", :platform_name, :"platform-name" then
name == v
when :release then
check_release(v)
else
false
end
break if status == false
end
return true if status == true

View file

@ -0,0 +1,35 @@
require "inspec/resources/ini"
require "inspec/utils/simpleconfig"
module Inspec::Resources
class PostfixConf < IniConfig
name "postfix_conf"
supports platform: "linux"
desc "Use the postfix_conf Inspec audit resource to test the configuration of the Postfix Mail Transfer Agent"
# Allow user to specify a custom configuration path, use default Postfix configuration path if no custom path is provided
def initialize(*opts)
@params = {}
if opts.length == 1
@raw_content = load_raw_content(opts[0])
else
@raw_content = load_raw_content("/etc/postfix/main.cf")
end
@params = parse(@raw_content)
end
def parse(content)
SimpleConfig.new(content).params
end
def to_s
"Postfix Mail Transfer Agent"
end
private
def resource_base_name
"Postfix Config"
end
end
end

View file

@ -47,6 +47,10 @@ module Inspec::Resources
@sids.key?(@name)
end
def to_s
"Security Identifier"
end
private
def fetch_sids

View file

@ -6,92 +6,94 @@ require "uri"
require "parallel"
# Custom resource based on the InSpec resource DSL
class SSL < Inspec.resource(1)
name "ssl"
supports platform: "unix"
supports platform: "windows"
module Inspec::Resources
class SSL < Inspec.resource(1)
name "ssl"
supports platform: "unix"
supports platform: "windows"
desc "
SSL test resource
"
desc "
SSL test resource
"
example <<~EXAMPLE
describe ssl(port: 443) do
it { should be_enabled }
end
# protocols: ssl2, ssl3, tls1.0, tls1.1, tls1.2
describe ssl(port: 443).protocols('ssl2') do
it { should_not be_enabled }
end
# any ciphers, filter by name or regex
describe ssl(port: 443).ciphers(/rc4/i) do
it { should_not be_enabled }
end
EXAMPLE
VERSIONS = [
"ssl2",
"ssl3",
"tls1.0",
"tls1.1",
"tls1.2",
].freeze
attr_reader :host, :port, :timeout, :retries
def initialize(opts = {})
@host = opts[:host]
if @host.nil?
# Transports like SSH and WinRM will provide a hostname
if inspec.backend.respond_to?("hostname")
@host = inspec.backend.hostname
elsif inspec.backend.class.to_s == "Train::Transports::Local::Connection"
@host = "localhost"
example <<~EXAMPLE
describe ssl(port: 443) do
it { should be_enabled }
end
end
@port = opts[:port] || 443
@timeout = opts[:timeout]
@retries = opts[:retries]
end
filter = FilterTable.create
filter.register_custom_matcher(:enabled?) do |x|
raise "Cannot determine host for SSL test. Please specify it or use a different target." if x.resource.host.nil?
x.handshake.values.any? { |i| i["success"] }
end
filter.register_column(:ciphers, field: "cipher")
.register_column(:protocols, field: "protocol")
.register_custom_property(:handshake) do |x|
groups = x.entries.group_by(&:protocol)
res = Parallel.map(groups, in_threads: 8) do |proto, e|
[proto, SSLShake.hello(x.resource.host, port: x.resource.port,
protocol: proto, ciphers: e.map(&:cipher),
timeout: x.resource.timeout, retries: x.resource.retries, servername: x.resource.host)]
# protocols: ssl2, ssl3, tls1.0, tls1.1, tls1.2
describe ssl(port: 443).protocols('ssl2') do
it { should_not be_enabled }
end
Hash[res]
end
.install_filter_methods_on_resource(self, :scan_config)
def to_s
"SSL/TLS on #{@host}:#{@port}"
end
private
def scan_config
[
{ "protocol" => "ssl2", "ciphers" => SSLShake::SSLv2::CIPHERS.keys },
{ "protocol" => "ssl3", "ciphers" => SSLShake::TLS::SSL3_CIPHERS.keys },
{ "protocol" => "tls1.0", "ciphers" => SSLShake::TLS::TLS10_CIPHERS.keys },
{ "protocol" => "tls1.1", "ciphers" => SSLShake::TLS::TLS10_CIPHERS.keys },
{ "protocol" => "tls1.2", "ciphers" => SSLShake::TLS::TLS_CIPHERS.keys },
].map do |line|
line["ciphers"].map do |cipher|
{ "protocol" => line["protocol"], "cipher" => cipher }
# any ciphers, filter by name or regex
describe ssl(port: 443).ciphers(/rc4/i) do
it { should_not be_enabled }
end
end.flatten
EXAMPLE
VERSIONS = [
"ssl2",
"ssl3",
"tls1.0",
"tls1.1",
"tls1.2",
].freeze
attr_reader :host, :port, :timeout, :retries
def initialize(opts = {})
@host = opts[:host]
if @host.nil?
# Transports like SSH and WinRM will provide a hostname
if inspec.backend.respond_to?("hostname")
@host = inspec.backend.hostname
elsif inspec.backend.class.to_s == "Train::Transports::Local::Connection"
@host = "localhost"
end
end
@port = opts[:port] || 443
@timeout = opts[:timeout]
@retries = opts[:retries]
end
filter = FilterTable.create
filter.register_custom_matcher(:enabled?) do |x|
raise "Cannot determine host for SSL test. Please specify it or use a different target." if x.resource.host.nil?
x.handshake.values.any? { |i| i["success"] }
end
filter.register_column(:ciphers, field: "cipher")
.register_column(:protocols, field: "protocol")
.register_custom_property(:handshake) do |x|
groups = x.entries.group_by(&:protocol)
res = Parallel.map(groups, in_threads: 8) do |proto, e|
[proto, SSLShake.hello(x.resource.host, port: x.resource.port,
protocol: proto, ciphers: e.map(&:cipher),
timeout: x.resource.timeout, retries: x.resource.retries, servername: x.resource.host)]
end
Hash[res]
end
.install_filter_methods_on_resource(self, :scan_config)
def to_s
"SSL/TLS on #{@host}:#{@port}"
end
private
def scan_config
[
{ "protocol" => "ssl2", "ciphers" => SSLShake::SSLv2::CIPHERS.keys },
{ "protocol" => "ssl3", "ciphers" => SSLShake::TLS::SSL3_CIPHERS.keys },
{ "protocol" => "tls1.0", "ciphers" => SSLShake::TLS::TLS10_CIPHERS.keys },
{ "protocol" => "tls1.1", "ciphers" => SSLShake::TLS::TLS10_CIPHERS.keys },
{ "protocol" => "tls1.2", "ciphers" => SSLShake::TLS::TLS_CIPHERS.keys },
].map do |line|
line["ciphers"].map do |cipher|
{ "protocol" => line["protocol"], "cipher" => cipher }
end
end.flatten
end
end
end

View file

@ -13,20 +13,77 @@ module Inspec::Resources
describe sys_info do
its('hostname') { should eq 'example.com' }
end
describe sys_info do
its('fqdn') { should eq 'user.example.com' }
end
EXAMPLE
%w{ domain fqdn ip_address short }.each do |opt|
define_method(opt.to_sym) do
hostname(opt)
end
end
# returns the hostname of the local system
def hostname
def hostname(opt = nil)
os = inspec.os
if os.linux? || os.darwin?
inspec.command("hostname").stdout.chomp
if os.linux?
linux_hostname(opt)
elsif os.darwin?
mac_hostname(opt)
elsif os.windows?
inspec.powershell("$env:computername").stdout.chomp
if !opt.nil?
skip_resource "The `sys_info.hostname` resource is not supported with that option on your OS."
else
inspec.powershell("$env:computername").stdout.chomp
end
else
skip_resource "The `sys_info.hostname` resource is not supported on your OS yet."
end
end
def linux_hostname(opt = nil)
if !opt.nil?
opt = case opt
when "f", "long", "fqdn", "full"
" -f"
when "d", "domain"
" -d"
when "i", "ip_address"
" -I"
when "s", "short"
" -s"
else
"ERROR"
end
end
if opt == "ERROR"
skip_resource "The `sys_info.hostname` resource is not supported with that option on your OS."
else
inspec.command("hostname#{opt}").stdout.chomp
end
end
def mac_hostname(opt = nil)
if !opt.nil?
opt = case opt
when "f", "long", "fqdn", "full"
" -f"
when "s", "short"
" -s"
else
"ERROR"
end
end
if opt == "ERROR"
skip_resource "The `sys_info.hostname` resource is not supported with that option on your OS."
else
inspec.command("hostname#{opt}").stdout.chomp
end
end
# returns the Manufacturer of the local system
def manufacturer
os = inspec.os
@ -54,5 +111,9 @@ module Inspec::Resources
skip_resource "The `sys_info.model` resource is not supported on your OS yet."
end
end
def to_s
"System Information"
end
end
end

View file

@ -0,0 +1 @@
require "inspec/resources/users"

View file

@ -1,10 +1,12 @@
# copyright: 2015, Dominik Richter
require "method_source"
require "date"
require "inspec/describe"
require "inspec/expect"
require "inspec/resource"
require "inspec/resources/os"
require "inspec/input_registry"
module Inspec
class Rule
@ -28,6 +30,7 @@ module Inspec
@resource_dsl
end
attr_reader :__waiver_data
def initialize(id, profile_id, opts, &block)
@impact = nil
@title = nil
@ -42,7 +45,7 @@ module Inspec
@__rule_id = id
@__profile_id = profile_id
@__checks = []
@__skip_rule = {}
@__skip_rule = {} # { result: true, message: "Why", type: [:only_if, :waiver] }
@__merge_count = 0
@__merge_changes = []
@__skip_only_if_eval = opts[:skip_only_if_eval]
@ -52,6 +55,11 @@ module Inspec
begin
instance_eval(&block)
# By applying waivers *after* the instance eval, we assure that
# waivers have higher precedence than only_if.
__apply_waivers
rescue StandardError => e
# We've encountered an exception while trying to eval the code inside the
# control block. We need to prevent the exception from bubbling up, and
@ -141,6 +149,7 @@ module Inspec
return if @__skip_only_if_eval == true
@__skip_rule[:result] ||= !yield
@__skip_rule[:type] = :only_if
@__skip_rule[:message] = message
end
@ -193,9 +202,9 @@ module Inspec
rule.instance_variable_get(:@__skip_rule)
end
def self.set_skip_rule(rule, value, message = nil)
def self.set_skip_rule(rule, value, message = nil, type = :only_if)
rule.instance_variable_set(:@__skip_rule,
{ result: value, message: message })
{ result: value, message: message, type: type })
end
def self.merge_count(rule)
@ -206,14 +215,16 @@ module Inspec
rule.instance_variable_get(:@__merge_changes)
end
# If a rule is marked to be skipped, this
# creates a dummay array of "checks" with a skip outcome
def self.prepare_checks(rule)
skip_check = skip_status(rule)
return checks(rule) unless skip_check[:result].eql?(true)
if skip_check[:message]
msg = "Skipped control due to only_if condition: #{skip_check[:message]}"
msg = "Skipped control due to #{skip_check[:type]} condition: #{skip_check[:message]}"
else
msg = "Skipped control due to only_if condition."
msg = "Skipped control due to #{skip_check[:type]} condition."
end
# TODO: we use os as the carrier here, but should consider
@ -251,7 +262,8 @@ module Inspec
skip_check = skip_status(src)
sr = skip_check[:result]
msg = skip_check[:message]
set_skip_rule(dst, sr, msg) unless sr.nil?
skip_type = skip_check[:type]
set_skip_rule(dst, sr, msg, skip_type) unless sr.nil?
# Save merge history
dst.instance_variable_set(:@__merge_count, merge_count(dst) + 1)
@ -267,6 +279,56 @@ module Inspec
@__checks.push([describe_or_expect, values, block])
end
# Look for an input with a matching ID, and if found, apply waiver
# skipping logic. Basically, if we have a current waiver, and it says
# to skip, we'll replace all the checks with a dummy check (same as
# only_if mechanism)
# Double underscore: not intended to be called as part of the DSL
def __apply_waivers
input_name = @__rule_id # TODO: control ID slugging
registry = Inspec::InputRegistry.instance
input = registry.inputs_by_profile.dig(@__profile_id, input_name)
return unless input
# An InSpec Input is a datastructure that tracks a profile parameter
# over time. Its value can be set by many sources, and it keeps a
# log of each "set" event so that when it is collapsed to a value,
# it can determine the correct (highest priority) value.
# Store in an instance variable for.. later reading???
@__waiver_data = input.value
__waiver_data["skipped_due_to_waiver"] = false
__waiver_data["message"] = ""
# Waivers should have a hash value with keys possibly including skip and
# expiration_date. We only care here if it has a skip key and it
# is yes-like, since all non-skipped waiver operations are handled
# during reporting phase.
return unless __waiver_data.key?("skip") && __waiver_data["skip"]
# OK, the intent is to skip. Does it have an expiration date, and
# if so, is it in the future?
expiry = __waiver_data["expiration_date"]
if expiry
if expiry.is_a?(Date)
# It appears that yaml.rb automagically parses dates for us
if expiry < Date.today # If the waiver expired, return - no skip applied
__waiver_data["message"] = "Waiver expired on #{expiry}, evaluating control normally"
return
end
else
ui = Inspec::UI.new
ui.error("Unable to parse waiver expiration date '#{expiry}' for control #{@__rule_id}")
ui.exit(:usage_error)
end
end
# OK, apply a skip.
@__skip_rule[:result] = true
@__skip_rule[:type] = :waiver
@__skip_rule[:message] = __waiver_data["justification"]
__waiver_data["skipped_due_to_waiver"] = true
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

View file

@ -9,7 +9,6 @@ require "inspec/metadata"
require "inspec/config"
require "inspec/dependencies/cache"
require "inspec/dist"
require "inspec/resources"
require "inspec/reporters"
require "inspec/runner_rspec"
# spec requirements
@ -33,6 +32,7 @@ module Inspec
extend Forwardable
attr_reader :backend, :rules
attr_accessor :target_profiles
def attributes
Inspec.deprecate(:rename_attributes_to_inputs, "Don't call runner.attributes, call runner.inputs")
@ -56,6 +56,12 @@ module Inspec
RunnerRspec.new(@conf)
end
if @conf[:waiver_file]
waivers = @conf.delete(:waiver_file)
@conf[:input_file] ||= []
@conf[:input_file].concat waivers
end
# About reading inputs:
# @conf gets passed around a lot, eventually to
# Inspec::InputRegistry.register_external_inputs.

View file

@ -171,6 +171,7 @@ module Inspec
metadata[:descriptions] = rule.descriptions
metadata[:code] = rule.instance_variable_get(:@__code)
metadata[:source_location] = rule.instance_variable_get(:@__source_location)
metadata[:waiver_data] = rule.__waiver_data
end
end
end

View file

@ -112,6 +112,7 @@ module Inspec
#{print_target_info}
EOF
elsif topic == "resources"
require "inspec/resources"
resources.sort.each do |resource|
puts " - #{resource}"
end
@ -134,7 +135,13 @@ module Inspec
info += "https://www.inspec.io/docs/reference/resources/#{topic}\n\n"
puts info
else
puts "The resource #{topic} does not exist. For a list of valid resources, type: help resources"
begin
require "inspec/resources/#{topic}"
help topic
rescue LoadError
# TODO: stderr!
puts "The resource #{topic} does not exist. For a list of valid resources, type: help resources"
end
end
end

View file

@ -1,4 +1,4 @@
require "inspec/objects/input"
require "inspec/input"
module PkeyReader
def read_pkey(filecontent, passphrase)

View file

@ -1,3 +1,3 @@
module Inspec
VERSION = "4.10.4".freeze
VERSION = "4.17.5".freeze
end

View file

@ -1,5 +1,7 @@
# copyright: 2015, Vulcano Security GmbH
require "rspec/matchers"
RSpec::Matchers.define :be_readable do
match do |file|
file.readable?(@by, @by_user)

View file

@ -6,6 +6,14 @@ require "inspec/dist"
module InspecPlugins
module PluginManager
class CliCommand < Inspec.plugin(2, :cli_command)
INSTALL_TYPE_LABELS = {
bundle: "core", # Calling this core, too - not much of a distinction
core: "core",
path: "path",
user_gem: "gem (user)",
system_gem: "gem (system)",
}.freeze
include Inspec::Dist
subcommand_desc "plugin SUBCOMMAND", "Manage #{PRODUCT_NAME} and Train plugins"
@ -15,22 +23,36 @@ module InspecPlugins
#==================================================================#
desc "list [options]", "Lists user-installed #{PRODUCT_NAME} plugins."
option :all, desc: "Include plugins shipped with #{PRODUCT_NAME} as well.", type: :boolean, aliases: [:a]
option :all, desc: "List all types of plugins (default)", type: :boolean, default: true, aliases: [:a]
option :user, desc: "List user plugins, from ~/.inspec/gems", banner: "", type: :boolean, default: false, aliases: [:u]
option :system, desc: "List system plugins, those InSpec depends on", banner: "", type: :boolean, default: false, aliases: [:s]
option :core, desc: "List core plugins, those InSpec ships with", banner: "", type: :boolean, default: false, aliases: [:c]
def list
plugin_statuses = Inspec::Plugin::V2::Registry.instance.plugin_statuses
plugin_statuses.reject! { |s| %i{core bundle}.include?(s.installation_type) } unless options[:all]
puts
ui.bold(format(" %-30s%-10s%-8s%-6s", "Plugin Name", "Version", "Via", "ApiVer"))
ui.line
plugin_statuses.sort_by(&:name).each do |status|
ui.plain(format(" %-30s%-10s%-8s%-6s", status.name,
make_pretty_version(status),
status.installation_type,
status.api_generation.to_s))
options[:all] = false if options[:core] || options[:user] || options[:system]
plugin_statuses.select! do |status|
type = status.installation_type
options[:all] ||
(options[:core] && %i{core bundle}.include?(type)) ||
(options[:user] && %i{user_gem path}.include?(type)) ||
(options[:system] && :system_gem == type)
end
ui.line
ui.plain(" #{plugin_statuses.count} plugin(s) total")
unless plugin_statuses.empty?
ui.table do |t|
t.header = ["Plugin Name", "Version", "Via", "ApiVer"]
plugin_statuses.sort_by { |s| s.name.to_s }.each do |status|
t << [
status.name,
make_pretty_version(status),
make_pretty_install_type(status),
status.api_generation,
]
end
end
end
ui.plain_line(" #{plugin_statuses.count} plugin(s) total")
puts
end
@ -60,15 +82,15 @@ module InspecPlugins
end
puts
ui.bold(format(" %-30s%-50s", "Plugin Name", "Versions Available"))
ui.bold(format(" %-30s%-50s\n", "Plugin Name", "Versions Available"))
ui.line
search_results.keys.sort.each do |plugin_name|
versions = options[:all] ? search_results[plugin_name] : [search_results[plugin_name].first]
versions = "(" + versions.join(", ") + ")"
ui.plain(format(" %-30s%-50s", plugin_name, versions))
ui.plain_line(format(" %-30s%-50s", plugin_name, versions))
end
ui.line
ui.plain(" #{search_results.count} plugin(s) found")
ui.plain_line(" #{search_results.count} plugin(s) found")
puts
ui.exit Inspec::UI::EXIT_PLUGIN_ERROR if search_results.empty?
@ -118,14 +140,14 @@ module InspecPlugins
begin
installer.update(plugin_name)
rescue Inspec::Plugin::V2::UpdateError => ex
ui.plain("#{ui.red('Update error:')} #{ex.message} - update failed")
ui.plain_line("#{ui.red("Update error:", print: false)} #{ex.message} - update failed")
ui.exit Inspec::UI::EXIT_USAGE_ERROR
end
post_update_versions = installer.list_installed_plugin_gems.select { |spec| spec.name == plugin_name }.map { |spec| spec.version.to_s }
new_version = (post_update_versions - pre_update_versions).first
ui.bold(plugin_name + " plugin, version #{old_version} -> " \
"#{new_version}, updated from rubygems.org")
"#{new_version}, updated from rubygems.org\n")
end
#--------------------------
@ -144,7 +166,7 @@ module InspecPlugins
def uninstall(plugin_name)
status = Inspec::Plugin::V2::Registry.instance[plugin_name.to_sym]
unless status
ui.plain("#{ui.red('No such plugin installed:')} #{plugin_name} is not " \
ui.plain_line("#{ui.red("No such plugin installed:", print: false)} #{plugin_name} is not " \
"installed - uninstall failed")
ui.exit Inspec::UI::EXIT_USAGE_ERROR
end
@ -157,11 +179,12 @@ module InspecPlugins
if status.installation_type == :path
ui.bold(plugin_name + " path-based plugin install has been " \
"uninstalled")
"uninstalled\n")
else
ui.bold(plugin_name + " plugin, version #{old_version}, has " \
"been uninstalled")
"been uninstalled\n")
end
ui.exit Inspec::UI::EXIT_NORMAL
end
@ -174,7 +197,7 @@ module InspecPlugins
def install_from_gemfile(gem_file)
unless File.exist? gem_file
ui.red("No such plugin gem file #{gem_file} - installation failed.")
ui.red("No such plugin gem file #{gem_file} - installation failed.\n")
ui.exit Inspec::UI::EXIT_USAGE_ERROR
end
@ -186,13 +209,13 @@ module InspecPlugins
installer.install(plugin_name, gem_file: gem_file)
ui.bold("#{plugin_name} plugin, version #{version}, installed from " \
"local .gem file")
"local .gem file\n")
ui.exit Inspec::UI::EXIT_NORMAL
end
def install_from_path(path)
unless File.exist? path
ui.red("No such source code path #{path} - installation failed.")
ui.red("No such source code path #{path} - installation failed.\n")
ui.exit Inspec::UI::EXIT_USAGE_ERROR
end
@ -209,7 +232,7 @@ module InspecPlugins
if registry.known_plugin?(plugin_name.to_sym)
ui.red("Plugin already installed - #{plugin_name} - Use '#{EXEC_NAME} " \
"plugin list' to see previously installed plugin - " \
"installation failed.")
"installation failed.\n")
ui.exit Inspec::UI::EXIT_PLUGIN_ERROR
end
@ -223,7 +246,7 @@ module InspecPlugins
installer.install(plugin_name, path: entry_point)
ui.bold("#{plugin_name} plugin installed via source path reference, " \
"resolved to entry point #{entry_point}")
"resolved to entry point #{entry_point}\n")
ui.exit Inspec::UI::EXIT_NORMAL
end
@ -288,7 +311,7 @@ module InspecPlugins
# Give up.
ui.red("Unrecognizable plugin structure - #{parts[2]} - When " \
"installing from a path, please provide the path of the " \
"entry point file - installation failed.")
"entry point file - installation failed.\n")
ui.exit Inspec::UI::EXIT_USAGE_ERROR
end
@ -299,8 +322,8 @@ module InspecPlugins
rescue LoadError => ex
ui.red("Plugin contains errors - #{plugin_name} - Encountered " \
"errors while trying to test load the plugin entry point, " \
"resolved to #{entry_point} - installation failed")
ui.plain ex.message
"resolved to #{entry_point} - installation failed\n")
ui.plain_line ex.message
ui.exit Inspec::UI::EXIT_USAGE_ERROR
end
@ -313,7 +336,7 @@ module InspecPlugins
ui.red("Does not appear to be a plugin - #{plugin_name} - After " \
"probe-loading the supposed plugin, it did not register " \
"itself to Train. Ensure something inherits from " \
"'Train.plugin(1)' - installation failed.")
"'Train.plugin(1)' - installation failed.\n")
ui.exit Inspec::UI::EXIT_USAGE_ERROR
end
else
@ -321,7 +344,7 @@ module InspecPlugins
ui.red("Does not appear to be a plugin - #{plugin_name} - After " \
"probe-loading the supposed plugin, it did not register " \
"itself to InSpec. Ensure something inherits from " \
"'Inspec.plugin(2)' - installation failed.")
"'Inspec.plugin(2)' - installation failed.\n")
ui.exit Inspec::UI::EXIT_USAGE_ERROR
end
end
@ -343,7 +366,7 @@ module InspecPlugins
new_version = (post_installed_versions - pre_installed_versions).first
ui.bold("#{plugin_name} plugin, version #{new_version}, installed " \
"from rubygems.org")
"from rubygems.org\n")
ui.exit Inspec::UI::EXIT_NORMAL
end
@ -367,16 +390,16 @@ module InspecPlugins
what_we_would_install_is_already_installed = pre_installed_versions.include?(requested_version)
if what_we_would_install_is_already_installed && they_explicitly_asked_for_a_version
ui.red("Plugin already installed at requested version - plugin " \
"#{plugin_name} #{requested_version} - refusing to install.")
"#{plugin_name} #{requested_version} - refusing to install.\n")
elsif what_we_would_install_is_already_installed && !they_explicitly_asked_for_a_version
ui.red("Plugin already installed at latest version - plugin " \
"#{plugin_name} #{requested_version} - refusing to install.")
"#{plugin_name} #{requested_version} - refusing to install.\n")
else
# There are existing versions installed, but none of them are what was requested
ui.red("Update required - plugin #{plugin_name}, requested " \
"#{requested_version}, have " \
"#{pre_installed_versions.join(', ')}; use `inspec " \
"plugin update` - refusing to install.")
"#{pre_installed_versions.join(", ")}; use `inspec " \
"plugin update` - refusing to install.\n")
end
ui.exit Inspec::UI::EXIT_PLUGIN_ERROR
@ -387,11 +410,11 @@ module InspecPlugins
installer.install(plugin_name, version: options[:version])
rescue Inspec::Plugin::V2::PluginExcludedError => ex
ui.red("Plugin on Exclusion List - #{plugin_name} is listed as an " \
"incompatible gem - refusing to install.")
ui.plain("Rationale: #{ex.details.rationale}")
ui.plain("Exclusion list location: " +
"incompatible gem - refusing to install.\n")
ui.plain_line("Rationale: #{ex.details.rationale}")
ui.plain_line("Exclusion list location: " +
File.join(Inspec.src_root, "etc", "plugin_filters.json"))
ui.plain("If you disagree with this determination, please accept " \
ui.plain_line("If you disagree with this determination, please accept " \
"our apologies for the misunderstanding, and open an issue " \
"at https://github.com/inspec/inspec/issues/new")
ui.exit Inspec::UI::EXIT_PLUGIN_ERROR
@ -401,13 +424,13 @@ module InspecPlugins
results = installer.search(plugin_name, exact: true)
if results.empty?
ui.red("No such plugin gem #{plugin_name} could be found on " \
"rubygems.org - installation failed.")
"rubygems.org - installation failed.\n")
elsif options[:version] && !results[plugin_name].include?(options[:version])
ui.red("No such version - #{plugin_name} exists, but no such " \
"version #{options[:version]} found on rubygems.org - " \
"installation failed.")
"installation failed.\n")
else
ui.red("Unknown error occured - installation failed.")
ui.red("Unknown error occured - installation failed.\n")
end
ui.exit Inspec::UI::EXIT_USAGE_ERROR
end
@ -420,10 +443,10 @@ module InspecPlugins
# Check for path install
status = Inspec::Plugin::V2::Registry.instance[plugin_name.to_sym]
if !status
ui.plain("#{ui.red('No such plugin installed:')} #{plugin_name} - update failed")
ui.plain_line("#{ui.red("No such plugin installed:", print: false)} #{plugin_name} - update failed")
ui.exit Inspec::UI::EXIT_USAGE_ERROR
elsif status.installation_type == :path
ui.plain("#{ui.red('Cannot update path-based install:')} " \
ui.plain_line("#{ui.red("Cannot update path-based install:", print: false)} " \
"#{plugin_name} is installed via path reference; " \
"use `inspec plugin uninstall` to remove - refusing to" \
"update")
@ -436,7 +459,7 @@ module InspecPlugins
latest_version = latest_version[plugin_name]&.last
if pre_update_versions.include?(latest_version)
ui.plain("#{ui.red('Already installed at latest version:')} " \
ui.plain_line("#{ui.red("Already installed at latest version:", print: false)} " \
"#{plugin_name} is at #{latest_version}, which the " \
"latest - refusing to update")
ui.exit Inspec::UI::EXIT_PLUGIN_ERROR
@ -458,7 +481,7 @@ module InspecPlugins
unless plugin_name =~ /^(inspec|train)-/
ui.red("Invalid plugin name - #{plugin_name} - All inspec " \
"plugins must begin with either 'inspec-' or 'train-' " \
"- #{action} failed.")
"- #{action} failed.\n")
ui.exit Inspec::UI::EXIT_USAGE_ERROR
end
end
@ -467,17 +490,29 @@ module InspecPlugins
case status.installation_type
when :core, :bundle
Inspec::VERSION
when :gem
# TODO: this is naive, and assumes the latest version is the one that will be used. Logged on #3317
# In fact, the logic to determine "what version would be used" belongs in the Loader.
Inspec::Plugin::V2::Loader.list_installed_plugin_gems
.select { |spec| spec.name == status.name.to_s }
.sort_by(&:version)
.last.version
when :user_gem, :system_gem
if status.version.nil?
"(unknown)"
elsif status.version =~ /^\d+\.\d+\.\d+$/
status.version
else
# Assume it is a version constraint string and try to resolve
# TODO: this is naive, and assumes the latest version is the one that will be used. Logged on #3317
# In fact, the logic to determine "what version would be used" belongs in the Loader.
plugin_name = status.name.to_s
Inspec::Plugin::V2::Loader.list_installed_plugin_gems
.select { |spec| spec.name == plugin_name }
.sort_by(&:version)
.last.version
end
when :path
"src"
end
end
def make_pretty_install_type(status)
INSTALL_TYPE_LABELS[status.installation_type]
end
end
end
end

View file

@ -0,0 +1,23 @@
require_relative "helper"
class PluginManagerCliHelp < Minitest::Test
include CorePluginFunctionalHelper
# Main inspec help subcommand listing
def test_inspec_help_includes_plugin
result = run_inspec_process_with_this_plugin("help")
skip_windows!
assert_includes result.stdout, "inspec plugin"
end
# inspec plugin help subcommand listing
def test_inspec_plugin_help_includes_plugin
result = run_inspec_process_with_this_plugin("plugin help")
skip_windows!
assert_includes result.stdout, "inspec plugin list"
assert_includes result.stdout, "inspec plugin search"
assert_includes result.stdout, "inspec plugin install"
assert_includes result.stdout, "inspec plugin update"
assert_includes result.stdout, "inspec plugin uninstall"
end
end

View file

@ -0,0 +1,62 @@
require "plugins/shared/core_plugin_test_helper"
module PluginManagerHelpers
let(:project_repo_path) { File.expand_path(File.join(__FILE__, "..", "..", "..")) }
let(:project_fixtures_path) { File.join(project_repo_path, "test", "fixtures") }
let(:project_config_dirs_path) { File.join(project_fixtures_path, "config_dirs") }
let(:empty_config_dir_path) { File.join(project_config_dirs_path, "empty") }
let(:list_after_run) do
Proc.new do |run_result, tmp_dir|
# After installing/uninstalling/whatevering, run list with config in the same dir, and capture it.
run_result.payload.list_result = parse_plugin_list_lines(
run_inspec_process("plugin list", env: { INSPEC_CONFIG_DIR: tmp_dir }).stdout
)
end
end
def copy_in_project_config_dir(fixture_name, dest = nil)
src = Dir.glob(File.join(project_config_dirs_path, fixture_name, "*"))
dest ||= File.join(project_config_dirs_path, "empty")
src.each { |path| FileUtils.cp_r(path, dest) }
end
def copy_in_core_config_dir(fixture_name, dest = nil)
src = Dir.glob(File.join(core_config_dir_path, fixture_name, "*"))
dest ||= File.join(project_config_dirs_path, "empty")
src.each { |path| FileUtils.cp_r(path, dest) }
end
def clear_empty_config_dir
Dir.glob(File.join(project_config_dirs_path, "empty", "*")).each do |path|
next if path.end_with? ".gitkeep"
FileUtils.rm_rf(path)
end
end
def parse_plugin_list_lines(stdout)
plugins = []
stdout.force_encoding("UTF-8").lines.each do |line|
next if line.strip.empty?
next if line.include? "─────" # This is some unicode glyphiness
next if line.include? "Plugin Name"
next if line.include? "plugin(s) total"
parts = line.split(//u).map(&:strip!).compact
plugins << {
name: parts[0],
version: parts[1],
type: parts[2],
generation: parts[3],
raw: line,
}
end
plugins
end
def teardown
clear_empty_config_dir
end
end

View file

@ -1,817 +0,0 @@
#=========================================================================================#
# `inspec plugin SUBCOMMAND` facility
#=========================================================================================#
require_relative "../../../shared/core_plugin_test_helper.rb"
#-----------------------------------------------------------------------------------------#
# utilities
#-----------------------------------------------------------------------------------------#
module PluginManagerHelpers
let(:project_repo_path) { File.expand_path(File.join(__FILE__, "..", "..", "..")) }
let(:project_fixtures_path) { File.join(project_repo_path, "test", "fixtures") }
let(:project_config_dirs_path) { File.join(project_fixtures_path, "config_dirs") }
let(:empty_config_dir_path) { File.join(project_config_dirs_path, "empty") }
let(:list_after_run) do
Proc.new do |run_result, tmp_dir|
# After installing/uninstalling/whatevering, run list with config in the same dir, and capture it.
run_result.payload.list_result = run_inspec_process("plugin list", env: { INSPEC_CONFIG_DIR: tmp_dir })
end
end
def copy_in_project_config_dir(fixture_name, dest = nil)
src = Dir.glob(File.join(project_config_dirs_path, fixture_name, "*"))
dest ||= File.join(project_config_dirs_path, "empty")
src.each { |path| FileUtils.cp_r(path, dest) }
end
def copy_in_core_config_dir(fixture_name, dest = nil)
src = Dir.glob(File.join(core_config_dir_path, fixture_name, "*"))
dest ||= File.join(project_config_dirs_path, "empty")
src.each { |path| FileUtils.cp_r(path, dest) }
end
def clear_empty_config_dir
Dir.glob(File.join(project_config_dirs_path, "empty", "*")).each do |path|
next if path.end_with? ".gitkeep"
FileUtils.rm_rf(path)
end
end
def teardown
clear_empty_config_dir
end
end
#-----------------------------------------------------------------------------------------#
# inspec help
#-----------------------------------------------------------------------------------------#
class PluginManagerCliHelp < Minitest::Test
include CorePluginFunctionalHelper
# Main inspec help subcommand listing
def test_inspec_help_includes_plugin
result = run_inspec_process_with_this_plugin("help")
skip_windows!
assert_includes result.stdout, "inspec plugin"
end
# inspec plugin help subcommand listing
def test_inspec_plugin_help_includes_plugin
result = run_inspec_process_with_this_plugin("plugin help")
skip_windows!
assert_includes result.stdout, "inspec plugin list"
assert_includes result.stdout, "inspec plugin search"
assert_includes result.stdout, "inspec plugin install"
assert_includes result.stdout, "inspec plugin update"
assert_includes result.stdout, "inspec plugin uninstall"
end
end
#-----------------------------------------------------------------------------------------#
# inspec plugin list
#-----------------------------------------------------------------------------------------#
class PluginManagerCliList < Minitest::Test
include CorePluginFunctionalHelper
include PluginManagerHelpers
def test_list_when_no_user_plugins_installed
result = run_inspec_process_with_this_plugin("plugin list")
skip_windows!
assert_includes result.stdout, "0 plugin(s) total", "Empty list should include zero count"
assert_exit_code 0, result
end
def test_list_all_when_no_user_plugins_installed
result = run_inspec_process_with_this_plugin("plugin list --all")
skip_windows!
assert_includes result.stdout, "6 plugin(s) total", "--all list should find six"
assert_includes result.stdout, "inspec-plugin-manager-cli", "--all list should find inspec-plugin-manager-cli"
assert_includes result.stdout, "habitat", "--all list should find habitat"
assert_exit_code 0, result
# TODO: split
result = run_inspec_process_with_this_plugin("plugin list -a")
assert_includes result.stdout, "6 plugin(s) total", "-a list should find six"
assert_exit_code 0, result
end
def test_list_when_gem_and_path_plugins_installed
pre_block = Proc.new do |plugin_statefile_data, tmp_dir|
plugin_statefile_data.clear # Signal not to write a file, we'll provide one.
copy_in_core_config_dir("test-fixture-1-float", tmp_dir)
end
result = run_inspec_process_with_this_plugin("plugin list", pre_run: pre_block)
skip_windows!
assert_includes result.stdout, "2 plugin(s) total", "gem+path should show two plugins"
# Plugin Name Version Via ApiVer
# -------------------------------------------------------
# inspec-meaning-of-life src path 2
# inspec-test-fixture 0.1.0 gem 2
# -------------------------------------------------------
# 2 plugin(s) total
gem_line = result.stdout.split("\n").grep(/gem/).first
assert_match(/\s*inspec-\S+\s+\d+\.\d+\.\d+\s+gem\s+2/, gem_line)
path_line = result.stdout.split("\n").grep(/path/).first
assert_match(/\s*inspec-\S+\s+src\s+path\s+2/, path_line)
assert_exit_code 0, result
end
def test_list_when_a_train_plugin_is_installed
pre_block = Proc.new do |plugin_statefile_data, tmp_dir|
plugin_statefile_data.clear # Signal not to write a file, we'll provide one.
copy_in_core_config_dir("train-test-fixture", tmp_dir)
end
result = run_inspec_process_with_this_plugin("plugin list", pre_run: pre_block)
skip_windows!
assert_includes result.stdout, "1 plugin(s) total", "list train should show one plugins"
# Plugin Name Version Via ApiVer
# -------------------------------------------------------
# train-test-fixture 0.1.0 gem train-1
# -------------------------------------------------------
# 1 plugin(s) total
train_line = result.stdout.split("\n").grep(/train/).first
assert_includes(train_line, "train-test-fixture")
assert_includes(train_line, "0.1.0")
assert_includes(train_line, "gem")
assert_includes(train_line, "train-1")
assert_exit_code 0, result
end
end
#-----------------------------------------------------------------------------------------#
# inspec plugin search
#-----------------------------------------------------------------------------------------#
class PluginManagerCliSearch < Minitest::Test
include CorePluginFunctionalHelper
include PluginManagerHelpers
# TODO: Thor can't hide options, but we wish it could.
# def test_search_include_fixture_hidden_option
# result = run_inspec_process_with_this_plugin('plugin help search')
# refute_includes result.stdout, '--include-test-fixture'
# end
def test_search_for_a_real_gem_with_full_name_no_options
result = run_inspec_process("plugin search --include-test-fixture inspec-test-fixture")
assert_includes result.stdout, "inspec-test-fixture", "Search result should contain the gem name"
assert_includes result.stdout, "1 plugin(s) found", "Search result should find 1 plugin"
line = result.stdout.split("\n").grep(/inspec-test-fixture/).first
assert_match(/\s*inspec-test-fixture\s+\((\d+\.\d+\.\d+){1}\)/, line, "Plugin line should include name and exactly one version")
assert_exit_code 0, result
end
def test_search_for_a_real_gem_with_stub_name_no_options
result = run_inspec_process("plugin search --include-test-fixture inspec-test-")
assert_includes result.stdout, "inspec-test-fixture", "Search result should contain the gem name"
assert_includes result.stdout, "1 plugin(s) found", "Search result should find 1 plugin"
line = result.stdout.split("\n").grep(/inspec-test-fixture/).first
assert_match(/\s*inspec-test-fixture\s+\((\d+\.\d+\.\d+){1}\)/, line, "Plugin line should include name and exactly one version")
assert_exit_code 0, result
end
def test_search_for_a_real_gem_with_full_name_and_exact_option
result = run_inspec_process("plugin search --exact --include-test-fixture inspec-test-fixture")
assert_includes result.stdout, "inspec-test-fixture", "Search result should contain the gem name"
assert_includes result.stdout, "1 plugin(s) found", "Search result should find 1 plugin"
assert_exit_code 0, result
# TODO: split
result = run_inspec_process("plugin search -e --include-test-fixture inspec-test-fixture")
assert_exit_code 0, result
end
def test_search_for_a_real_gem_with_stub_name_and_exact_option
result = run_inspec_process("plugin search --exact --include-test-fixture inspec-test-")
assert_includes result.stdout, "0 plugin(s) found", "Search result should find 0 plugins"
assert_exit_code 2, result
# TODO: split
result = run_inspec_process("plugin search -e --include-test-fixture inspec-test-")
assert_exit_code 2, result
end
def test_search_for_a_real_gem_with_full_name_and_all_option
result = run_inspec_process("plugin search --all --include-test-fixture inspec-test-fixture")
assert_includes result.stdout, "inspec-test-fixture", "Search result should contain the gem name"
assert_includes result.stdout, "1 plugin(s) found", "Search result should find 1 plugin"
line = result.stdout.split("\n").grep(/inspec-test-fixture/).first
assert_match(/\s*inspec-test-fixture\s+\((\d+\.\d+\.\d+(,\s)?){2,}\)/, line, "Plugin line should include name and at least two versions")
assert_exit_code 0, result
# TODO: split
result = run_inspec_process("plugin search -a --include-test-fixture inspec-test-fixture")
assert_exit_code 0, result
end
def test_search_for_a_gem_with_missing_prefix
result = run_inspec_process("plugin search --include-test-fixture test-fixture")
assert_exit_code 1, result
assert_includes result.stdout, "All inspec plugins must begin with either 'inspec-' or 'train-'"
end
def test_search_for_a_gem_that_does_not_exist
result = run_inspec_process("plugin search --include-test-fixture inspec-test-fixture-nonesuch")
assert_includes result.stdout, "0 plugin(s) found", "Search result should find 0 plugins"
assert_exit_code 2, result
end
def test_search_for_a_real_gem_with_full_name_no_options_and_train_name
result = run_inspec_process("plugin search --include-test-fixture train-test-fixture")
assert_includes result.stdout, "train-test-fixture", "Search result should contain the gem name"
assert_includes result.stdout, "1 plugin(s) found", "Search result should find 1 plugin"
line = result.stdout.split("\n").grep(/train-test-fixture/).first
assert_match(/\s*train-test-fixture\s+\((\d+\.\d+\.\d+){1}\)/, line, "Plugin line should include name and exactly one version")
assert_exit_code 0, result
end
def test_search_omit_excluded_inspec_plugins
result = run_inspec_process("plugin search --include-test-fixture inspec-")
assert_includes result.stdout, "inspec-test-fixture", "Search result should contain the test gem"
%w{
inspec-core
inspec-multi-server
}.each do |plugin_name|
refute_includes result.stdout, plugin_name, "Search result should not contain excluded gems"
end
assert_exit_code 0, result
end
def test_search_for_a_real_gem_with_full_name_no_options_filter_fixtures
result = run_inspec_process("plugin search inspec-test-fixture")
refute_includes result.stdout, "inspec-test-fixture", "Search result should not contain the fixture gem name"
end
def test_search_for_a_real_gem_with_full_name_no_options_filter_fixtures_train
result = run_inspec_process("plugin search train-test-fixture")
refute_includes result.stdout, "train-test-fixture", "Search result should not contain the fixture gem name"
end
end
#-----------------------------------------------------------------------------------------#
# inspec plugin install
#-----------------------------------------------------------------------------------------#
class PluginManagerCliInstall < Minitest::Test
include CorePluginFunctionalHelper # gives us instance methods, like `let` aliases inside test methods
extend CorePluginFunctionalHelper # gives us class methods, like `let` aliases out here outside test methods
include PluginManagerHelpers
ruby_abi_version = (Gem.ruby_version.segments[0, 2] << 0).join(".")
# Test multiple hueristics of the path-mode install.
# These are all positive tests; they should resolve the entry point to the same path in each case.
{
"is_perfect" => {
given: File.join(core_fixture_plugins_path, "inspec-test-fixture", "lib", "inspec-test-fixture.rb"),
},
"refers_to_the_entry_point_with_no_extension" => {
given: File.join(core_fixture_plugins_path, "inspec-test-fixture", "lib", "inspec-test-fixture"),
},
"refers_to_the_src_root_of_a_plugin" => {
given: File.join(core_fixture_plugins_path, "inspec-test-fixture"),
},
"refers_to_a_versioned_gem_install" => {
given: File.join(core_config_dir_path, "test-fixture-1-float", "gems", ruby_abi_version, "gems", "inspec-test-fixture-0.1.0", "lib", "inspec-test-fixture.rb"),
resolved_path: File.join(core_config_dir_path, "test-fixture-1-float", "gems", ruby_abi_version, "gems", "inspec-test-fixture-0.1.0", "lib", "inspec-test-fixture.rb"),
},
"refers_to_a_versioned_gem_install_missing_extension" => {
given: File.join(core_config_dir_path, "test-fixture-1-float", "gems", ruby_abi_version, "gems", "inspec-test-fixture-0.1.0", "lib", "inspec-test-fixture"),
resolved_path: File.join(core_config_dir_path, "test-fixture-1-float", "gems", ruby_abi_version, "gems", "inspec-test-fixture-0.1.0", "lib", "inspec-test-fixture.rb"),
},
"refers_to_a_relative_path" => {
given: File.join("test", "unit", "mock", "plugins", "inspec-test-fixture", "lib", "inspec-test-fixture.rb"),
},
"refers_to_a_train_plugin" => {
given: File.join(core_config_dir_path, "train-test-fixture", "gems", ruby_abi_version, "gems", "train-test-fixture-0.1.0", "lib", "train-test-fixture.rb"),
plugin_name: "train-test-fixture",
resolved_path: File.join(core_config_dir_path, "train-test-fixture", "gems", ruby_abi_version, "gems", "train-test-fixture-0.1.0", "lib", "train-test-fixture.rb"),
},
}.each do |test_name, fixture_info|
define_method(("test_install_from_path_when_path_" + test_name).to_sym) do
fixture_info = {
plugin_name: "inspec-test-fixture",
resolved_path: File.join(core_fixture_plugins_path, "inspec-test-fixture", "lib", "inspec-test-fixture.rb"),
}.merge(fixture_info)
install_result = run_inspec_process_with_this_plugin("plugin install #{fixture_info[:given]}", post_run: list_after_run)
# Check UX messaging
success_message = install_result.stdout.split("\n").grep(/installed/).last
skip_windows!
refute_nil success_message, "Should find a success message at the end"
assert_includes success_message, fixture_info[:plugin_name]
assert_includes success_message, "plugin installed via source path reference"
# Check round-trip UX via list
list_result = install_result.payload.list_result
itf_line = list_result.stdout.split("\n").grep(Regexp.new(fixture_info[:plugin_name])).first
refute_nil itf_line, "plugin name should now appear in the output of inspec list"
assert_match(/\s*(inspec|train)-test-fixture\s+src\s+path\s+/, itf_line, "list output should show that it is a path installation")
# Check plugin statefile. Extra important in this case, since all should resolve to the same entry point.
plugin_data = install_result.payload.plugin_data
entry = plugin_data["plugins"].detect { |e| e["name"] == fixture_info[:plugin_name] }
assert_equal fixture_info[:resolved_path], entry["installation_path"], "Regardless of input, the entry point should be correct."
assert_empty install_result.stderr
assert_exit_code 0, install_result
end
end
def test_fail_install_from_nonexistant_path
bad_path = File.join(project_fixtures_path, "none", "such", "inspec-test-fixture-nonesuch.rb")
install_result = run_inspec_process_with_this_plugin("plugin install #{bad_path}")
error_message = install_result.stdout.split("\n").last
skip_windows!
assert_includes error_message, "No such source code path"
assert_includes error_message, "inspec-test-fixture-nonesuch.rb"
assert_includes error_message, "installation failed"
assert_empty install_result.stderr
assert_exit_code 1, install_result
end
def test_fail_install_from_path_with_wrong_name
bad_path = File.join(project_fixtures_path, "plugins", "wrong-name", "lib", "wrong-name.rb")
install_result = run_inspec_process_with_this_plugin("plugin install #{bad_path}")
error_message = install_result.stdout.split("\n").last
skip_windows!
assert_includes error_message, "Invalid plugin name"
assert_includes error_message, "wrong-name"
assert_includes error_message, "All inspec plugins must begin with either 'inspec-' or 'train-'"
assert_includes error_message, "installation failed"
assert_empty install_result.stderr
assert_exit_code 1, install_result
end
def test_fail_install_from_path_when_it_is_not_a_plugin
bad_path = File.join(project_fixtures_path, "plugins", "inspec-egg-white-omelette", "lib", "inspec-egg-white-omelette.rb")
install_result = run_inspec_process_with_this_plugin("plugin install #{bad_path}")
error_message = install_result.stdout.split("\n").last
skip_windows!
assert_includes error_message, "Does not appear to be a plugin"
assert_includes error_message, "inspec-egg-white-omelette"
assert_includes error_message, "After probe-loading the supposed plugin, it did not register"
assert_includes error_message, "Ensure something inherits from 'Inspec.plugin(2)'"
assert_includes error_message, "installation failed"
assert_empty install_result.stderr
assert_exit_code 1, install_result
end
def test_fail_install_from_path_when_it_is_already_installed
plugin_path = File.join(core_fixture_plugins_path, "inspec-test-fixture", "lib", "inspec-test-fixture.rb")
pre_block = Proc.new do |plugin_data, _tmp_dir|
plugin_data["plugins"] << {
"name" => "inspec-test-fixture",
"installation_type" => "path",
"installation_path" => plugin_path,
}
end
install_result = run_inspec_process_with_this_plugin("plugin install #{plugin_path}", pre_run: pre_block)
error_message = install_result.stdout.split("\n").last
skip_windows!
assert_includes error_message, "Plugin already installed"
assert_includes error_message, "inspec-test-fixture"
assert_includes error_message, "Use 'inspec plugin list' to see previously installed plugin"
assert_includes error_message, "installation failed"
assert_empty install_result.stderr
assert_exit_code 2, install_result
end
def test_fail_install_from_path_when_the_dir_structure_is_wrong
bad_path = File.join(project_fixtures_path, "plugins", "inspec-wrong-structure")
install_result = run_inspec_process_with_this_plugin("plugin install #{bad_path}")
error_message = install_result.stdout.split("\n").last
skip_windows!
assert_includes error_message, "Unrecognizable plugin structure"
assert_includes error_message, "inspec-wrong-structure"
assert_includes error_message, " When installing from a path, please provide the path of the entry point file"
assert_includes error_message, "installation failed"
assert_empty install_result.stderr
assert_exit_code 1, install_result
end
def test_install_from_gemfile
fixture_gemfile_path = File.join(core_fixture_plugins_path, "inspec-test-fixture", "pkg", "inspec-test-fixture-0.1.0.gem")
install_result = run_inspec_process_with_this_plugin("plugin install #{fixture_gemfile_path}", post_run: list_after_run)
success_message = install_result.stdout.split("\n").grep(/installed/).last
skip_windows!
refute_nil success_message, "Should find a success message at the end"
assert_includes success_message, "inspec-test-fixture"
assert_includes success_message, "0.1.0"
assert_includes success_message, "installed from local .gem file"
list_result = install_result.payload.list_result
itf_line = list_result.stdout.split("\n").grep(/inspec-test-fixture/).first
refute_nil itf_line, "inspec-test-fixture should now appear in the output of inspec list"
assert_match(/\s*inspec-test-fixture\s+0.1.0\s+gem\s+/, itf_line, "list output should show that it is a gem installation with version")
assert_empty install_result.stderr
assert_exit_code 0, install_result
end
def test_fail_install_from_nonexistant_gemfile
bad_path = File.join(project_fixtures_path, "none", "such", "inspec-test-fixture-nonesuch-0.3.0.gem")
install_result = run_inspec_process_with_this_plugin("plugin install #{bad_path}")
skip_windows!
assert_match(/No such plugin gem file .+ - installation failed./, install_result.stdout)
assert_empty install_result.stderr
assert_exit_code 1, install_result
end
def test_install_from_rubygems_latest
install_result = run_inspec_process_with_this_plugin("plugin install inspec-test-fixture", post_run: list_after_run)
success_message = install_result.stdout.split("\n").grep(/installed/).last
skip_windows!
refute_nil success_message, "Should find a success message at the end"
assert_includes success_message, "inspec-test-fixture"
assert_includes success_message, "0.2.0"
assert_includes success_message, "installed from rubygems.org"
list_result = install_result.payload.list_result
itf_line = list_result.stdout.split("\n").grep(/inspec-test-fixture/).first
refute_nil itf_line, "inspec-test-fixture should now appear in the output of inspec list"
assert_match(/\s*inspec-test-fixture\s+0.2.0\s+gem\s+/, itf_line, "list output should show that it is a gem installation with version")
assert_empty install_result.stderr
assert_exit_code 0, install_result
end
def test_fail_install_from_nonexistant_remote_rubygem
install_result = run_inspec_process_with_this_plugin("plugin install inspec-test-fixture-nonesuch")
skip_windows!
assert_match(/No such plugin gem .+ could be found on rubygems.org - installation failed./, install_result.stdout)
assert_empty install_result.stderr
assert_exit_code 1, install_result
end
def test_install_from_rubygems_with_pinned_version
install_result = run_inspec_process_with_this_plugin("plugin install inspec-test-fixture -v 0.1.0", post_run: list_after_run)
success_message = install_result.stdout.split("\n").grep(/installed/).last
skip_windows!
refute_nil success_message, "Should find a success message at the end"
assert_includes success_message, "inspec-test-fixture"
assert_includes success_message, "0.1.0"
assert_includes success_message, "installed from rubygems.org"
list_result = install_result.payload.list_result
itf_line = list_result.stdout.split("\n").grep(/inspec-test-fixture/).first
refute_nil itf_line, "inspec-test-fixture should now appear in the output of inspec list"
assert_match(/\s*inspec-test-fixture\s+0.1.0\s+gem\s+/, itf_line, "list output should show that it is a gem installation with version")
assert_empty install_result.stderr
assert_exit_code 0, install_result
end
def test_fail_install_from_nonexistant_rubygem_version
install_result = run_inspec_process_with_this_plugin("plugin install inspec-test-fixture -v 99.99.99")
fail_message = install_result.stdout.split("\n").grep(/failed/).last
skip_windows!
refute_nil fail_message, "Should find a failure message at the end"
assert_includes fail_message, "inspec-test-fixture"
assert_includes fail_message, "99.99.99"
assert_includes fail_message, "no such version"
assert_includes fail_message, "on rubygems.org"
assert_empty install_result.stderr
assert_exit_code 1, install_result
end
def test_refuse_install_when_missing_prefix
install_result = run_inspec_process_with_this_plugin("plugin install test-fixture")
fail_message = install_result.stdout.split("\n").grep(/failed/).last
skip_windows!
refute_nil fail_message, "Should find a failure message at the end"
assert_includes fail_message, "test-fixture"
assert_includes fail_message, "All inspec plugins must begin with either 'inspec-' or 'train-'"
assert_empty install_result.stderr
assert_exit_code 1, install_result
end
def test_refuse_install_when_already_installed_same_version
pre_block = Proc.new do |plugin_statefile_data, tmp_dir|
plugin_statefile_data.clear # Signal not to write a file, we'll provide one.
copy_in_core_config_dir("test-fixture-2-float", tmp_dir)
end
install_result = run_inspec_process_with_this_plugin("plugin install inspec-test-fixture", pre_run: pre_block)
refusal_message = install_result.stdout.split("\n").grep(/refusing/).last
skip_windows!
refute_nil refusal_message, "Should find a failure message at the end"
assert_includes refusal_message, "inspec-test-fixture"
assert_includes refusal_message, "0.2.0"
assert_includes refusal_message, "Plugin already installed at latest version"
assert_empty install_result.stderr
assert_exit_code 2, install_result
end
def test_refuse_install_when_already_installed_can_update
pre_block = Proc.new do |plugin_statefile_data, tmp_dir|
plugin_statefile_data.clear # Signal not to write a file, we'll provide one.
copy_in_core_config_dir("test-fixture-1-float", tmp_dir)
end
install_result = run_inspec_process_with_this_plugin("plugin install inspec-test-fixture", pre_run: pre_block)
refusal_message = install_result.stdout.split("\n").grep(/refusing/).last
skip_windows!
refute_nil refusal_message, "Should find a failure message at the end"
assert_includes refusal_message, "inspec-test-fixture"
assert_includes refusal_message, "0.1.0"
assert_includes refusal_message, "0.2.0"
assert_includes refusal_message, "Update required"
assert_includes refusal_message, "inspec plugin update"
assert_empty install_result.stderr
assert_exit_code 2, install_result
end
def test_install_from_rubygems_latest_with_train_plugin
install_result = run_inspec_process_with_this_plugin("plugin install train-test-fixture", post_run: list_after_run)
success_message = install_result.stdout.split("\n").grep(/installed/).last
skip_windows!
refute_nil success_message, "Should find a success message at the end"
assert_includes success_message, "train-test-fixture"
assert_includes success_message, "0.1.0"
assert_includes success_message, "installed from rubygems.org"
list_result = install_result.payload.list_result
itf_line = list_result.stdout.split("\n").grep(/train-test-fixture/).first
refute_nil itf_line, "train-test-fixture should now appear in the output of inspec list"
assert_match(/\s*train-test-fixture\s+0.1.0\s+gem\s+/, itf_line, "list output should show that it is a gem installation with version")
assert_empty install_result.stderr
assert_exit_code 0, install_result
end
def test_refuse_install_when_plugin_on_exclusion_list
# Here, 'inspec-core', 'inspec-multi-server', and 'train-tax-collector'
# are the names of real rubygems. They are not InSpec/Train plugins, though,
# and installing them would be a jam-up.
# This is configured in 'etc/plugin-filter.json'.
%w{
inspec-core
inspec-multi-server
train-tax-calculator
}.each do |plugin_name|
install_result = run_inspec_process_with_this_plugin("plugin install #{plugin_name}")
refusal_message = install_result.stdout
refute_nil refusal_message, "Should find a failure message at the end"
skip_windows!
assert_includes refusal_message, plugin_name
assert_includes refusal_message, "Plugin on Exclusion List"
assert_includes refusal_message, "refusing to install"
assert_includes refusal_message, "Rationale:"
assert_includes refusal_message, "etc/plugin_filters.json"
assert_includes refusal_message, "github.com/inspec/inspec/issues/new"
assert_empty install_result.stderr
assert_exit_code 2, install_result
end
end
def test_error_install_with_debug_enabled
skip "this test requires bundler to pass" unless defined? ::Bundler
install_result = run_inspec_process_with_this_plugin("plugin install inspec-test-fixture -v 0.1.1 --log-level debug")
skip_windows!
assert_includes install_result.stdout, "DEBUG"
assert_includes install_result.stderr, "can't activate rake"
assert_exit_code 1, install_result
end
end
#-----------------------------------------------------------------------------------------#
# inspec plugin update
#-----------------------------------------------------------------------------------------#
class PluginManagerCliUpdate < Minitest::Test
include CorePluginFunctionalHelper
include PluginManagerHelpers
def test_when_a_plugin_can_be_updated
skip "this test requires bundler to pass" unless defined? ::Bundler
pre_block = Proc.new do |plugin_statefile_data, tmp_dir|
plugin_statefile_data.clear # Signal not to write a file, we'll provide one.
copy_in_core_config_dir("test-fixture-1-float", tmp_dir)
end
update_result = run_inspec_process_with_this_plugin("plugin update inspec-test-fixture", pre_run: pre_block, post_run: list_after_run)
success_message = update_result.stdout.split("\n").grep(/updated/).last
skip_windows!
refute_nil success_message, "Should find a success message at the end"
assert_includes success_message, "inspec-test-fixture"
assert_includes success_message, "0.1.0"
assert_includes success_message, "0.2.0"
assert_includes success_message, "updated from rubygems.org"
list_result = update_result.payload.list_result
itf_line = list_result.stdout.split("\n").grep(/inspec-test-fixture/).first
refute_nil itf_line, "inspec-test-fixture should appear in the output of inspec list"
assert_match(/\s*inspec-test-fixture\s+0.2.0\s+gem\s+/, itf_line, "list output should show that it is a gem installation with version 0.2.0")
assert_empty update_result.stderr
assert_exit_code 0, update_result
end
def test_refuse_update_when_already_current
pre_block = Proc.new do |plugin_statefile_data, tmp_dir|
plugin_statefile_data.clear # Signal not to write a file, we'll provide one.
copy_in_core_config_dir("test-fixture-2-float", tmp_dir)
end
update_result = run_inspec_process_with_this_plugin("plugin update inspec-test-fixture", pre_run: pre_block)
refusal_message = update_result.stdout.split("\n").grep(/refusing/).last
skip_windows!
refute_nil refusal_message, "Should find a failure message at the end"
assert_includes refusal_message, "inspec-test-fixture"
assert_includes refusal_message, "0.2.0"
assert_includes refusal_message, "Already installed at latest version"
assert_empty update_result.stderr
assert_exit_code 2, update_result
end
def test_fail_update_from_nonexistant_gem
update_result = run_inspec_process_with_this_plugin("plugin update inspec-test-fixture-nonesuch")
skip_windows!
assert_match(/No such plugin installed:.+ - update failed/, update_result.stdout)
assert_empty update_result.stderr
assert_exit_code 1, update_result
end
def test_fail_update_path
pre_block = Proc.new do |plugin_statefile_data, tmp_dir|
plugin_statefile_data.clear # Signal not to write a file, we'll provide one.
copy_in_core_config_dir("meaning_by_path", tmp_dir)
end
update_result = run_inspec_process_with_this_plugin("plugin update inspec-meaning-of-life", pre_run: pre_block)
refusal_message = update_result.stdout.split("\n").grep(/refusing/).last
skip_windows!
refute_nil refusal_message, "Should find a failure message at the end"
assert_includes refusal_message, "inspec-meaning-of-life"
assert_includes refusal_message, "inspec plugin uninstall"
assert_includes refusal_message, "Cannot update path-based install"
assert_empty update_result.stderr
assert_exit_code 2, update_result
end
end
#-----------------------------------------------------------------------------------------#
# inspec plugin uninstall
#-----------------------------------------------------------------------------------------#
class PluginManagerCliUninstall < Minitest::Test
include CorePluginFunctionalHelper
include PluginManagerHelpers
def test_when_a_gem_plugin_can_be_uninstalled
pre_block = Proc.new do |plugin_statefile_data, tmp_dir|
plugin_statefile_data.clear # Signal not to write a file, we'll provide one.
copy_in_core_config_dir("test-fixture-1-float", tmp_dir)
end
# Attempt uninstall
uninstall_result = run_inspec_process_with_this_plugin("plugin uninstall inspec-test-fixture", pre_run: pre_block, post_run: list_after_run)
success_message = uninstall_result.stdout.split("\n").grep(/uninstalled/).last
skip_windows!
refute_nil success_message, "Should find a success message at the end"
assert_includes success_message, "inspec-test-fixture"
assert_includes success_message, "0.1.0"
assert_includes success_message, "has been uninstalled"
list_result = uninstall_result.payload.list_result
itf_line = list_result.stdout.split("\n").grep(/inspec-test-fixture/).first
assert_nil itf_line, "inspec-test-fixture should not appear in the output of inspec list"
assert_empty uninstall_result.stderr
assert_exit_code 0, uninstall_result
end
def test_when_a_path_plugin_can_be_uninstalled
pre_block = Proc.new do |plugin_statefile_data, tmp_dir|
plugin_statefile_data.clear # Signal not to write a file, we'll provide one.
# This fixture includes a path install for inspec-meaning-of-life
copy_in_core_config_dir("test-fixture-1-float", tmp_dir)
end
uninstall_result = run_inspec_process_with_this_plugin("plugin uninstall inspec-meaning-of-life", pre_run: pre_block, post_run: list_after_run)
success_message = uninstall_result.stdout.split("\n").grep(/uninstalled/).last
skip_windows!
refute_nil success_message, "Should find a success message at the end"
assert_includes success_message, "inspec-meaning-of-life"
assert_includes success_message, "path-based plugin install"
assert_includes success_message, "has been uninstalled"
list_result = uninstall_result.payload.list_result
itf_line = list_result.stdout.split("\n").grep(/inspec-meaning-of-life/).first
assert_nil itf_line, "inspec-meaning-of-life should not appear in the output of inspec list"
assert_empty uninstall_result.stderr
assert_exit_code 0, uninstall_result
end
def test_fail_uninstall_from_plugin_that_is_not_installed
uninstall_result = run_inspec_process_with_this_plugin("plugin uninstall inspec-test-fixture-nonesuch")
skip_windows!
refute_includes "Inspec::Plugin::V2::UnInstallError", uninstall_result.stdout # Stacktrace marker
assert_match(/No such plugin installed:.+ - uninstall failed/, uninstall_result.stdout)
assert_empty uninstall_result.stderr
assert_exit_code 1, uninstall_result
end
end

View file

@ -0,0 +1,368 @@
require_relative "helper"
class PluginManagerCliInstall < Minitest::Test
include CorePluginFunctionalHelper # gives us instance methods, like `let` aliases inside test methods
extend CorePluginFunctionalHelper # gives us class methods, like `let` aliases out here outside test methods
parallelize_me!
include PluginManagerHelpers
ruby_abi_version = (Gem.ruby_version.segments[0, 2] << 0).join(".")
# Test multiple hueristics of the path-mode install.
# These are all positive tests; they should resolve the entry point to the same path in each case.
{
"is_perfect" => {
given: File.join(core_fixture_plugins_path, "inspec-test-fixture", "lib", "inspec-test-fixture.rb"),
},
"refers_to_the_entry_point_with_no_extension" => {
given: File.join(core_fixture_plugins_path, "inspec-test-fixture", "lib", "inspec-test-fixture"),
},
"refers_to_the_src_root_of_a_plugin" => {
given: File.join(core_fixture_plugins_path, "inspec-test-fixture"),
},
"refers_to_a_versioned_gem_install" => {
given: File.join(core_config_dir_path, "test-fixture-1-float", "gems", ruby_abi_version, "gems", "inspec-test-fixture-0.1.0", "lib", "inspec-test-fixture.rb"),
resolved_path: File.join(core_config_dir_path, "test-fixture-1-float", "gems", ruby_abi_version, "gems", "inspec-test-fixture-0.1.0", "lib", "inspec-test-fixture.rb"),
},
"refers_to_a_versioned_gem_install_missing_extension" => {
given: File.join(core_config_dir_path, "test-fixture-1-float", "gems", ruby_abi_version, "gems", "inspec-test-fixture-0.1.0", "lib", "inspec-test-fixture"),
resolved_path: File.join(core_config_dir_path, "test-fixture-1-float", "gems", ruby_abi_version, "gems", "inspec-test-fixture-0.1.0", "lib", "inspec-test-fixture.rb"),
},
"refers_to_a_relative_path" => {
given: File.join("test", "unit", "mock", "plugins", "inspec-test-fixture", "lib", "inspec-test-fixture.rb"),
},
"refers_to_a_train_plugin" => {
given: File.join(core_config_dir_path, "train-test-fixture", "gems", ruby_abi_version, "gems", "train-test-fixture-0.1.0", "lib", "train-test-fixture.rb"),
plugin_name: "train-test-fixture",
resolved_path: File.join(core_config_dir_path, "train-test-fixture", "gems", ruby_abi_version, "gems", "train-test-fixture-0.1.0", "lib", "train-test-fixture.rb"),
},
}.each do |test_name, fixture_info|
define_method(("test_install_from_path_when_path_" + test_name).to_sym) do
fixture_info = {
plugin_name: "inspec-test-fixture",
resolved_path: File.join(core_fixture_plugins_path, "inspec-test-fixture", "lib", "inspec-test-fixture.rb"),
}.merge(fixture_info)
install_result = run_inspec_process_with_this_plugin("plugin install #{fixture_info[:given]}", post_run: list_after_run)
# Check UX messaging
success_message = install_result.stdout.split("\n").grep(/installed/).last
skip_windows!
assert_empty install_result.stderr
refute_nil success_message, "Should find a success message at the end"
assert_includes success_message, fixture_info[:plugin_name]
assert_includes success_message, "plugin installed via source path reference"
# Check round-trip UX via list
itf_plugin = install_result.payload.list_result.detect { |p| p[:name] == fixture_info[:plugin_name] }
refute_nil itf_plugin, "plugin name should now appear in the output of inspec list"
assert_equal "path", itf_plugin[:type], "list output should show that it is a path installation"
# Check plugin statefile. Extra important in this case, since all should resolve to the same entry point.
plugin_data = install_result.payload.plugin_data
entry = plugin_data["plugins"].detect { |e| e["name"] == fixture_info[:plugin_name] }
assert_equal fixture_info[:resolved_path], entry["installation_path"], "Regardless of input, the entry point should be correct."
assert_exit_code 0, install_result
end
end
def test_fail_install_from_nonexistant_path
bad_path = File.join(project_fixtures_path, "none", "such", "inspec-test-fixture-nonesuch.rb")
install_result = run_inspec_process_with_this_plugin("plugin install #{bad_path}")
skip_windows!
error_message = install_result.stdout
assert_includes error_message, "No such source code path"
assert_includes error_message, "inspec-test-fixture-nonesuch.rb"
assert_includes error_message, "installation failed"
assert_empty install_result.stderr
assert_exit_code 1, install_result
end
def test_fail_install_from_path_with_wrong_name
bad_path = File.join(project_fixtures_path, "plugins", "wrong-name", "lib", "wrong-name.rb")
install_result = run_inspec_process_with_this_plugin("plugin install #{bad_path}")
skip_windows!
error_message = install_result.stdout
assert_includes error_message, "Invalid plugin name"
assert_includes error_message, "wrong-name"
assert_includes error_message, "All inspec plugins must begin with either 'inspec-' or 'train-'"
assert_includes error_message, "installation failed"
assert_empty install_result.stderr
assert_exit_code 1, install_result
end
def test_fail_install_from_path_when_it_is_not_a_plugin
bad_path = File.join(project_fixtures_path, "plugins", "inspec-egg-white-omelette", "lib", "inspec-egg-white-omelette.rb")
install_result = run_inspec_process_with_this_plugin("plugin install #{bad_path}")
skip_windows!
error_message = install_result.stdout
assert_includes error_message, "Does not appear to be a plugin"
assert_includes error_message, "inspec-egg-white-omelette"
assert_includes error_message, "After probe-loading the supposed plugin, it did not register"
assert_includes error_message, "Ensure something inherits from 'Inspec.plugin(2)'"
assert_includes error_message, "installation failed"
assert_empty install_result.stderr
assert_exit_code 1, install_result
end
def test_fail_install_from_path_when_it_is_already_installed
plugin_path = File.join(core_fixture_plugins_path, "inspec-test-fixture", "lib", "inspec-test-fixture.rb")
pre_block = Proc.new do |plugin_data, _tmp_dir|
plugin_data["plugins"] << {
"name" => "inspec-test-fixture",
"installation_type" => "path",
"installation_path" => plugin_path,
}
end
install_result = run_inspec_process_with_this_plugin("plugin install #{plugin_path}", pre_run: pre_block)
skip_windows!
error_message = install_result.stdout
assert_includes error_message, "Plugin already installed"
assert_includes error_message, "inspec-test-fixture"
assert_includes error_message, "Use 'inspec plugin list' to see previously installed plugin"
assert_includes error_message, "installation failed"
assert_empty install_result.stderr
assert_exit_code 2, install_result
end
def test_fail_install_from_path_when_the_dir_structure_is_wrong
bad_path = File.join(project_fixtures_path, "plugins", "inspec-wrong-structure")
install_result = run_inspec_process_with_this_plugin("plugin install #{bad_path}")
skip_windows!
error_message = install_result.stdout
assert_includes error_message, "Unrecognizable plugin structure"
assert_includes error_message, "inspec-wrong-structure"
assert_includes error_message, " When installing from a path, please provide the path of the entry point file"
assert_includes error_message, "installation failed"
assert_empty install_result.stderr
assert_exit_code 1, install_result
end
def test_install_from_gemfile
fixture_gemfile_path = File.join(core_fixture_plugins_path, "inspec-test-fixture", "pkg", "inspec-test-fixture-0.1.0.gem")
install_result = run_inspec_process_with_this_plugin("plugin install #{fixture_gemfile_path}", post_run: list_after_run)
skip_windows!
success_message = install_result.stdout.split("\n").grep(/installed/).last
refute_nil success_message, "Should find a success message at the end"
assert_includes success_message, "installed from local .gem file"
itf_plugin = install_result.payload.list_result.detect { |p| p[:name] == "inspec-test-fixture" }
refute_nil itf_plugin, "plugin name should now appear in the output of inspec list"
assert_equal "gem (user)", itf_plugin[:type]
assert_equal "0.1.0", itf_plugin[:version]
assert_empty install_result.stderr
assert_exit_code 0, install_result
end
def test_fail_install_from_nonexistant_gemfile
bad_path = File.join(project_fixtures_path, "none", "such", "inspec-test-fixture-nonesuch-0.3.0.gem")
install_result = run_inspec_process_with_this_plugin("plugin install #{bad_path}")
skip_windows!
assert_match(/No such plugin gem file .+ - installation failed./, install_result.stdout)
assert_empty install_result.stderr
assert_exit_code 1, install_result
end
def test_install_from_rubygems_latest
install_result = run_inspec_process_with_this_plugin("plugin install inspec-test-fixture", post_run: list_after_run)
skip_windows!
success_message = install_result.stdout.split("\n").grep(/installed/).last
refute_nil success_message, "Should find a success message at the end"
assert_includes success_message, "inspec-test-fixture"
assert_includes success_message, "0.2.0"
assert_includes success_message, "installed from rubygems.org"
itf_plugin = install_result.payload.list_result.detect { |p| p[:name] == "inspec-test-fixture" }
refute_nil itf_plugin, "plugin name should now appear in the output of inspec list"
assert_equal "gem (user)", itf_plugin[:type]
assert_equal "0.2.0", itf_plugin[:version]
assert_empty install_result.stderr
assert_exit_code 0, install_result
end
def test_fail_install_from_nonexistant_remote_rubygem
install_result = run_inspec_process_with_this_plugin("plugin install inspec-test-fixture-nonesuch")
skip_windows!
assert_match(/No such plugin gem .+ could be found on rubygems.org - installation failed./, install_result.stdout)
assert_empty install_result.stderr
assert_exit_code 1, install_result
end
def test_install_from_rubygems_with_pinned_version
install_result = run_inspec_process_with_this_plugin("plugin install inspec-test-fixture -v 0.1.0", post_run: list_after_run)
success_message = install_result.stdout.split("\n").grep(/installed/).last
skip_windows!
refute_nil success_message, "Should find a success message at the end"
assert_includes success_message, "inspec-test-fixture"
assert_includes success_message, "0.1.0"
assert_includes success_message, "installed from rubygems.org"
itf_plugin = install_result.payload.list_result.detect { |p| p[:name] == "inspec-test-fixture" }
refute_nil itf_plugin, "plugin name should now appear in the output of inspec list"
assert_equal "gem (user)", itf_plugin[:type]
assert_equal "0.1.0", itf_plugin[:version]
assert_empty install_result.stderr
assert_exit_code 0, install_result
end
def test_fail_install_from_nonexistant_rubygem_version
install_result = run_inspec_process_with_this_plugin("plugin install inspec-test-fixture -v 99.99.99")
fail_message = install_result.stdout.split("\n").grep(/failed/).last
skip_windows!
refute_nil fail_message, "Should find a failure message at the end"
assert_includes fail_message, "inspec-test-fixture"
assert_includes fail_message, "99.99.99"
assert_includes fail_message, "no such version"
assert_includes fail_message, "on rubygems.org"
assert_empty install_result.stderr
assert_exit_code 1, install_result
end
def test_refuse_install_when_missing_prefix
install_result = run_inspec_process_with_this_plugin("plugin install test-fixture")
fail_message = install_result.stdout.split("\n").grep(/failed/).last
skip_windows!
refute_nil fail_message, "Should find a failure message at the end"
assert_includes fail_message, "test-fixture"
assert_includes fail_message, "All inspec plugins must begin with either 'inspec-' or 'train-'"
assert_empty install_result.stderr
assert_exit_code 1, install_result
end
def test_refuse_install_when_already_installed_same_version
pre_block = Proc.new do |plugin_statefile_data, tmp_dir|
plugin_statefile_data.clear # Signal not to write a file, we'll provide one.
copy_in_core_config_dir("test-fixture-2-float", tmp_dir)
end
install_result = run_inspec_process_with_this_plugin("plugin install inspec-test-fixture", pre_run: pre_block)
refusal_message = install_result.stdout.split("\n").grep(/refusing/).last
skip_windows!
refute_nil refusal_message, "Should find a failure message at the end"
assert_includes refusal_message, "inspec-test-fixture"
assert_includes refusal_message, "0.2.0"
assert_includes refusal_message, "Plugin already installed at latest version"
assert_empty install_result.stderr
assert_exit_code 2, install_result
end
def test_refuse_install_when_already_installed_can_update
pre_block = Proc.new do |plugin_statefile_data, tmp_dir|
plugin_statefile_data.clear # Signal not to write a file, we'll provide one.
copy_in_core_config_dir("test-fixture-1-float", tmp_dir)
end
install_result = run_inspec_process_with_this_plugin("plugin install inspec-test-fixture", pre_run: pre_block)
refusal_message = install_result.stdout.split("\n").grep(/refusing/).last
skip_windows!
refute_nil refusal_message, "Should find a failure message at the end"
assert_includes refusal_message, "inspec-test-fixture"
assert_includes refusal_message, "0.1.0"
assert_includes refusal_message, "0.2.0"
assert_includes refusal_message, "Update required"
assert_includes refusal_message, "inspec plugin update"
assert_empty install_result.stderr
assert_exit_code 2, install_result
end
def test_install_from_rubygems_latest_with_train_plugin
install_result = run_inspec_process_with_this_plugin("plugin install train-test-fixture", post_run: list_after_run)
skip_windows!
success_message = install_result.stdout.split("\n").grep(/installed/).last
refute_nil success_message, "Should find a success message at the end"
assert_includes success_message, "train-test-fixture"
assert_includes success_message, "0.1.0"
assert_includes success_message, "installed from rubygems.org"
ttf_plugin = install_result.payload.list_result.detect { |p| p[:name] == "train-test-fixture" }
refute_nil ttf_plugin, "plugin name should now appear in the output of inspec list"
assert_equal "gem (user)", ttf_plugin[:type]
assert_equal "0.1.0", ttf_plugin[:version]
assert_empty install_result.stderr
assert_exit_code 0, install_result
end
def test_refuse_install_when_plugin_on_exclusion_list
# Here, 'inspec-core', 'inspec-multi-server', and 'train-tax-collector'
# are the names of real rubygems. They are not InSpec/Train plugins, though,
# and installing them would be a jam-up.
# This is configured in 'etc/plugin-filter.json'.
%w{
inspec-core
inspec-multi-server
train-tax-calculator
}.each do |plugin_name|
install_result = run_inspec_process_with_this_plugin("plugin install #{plugin_name}")
refusal_message = install_result.stdout
refute_nil refusal_message, "Should find a failure message at the end"
skip_windows!
assert_includes refusal_message, plugin_name
assert_includes refusal_message, "Plugin on Exclusion List"
assert_includes refusal_message, "refusing to install"
assert_includes refusal_message, "Rationale:"
assert_includes refusal_message, "etc/plugin_filters.json"
assert_includes refusal_message, "github.com/inspec/inspec/issues/new"
assert_empty install_result.stderr
assert_exit_code 2, install_result
end
end
def test_error_install_with_debug_enabled
skip "this test requires bundler to pass" unless defined? ::Bundler
install_result = run_inspec_process_with_this_plugin("plugin install inspec-test-fixture -v 0.1.1 --log-level debug")
skip_windows!
assert_includes install_result.stdout, "DEBUG"
assert_includes install_result.stderr, "can't activate rake"
assert_exit_code 1, install_result
end
end

View file

@ -0,0 +1,101 @@
require_relative "helper"
class PluginManagerCliList < Minitest::Test
include CorePluginFunctionalHelper
include PluginManagerHelpers
# Listing all plugins is now default behavior
LIST_CASES = [
{ arg: "-c", name: "inspec-plugin-manager-cli", type: "core" },
{ arg: "-c", name: "inspec-supermarket", type: "core" },
{ arg: "-s", name: "train-aws", type: "gem (system)" },
].freeze
def test_list_all_when_no_user_plugins_installed
result = run_inspec_process_with_this_plugin("plugin list --all")
skip_windows!
assert_empty result.stderr
plugins_seen = parse_plugin_list_lines(result.stdout)
# Look for a specific plugin of each type - core, bundle, and system
LIST_CASES.each do |test_case|
plugin_line = plugins_seen.detect { |plugin| plugin[:name] == test_case[:name] }
refute_nil plugin_line, "#{test_case[:name]} should be detected in plugin list --all output"
assert_equal test_case[:type], plugin_line[:type], "#{test_case[:name]} should be detected as a '#{test_case[:type]}' type in list --all "
end
assert_exit_code 0, result
end
def test_list_selective_when_no_user_plugins_installed
LIST_CASES.each do |test_case|
result = run_inspec_process_with_this_plugin("plugin list #{test_case[:arg]}")
skip_windows!
assert_empty result.stderr
plugins_seen = parse_plugin_list_lines(result.stdout)
plugin_line = plugins_seen.detect { |plugin| plugin[:name] == test_case[:name] }
refute_nil plugin_line, "#{test_case[:name]} should be detected in plugin list #{test_case[:arg]} output"
assert_equal plugin_line[:type], test_case[:type], "#{test_case[:name]} should be detected as a '#{test_case[:type]}' type in list #{test_case[:arg]} "
assert_exit_code 0, result
end
end
def test_list_when_gem_and_path_plugins_installed
pre_block = Proc.new do |plugin_statefile_data, tmp_dir|
plugin_statefile_data.clear # Signal not to write a file, we'll provide one.
copy_in_core_config_dir("test-fixture-1-float", tmp_dir)
end
result = run_inspec_process_with_this_plugin("plugin list --user ", pre_run: pre_block)
skip_windows!
assert_empty result.stderr
plugins_seen = parse_plugin_list_lines(result.stdout)
assert_equal 2, plugins_seen.count
# Plugin Name Version Via ApiVer
# ---------------------------------------------------------
# inspec-meaning-of-life src path 2
# inspec-test-fixture 0.1.0 gem (user) 2
# ---------------------------------------------------------
# 2 plugin(s) total
meaning = plugins_seen.detect { |p| p[:name] == "inspec-meaning-of-life" }
refute_nil meaning
assert_equal "path", meaning[:type]
fixture = plugins_seen.detect { |p| p[:name] == "inspec-test-fixture" }
refute_nil fixture
assert_equal "gem (user)", fixture[:type]
assert_equal "0.1.0", fixture[:version]
assert_exit_code 0, result
end
def test_list_when_a_train_plugin_is_installed
pre_block = Proc.new do |plugin_statefile_data, tmp_dir|
plugin_statefile_data.clear # Signal not to write a file, we'll provide one.
copy_in_core_config_dir("train-test-fixture", tmp_dir)
end
result = run_inspec_process_with_this_plugin("plugin list --user ", pre_run: pre_block)
skip_windows!
assert_empty result.stderr
plugins_seen = parse_plugin_list_lines(result.stdout)
assert_equal 1, plugins_seen.count
assert_includes result.stdout, "1 plugin(s) total", "list train should show one plugins"
# Plugin Name Version Via ApiVer
# -------------------------------------------------------------
# train-test-fixture 0.1.0 gem (user) train-1
# -------------------------------------------------------------
# 1 plugin(s) total
train_plugin = plugins_seen.detect { |p| p[:name] == "train-test-fixture" }
refute_nil train_plugin
assert_equal "gem (user)", train_plugin[:type]
assert_equal "train-1", train_plugin[:generation]
assert_equal "0.1.0", train_plugin[:version]
assert_exit_code 0, result
end
end

View file

@ -0,0 +1,129 @@
require_relative "helper"
class PluginManagerCliSearch < Minitest::Test
include CorePluginFunctionalHelper
include PluginManagerHelpers
parallelize_me!
# TODO: Thor can't hide options, but we wish it could.
# def test_search_include_fixture_hidden_option
# result = run_inspec_process_with_this_plugin('plugin help search')
# refute_includes result.stdout, '--include-test-fixture'
# end
def test_search_for_a_real_gem_with_full_name_no_options
result = run_inspec_process("plugin search --include-test-fixture inspec-test-fixture")
assert_includes result.stdout, "inspec-test-fixture", "Search result should contain the gem name"
assert_includes result.stdout, "1 plugin(s) found", "Search result should find 1 plugin"
line = result.stdout.split("\n").grep(/inspec-test-fixture/).first
assert_match(/\s*inspec-test-fixture\s+\((\d+\.\d+\.\d+){1}\)/, line, "Plugin line should include name and exactly one version")
assert_exit_code 0, result
end
def test_search_for_a_real_gem_with_stub_name_no_options
result = run_inspec_process("plugin search --include-test-fixture inspec-test-")
assert_includes result.stdout, "inspec-test-fixture", "Search result should contain the gem name"
assert_includes result.stdout, "1 plugin(s) found", "Search result should find 1 plugin"
line = result.stdout.split("\n").grep(/inspec-test-fixture/).first
assert_match(/\s*inspec-test-fixture\s+\((\d+\.\d+\.\d+){1}\)/, line, "Plugin line should include name and exactly one version")
assert_exit_code 0, result
end
def test_search_for_a_real_gem_with_full_name_and_exact_option
result = run_inspec_process("plugin search --exact --include-test-fixture inspec-test-fixture")
assert_includes result.stdout, "inspec-test-fixture", "Search result should contain the gem name"
assert_includes result.stdout, "1 plugin(s) found", "Search result should find 1 plugin"
assert_exit_code 0, result
# TODO: split
result = run_inspec_process("plugin search -e --include-test-fixture inspec-test-fixture")
assert_exit_code 0, result
end
def test_search_for_a_real_gem_with_stub_name_and_exact_option
result = run_inspec_process("plugin search --exact --include-test-fixture inspec-test-")
assert_includes result.stdout, "0 plugin(s) found", "Search result should find 0 plugins"
assert_exit_code 2, result
# TODO: split
result = run_inspec_process("plugin search -e --include-test-fixture inspec-test-")
assert_exit_code 2, result
end
def test_search_for_a_real_gem_with_full_name_and_all_option
result = run_inspec_process("plugin search --all --include-test-fixture inspec-test-fixture")
assert_includes result.stdout, "inspec-test-fixture", "Search result should contain the gem name"
assert_includes result.stdout, "1 plugin(s) found", "Search result should find 1 plugin"
line = result.stdout.split("\n").grep(/inspec-test-fixture/).first
assert_match(/\s*inspec-test-fixture\s+\((\d+\.\d+\.\d+(,\s)?){2,}\)/, line, "Plugin line should include name and at least two versions")
assert_exit_code 0, result
# TODO: split
result = run_inspec_process("plugin search -a --include-test-fixture inspec-test-fixture")
assert_exit_code 0, result
end
def test_search_for_a_gem_with_missing_prefix
result = run_inspec_process("plugin search --include-test-fixture test-fixture")
assert_exit_code 1, result
assert_includes result.stdout, "All inspec plugins must begin with either 'inspec-' or 'train-'"
end
def test_search_for_a_gem_that_does_not_exist
result = run_inspec_process("plugin search --include-test-fixture inspec-test-fixture-nonesuch")
assert_includes result.stdout, "0 plugin(s) found", "Search result should find 0 plugins"
assert_exit_code 2, result
end
def test_search_for_a_real_gem_with_full_name_no_options_and_train_name
result = run_inspec_process("plugin search --include-test-fixture train-test-fixture")
assert_includes result.stdout, "train-test-fixture", "Search result should contain the gem name"
assert_includes result.stdout, "1 plugin(s) found", "Search result should find 1 plugin"
line = result.stdout.split("\n").grep(/train-test-fixture/).first
assert_match(/\s*train-test-fixture\s+\((\d+\.\d+\.\d+){1}\)/, line, "Plugin line should include name and exactly one version")
assert_exit_code 0, result
end
def test_search_omit_excluded_inspec_plugins
result = run_inspec_process("plugin search --include-test-fixture inspec-")
assert_includes result.stdout, "inspec-test-fixture", "Search result should contain the test gem"
%w{
inspec-core
inspec-multi-server
}.each do |plugin_name|
refute_includes result.stdout, plugin_name, "Search result should not contain excluded gems"
end
assert_exit_code 0, result
end
def test_search_for_a_real_gem_with_full_name_no_options_filter_fixtures
result = run_inspec_process("plugin search inspec-test-fixture")
refute_includes result.stdout, "inspec-test-fixture", "Search result should not contain the fixture gem name"
end
def test_search_for_a_real_gem_with_full_name_no_options_filter_fixtures_train
result = run_inspec_process("plugin search train-test-fixture")
refute_includes result.stdout, "train-test-fixture", "Search result should not contain the fixture gem name"
end
end

View file

@ -0,0 +1,63 @@
require_relative "helper"
class PluginManagerCliUninstall < Minitest::Test
include CorePluginFunctionalHelper
include PluginManagerHelpers
def test_when_a_gem_plugin_can_be_uninstalled
pre_block = Proc.new do |plugin_statefile_data, tmp_dir|
plugin_statefile_data.clear # Signal not to write a file, we'll provide one.
copy_in_core_config_dir("test-fixture-1-float", tmp_dir)
end
# Attempt uninstall
uninstall_result = run_inspec_process_with_this_plugin("plugin uninstall inspec-test-fixture", pre_run: pre_block, post_run: list_after_run)
success_message = uninstall_result.stdout.split("\n").grep(/uninstalled/).last
skip_windows!
refute_nil success_message, "Should find a success message at the end"
assert_includes success_message, "inspec-test-fixture"
assert_includes success_message, "0.1.0"
assert_includes success_message, "has been uninstalled"
itf_plugins = uninstall_result.payload.list_result.select { |p| p[:name] == "inspec-test-fixture" }
assert_empty itf_plugins, "inspec-test-fixture should not appear in the output of inspec list"
assert_empty uninstall_result.stderr
assert_exit_code 0, uninstall_result
end
def test_when_a_path_plugin_can_be_uninstalled
pre_block = Proc.new do |plugin_statefile_data, tmp_dir|
plugin_statefile_data.clear # Signal not to write a file, we'll provide one.
# This fixture includes a path install for inspec-meaning-of-life
copy_in_core_config_dir("test-fixture-1-float", tmp_dir)
end
uninstall_result = run_inspec_process_with_this_plugin("plugin uninstall inspec-meaning-of-life", pre_run: pre_block, post_run: list_after_run)
skip_windows!
success_message = uninstall_result.stdout.split("\n").grep(/uninstalled/).last
refute_nil success_message, "Should find a success message at the end"
assert_includes success_message, "inspec-meaning-of-life"
assert_includes success_message, "path-based plugin install"
assert_includes success_message, "has been uninstalled"
itf_plugins = uninstall_result.payload.list_result.select { |p| p[:name] == "inspec-meaning-of-life" }
assert_empty itf_plugins, "inspec-meaning-of-life should not appear in the output of inspec list"
assert_empty uninstall_result.stderr
assert_exit_code 0, uninstall_result
end
def test_fail_uninstall_from_plugin_that_is_not_installed
uninstall_result = run_inspec_process_with_this_plugin("plugin uninstall inspec-test-fixture-nonesuch")
skip_windows!
refute_includes "Inspec::Plugin::V2::UnInstallError", uninstall_result.stdout # Stacktrace marker
assert_match(/No such plugin installed:.+ - uninstall failed/, uninstall_result.stdout)
assert_empty uninstall_result.stderr
assert_exit_code 1, uninstall_result
end
end

View file

@ -0,0 +1,84 @@
require_relative "helper"
class PluginManagerCliUpdate < Minitest::Test
include CorePluginFunctionalHelper
include PluginManagerHelpers
def test_when_a_plugin_can_be_updated
skip "this test requires bundler to pass" unless defined? ::Bundler
pre_block = Proc.new do |plugin_statefile_data, tmp_dir|
plugin_statefile_data.clear # Signal not to write a file, we'll provide one.
copy_in_core_config_dir("test-fixture-1-float", tmp_dir)
end
update_result = run_inspec_process_with_this_plugin("plugin update inspec-test-fixture", pre_run: pre_block, post_run: list_after_run)
success_message = update_result.stdout.split("\n").grep(/updated/).last
skip_windows!
refute_nil success_message, "Should find a success message at the end"
assert_includes success_message, "inspec-test-fixture"
assert_includes success_message, "0.1.0"
assert_includes success_message, "0.2.0"
assert_includes success_message, "updated from rubygems.org"
itf_plugin = update_result.payload.list_result.detect { |p| p[:name] == "inspec-test-fixture" }
refute_nil itf_plugin, "plugin name should now appear in the output of inspec list"
assert_equal "gem (user)", itf_plugin[:type]
assert_equal "0.2.0", itf_plugin[:version]
assert_empty update_result.stderr
assert_exit_code 0, update_result
end
def test_refuse_update_when_already_current
pre_block = Proc.new do |plugin_statefile_data, tmp_dir|
plugin_statefile_data.clear # Signal not to write a file, we'll provide one.
copy_in_core_config_dir("test-fixture-2-float", tmp_dir)
end
update_result = run_inspec_process_with_this_plugin("plugin update inspec-test-fixture", pre_run: pre_block)
refusal_message = update_result.stdout.split("\n").grep(/refusing/).last
skip_windows!
refute_nil refusal_message, "Should find a failure message at the end"
assert_includes refusal_message, "inspec-test-fixture"
assert_includes refusal_message, "0.2.0"
assert_includes refusal_message, "Already installed at latest version"
assert_empty update_result.stderr
assert_exit_code 2, update_result
end
def test_fail_update_from_nonexistant_gem
update_result = run_inspec_process_with_this_plugin("plugin update inspec-test-fixture-nonesuch")
skip_windows!
assert_match(/No such plugin installed:.+ - update failed/, update_result.stdout)
assert_empty update_result.stderr
assert_exit_code 1, update_result
end
def test_fail_update_path
pre_block = Proc.new do |plugin_statefile_data, tmp_dir|
plugin_statefile_data.clear # Signal not to write a file, we'll provide one.
copy_in_core_config_dir("meaning_by_path", tmp_dir)
end
update_result = run_inspec_process_with_this_plugin("plugin update inspec-meaning-of-life", pre_run: pre_block)
refusal_message = update_result.stdout.split("\n").grep(/refusing/).last
skip_windows!
refute_nil refusal_message, "Should find a failure message at the end"
assert_includes refusal_message, "inspec-meaning-of-life"
assert_includes refusal_message, "inspec plugin uninstall"
assert_includes refusal_message, "Cannot update path-based install"
assert_empty update_result.stderr
assert_exit_code 2, update_result
end
end

View file

@ -13,13 +13,15 @@ class PluginManagerCliOptions < Minitest::Test
def test_list_args
arg_config = cli_class.all_commands["list"].options
assert_equal 1, arg_config.count, "The list command should have 1 option"
assert_equal 4, arg_config.count, "The list command should have 4 options"
assert_includes arg_config.keys, :all, "The list command should have an --all option"
assert_equal :boolean, arg_config[:all].type, "The --all option should be boolean"
assert_equal :a, arg_config[:all].aliases.first, "The --all option should be aliased as -a"
refute_nil arg_config[:all].description, "The --all option should have a description"
refute arg_config[:all].required, "The --all option should not be required"
{ u: :user, a: :all, c: :core, s: :system }.each do |abbrev, option|
assert_includes arg_config.keys, option, "The list command should have an --#{option} option"
assert_equal :boolean, arg_config[option].type, "The --#{option} option should be boolean"
assert_equal abbrev, arg_config[option].aliases.first, "The --#{option} option should be aliased as -#{abbrev}"
refute_nil arg_config[option].description, "The --#{option} option should have a description"
refute arg_config[option].required, "The --#{option} option should not be required"
end
assert_equal 0, cli_class.instance_method(:list).arity, "The list command should take no arguments"
end

View file

@ -40,25 +40,27 @@ module SourceReaders
raise "Unable to parse #{metadata_source}: #{e.class} -- #{e.message}"
end
def find_all(regexp)
@target.files.grep(regexp)
end
def load_all(regexp)
find_all(regexp)
.map { |path| file = @target.read(path); [path, file] if file }
.compact
.to_h
end
def load_tests
tests = @target.files.find_all do |path|
path.start_with?("controls") && path.end_with?(".rb")
end
Hash[tests.map { |x| [x, @target.read(x)] }.delete_if { |_file, contents| contents.nil? }]
load_all(%r{^controls/.*\.rb$})
end
def load_libs
tests = @target.files.find_all do |path|
path.start_with?("libraries") && path.end_with?(".rb")
end
Hash[tests.map { |x| [x, @target.read(x)] }.delete_if { |_file, contents| contents.nil? }]
load_all(%r{^libraries/.*\.rb$})
end
def load_data_files
files = @target.files.find_all do |path|
path.start_with?("files" + File::SEPARATOR)
end
Hash[files.map { |x| [x, @target.read(x)] }.delete_if { |_file, contents| contents.nil? }]
load_all(%r{^files/})
end
end
end

View file

@ -74,7 +74,7 @@ package :msi do
fast_msi true
upgrade_code "DFCD452F-31E5-4236-ACD1-253F4720250B"
wix_light_extension "WixUtilExtension"
signing_identity "E05FF095D07F233B78EB322132BFF0F035E11B5B", machine_store: true
signing_identity "AF21BA8C9E50AE20DA9907B6E2D4B0CC3306CA03", machine_store: true
end
exclude "**/.git"

View file

@ -100,10 +100,16 @@ module FunctionalHelper
TMP_CACHE[res.path] = res
end
ROOT_LICENSE_PATH = "/etc/chef/accepted_licenses/inspec".freeze
def without_license
ENV.delete "CHEF_LICENSE"
FileUtils.rm_f ROOT_LICENSE_PATH
yield
FileUtils.rm_f ROOT_LICENSE_PATH
ensure
ENV["CHEF_LICENSE"] = "accept-no-persist"
end

View file

@ -1,4 +1,5 @@
require "functional/helper"
require "tempfile"
# For tests related to reading inputs from plugins, see plugins_test.rb
@ -6,6 +7,8 @@ describe "inputs" do
include FunctionalHelper
let(:inputs_profiles_path) { File.join(profile_path, "inputs") }
parallelize_me!
# This tests being able to load complex structures from
# cli option-specified files.
%w{
@ -42,6 +45,11 @@ describe "inputs" do
line = lines.detect { |l| l.include? "--attrs" }
line.wont_be_nil
end
it "includes the --input option" do
result = run_inspec_process("exec help", lock: true) # --no-create-lockfile option breaks usage help
assert_match(/--input\s/, result.stdout) # Careful not to match --input-file
end
end
describe "when using a cli-specified file" do
@ -67,6 +75,130 @@ describe "inputs" do
end
end
describe "when being passed inputs via the Runner API" do
let(:run_result) { run_runner_api_process(runner_options) }
let(:common_options) do
{
profile: "#{inputs_profiles_path}/via-runner",
reporter: ["json"],
}
end
# options:
# profile: path to profile to run
# All other opts passed to InSpec::Runner.new(...)
# then add.target is called
def run_runner_api_process(options)
# Remove profile from options. All other are passed to Runner.
profile = options.delete(:profile)
# Make a tmpfile
Tempfile.open(mode: 0700) do |script| # 0700 - -rwx------
# Clear and concat - can't just assign, it's readonly
script.puts <<~EOSCRIPT
# Ruby load path
$LOAD_PATH.clear
$LOAD_PATH.concat(#{$LOAD_PATH})
# require inspec
require "inspec"
require "inspec/runner"
# inject pretty-printed runner opts
runner_args = #{options.inspect}
# Profile to run:
profile_location = "#{profile}"
# Run Execution
runner = Inspec::Runner.new(runner_args)
runner.add_target profile_location
runner.run
EOSCRIPT
script.flush
# TODO - portability - this does not have windows compat stuff from the inspec()
# method in functional/helper.rb - it is not portable to windows at this point yet.
# https://github.com/inspec/inspec/issues/4416
CMD.run_command("ruby #{script.path}")
end
end
describe "when using the current :inputs key" do
let(:runner_options) { common_options.merge({ inputs: { test_input_01: "value_from_api" } }) }
it "finds the values and does not issue any warnings" do
output = run_result.stdout
skip_windows!
refute_includes output, "DEPRECATION"
structured_output = JSON.parse(output)
assert_equal "passed", structured_output["profiles"][0]["controls"][0]["results"][0]["status"]
end
end
describe "when using the legacy :attributes key" do
let(:runner_options) { common_options.merge({ attributes: { test_input_01: "value_from_api" } }) }
it "finds the values but issues a DEPRECATION warning" do
output = run_result.stdout
skip_windows!
assert_includes output, "DEPRECATION"
structured_output = JSON.parse(output.lines.reject { |l| l.include? "DEPRECATION" }.join("\n") )
assert_equal "passed", structured_output["profiles"][0]["controls"][0]["results"][0]["status"]
end
end
end
describe "when using the --input inline raw input flag CLI option" do
let(:result) { run_inspec_process("exec #{inputs_profiles_path}/cli #{input_opt} #{control_opt}", json: true) }
let(:control_opt) { "" }
describe "when the --input is used once with one value" do
let(:input_opt) { "--input test_input_01=value_from_cli_01" }
let(:control_opt) { "--controls test_control_01" }
it("correctly reads the input") { result.must_have_all_controls_passing }
end
describe "when the --input is used once with two values" do
let(:input_opt) { "--input test_input_01=value_from_cli_01 test_input_02=value_from_cli_02" }
it("correctly reads the input") { result.must_have_all_controls_passing }
end
describe "when the --input is used once with two values and a comma" do
let(:input_opt) { "--input test_input_01=value_from_cli_01, test_input_02=value_from_cli_02" }
it("correctly reads the input") { result.must_have_all_controls_passing }
end
describe "when the --input is used twice with one value each" do
let(:input_opt) { "--input test_input_01=value_from_cli_01 --input test_input_02=value_from_cli_02" }
let(:control_opt) { "--controls test_control_02" }
# Expected, though unfortunate, behavior is to only notice the second input
it("correctly reads the input") { result.must_have_all_controls_passing }
end
describe "when the --input is used with no equal sign" do
let(:input_opt) { "--input value_from_cli_01" }
it "does not run and provides an error message" do
output = result.stdout
assert_includes "ERROR", output
assert_includes "An '=' is required", output
assert_includes "input_name_1=input_value_1", output
assert_equal 1, result.exit_status
end
end
describe "when the --input is used with a .yaml extension" do
let(:input_opt) { "--input myfile.yaml" }
it "does not run and provides an error message" do
output = result.stdout
assert_includes "ERROR", output
assert_includes "individual input values", output
assert_includes "Use --input-file", output
assert_equal 1, result.exit_status
end
end
end
describe "when accessing inputs in a variety of scopes using the DSL" do
it "is able to read the inputs using the input keyword" do
cmd = "exec #{inputs_profiles_path}/scoping"

View file

@ -7,6 +7,8 @@ describe "inspec archive" do
include FunctionalHelper
let(:auto_dst) { File.expand_path(File.join(repo_path, "profile-1.0.0.tar.gz")) }
parallelize_me!
it "archive is successful" do
prepare_examples("profile") do |dir|
out = inspec("archive " + dir + " --overwrite")

View file

@ -4,6 +4,8 @@ require "tmpdir"
describe "inspec check" do
include FunctionalHelper
parallelize_me!
describe "inspec check with json formatter" do
it "can check a profile and produce valid JSON" do
out = inspec("check " + example_profile + " --format json")
@ -106,4 +108,13 @@ describe "inspec check" do
assert_exit_code 1, out
end
end
describe "inspec check with unsatisfied runtime version constraint" do
it "should enforce runtime version constraint" do
out = inspec("check #{profile_path}/unsupported_inspec")
out.stdout.must_include "The current inspec version #{Inspec::VERSION}"
out.stdout.must_include ">= 99.0.0"
assert_exit_code 1, out
end
end
end

View file

@ -4,6 +4,8 @@ require "json-schema"
describe "inspec exec with json formatter" do
include FunctionalHelper
parallelize_me!
it "can execute a simple file and validate the json schema" do
out = inspec("exec " + example_control + " --reporter json --no-create-lockfile")

View file

@ -4,6 +4,8 @@ require "json-schema"
describe "inspec exec" do
include FunctionalHelper
parallelize_me!
it "can execute a profile with the mini json formatter and validate its schema" do
out = inspec("exec " + example_profile + " --reporter json-min --no-create-lockfile")
data = JSON.parse(out.stdout)

View file

@ -4,9 +4,12 @@ require "rexml/document"
describe "inspec exec with junit formatter" do
include FunctionalHelper
parallelize_me!
it "can execute a simple file with the junit formatter" do
out = inspec("exec " + example_control + " --reporter junit --no-create-lockfile")
# TODO: rexml is about as slow as you can go. Use nokogiri
doc = REXML::Document.new(out.stdout)
doc.has_elements?.must_equal true

View file

@ -1,6 +1,8 @@
require "functional/helper"
describe "inspec exec" do
parallelize_me!
include FunctionalHelper
let(:looks_like_a_stacktrace) { %r{lib/inspec/.+\.rb:\d+:in} }
@ -27,9 +29,8 @@ describe "inspec exec" do
# TODO: I do not know how to test this more directly. It should be possible.
inspec "exec -t aws:// #{profile_path}/incompatible_resource_for_transport.rb"
stdout.must_include "Bad File on TrainPlugins::Aws::Connection"
stdout.must_include "Resource `file` is not supported on platform aws/train-aws"
stderr.must_equal ""
stdout.must_be_empty
stderr.must_include "Unsupported resource/backend combination: file / aws. Exiting."
end
it "can execute the profile" do
@ -297,10 +298,9 @@ Test Summary: 0 successful, 0 failures, 0 skipped
let(:out) { inspec("exec " + File.join(profile_path, "aws-profile")) }
it "exits with an error" do
skip if ENV["NO_AWS"]
stdout.must_include "Resource `aws_iam_users` is not supported on platform"
stdout.must_include "Resource `aws_iam_access_keys` is not supported on platform"
stdout.must_include "Resource `aws_s3_bucket` is not supported on platform"
stdout.must_include "Unsupported resource/backend combination: aws_iam_users"
stdout.must_include "Unsupported resource/backend combination: aws_iam_access_keys"
stdout.must_include "Unsupported resource/backend combination: aws_s3_bucket"
stdout.must_include "3 failures"
assert_exit_code 100, out

View file

@ -4,6 +4,8 @@ require "mixlib/shellout"
describe "inspec json" do
include FunctionalHelper
parallelize_me!
it "read the profile json" do
out = inspec("json " + example_profile)
out.stderr.must_equal ""

View file

@ -3,6 +3,8 @@ require "functional/helper"
describe "inspec shell tests" do
include FunctionalHelper
parallelize_me!
describe "cmd" do
def assert_shell_c(code, exit_status, json = false, stderr = "")
json_suffix = " --reporter 'json'" if json

View file

@ -4,6 +4,8 @@ require "tmpdir"
describe "example inheritance profile" do
include FunctionalHelper
parallelize_me!
it "can vendor profile dependencies" do
prepare_examples("inheritance") do |dir|
out = inspec("vendor " + dir + " --overwrite")

Some files were not shown because too many files have changed in this diff Show more