mirror of
https://github.com/inspec/inspec
synced 2024-11-10 23:24:18 +00:00
Merge remote-tracking branch 'upstream/master' into add-startuser-systemd-service
Signed-off-by: Mendy Baitelman <mendy@baitelman.com>
This commit is contained in:
commit
d44f9edc55
151 changed files with 3803 additions and 2327 deletions
|
@ -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:-}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
11
.expeditor/templates/pull_request.mustache
Normal file
11
.expeditor/templates/pull_request.mustache
Normal 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!
|
|
@ -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
|
||||
|
|
|
@ -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
7
.github/CODEOWNERS
vendored
|
@ -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
1
.gitignore
vendored
|
@ -1,4 +1,5 @@
|
|||
*.gem
|
||||
*.swp
|
||||
.attribute.yml
|
||||
.bundle
|
||||
.delivery/cli.toml
|
||||
|
|
88
CHANGELOG.md
88
CHANGELOG.md
|
@ -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 -> 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'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'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'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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
14
README.md
14
README.md
|
@ -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
|
||||
|
||||
|
|
83
Rakefile
83
Rakefile
|
@ -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]
|
||||
|
|
2
VERSION
2
VERSION
|
@ -1 +1 @@
|
|||
4.10.4
|
||||
4.17.5
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
76
docs/resources/postfix_conf.md.erb
Normal file
76
docs/resources/postfix_conf.md.erb
Normal 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/).
|
|
@ -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:
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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."
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -28,4 +28,3 @@ require "inspec/base_cli"
|
|||
require "inspec/fetcher"
|
||||
require "inspec/source_reader"
|
||||
require "inspec/resource"
|
||||
require "inspec/resources"
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
#-----------------------------------------------------------------------#
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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
410
lib/inspec/input.rb
Normal 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
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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?
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -15,7 +15,7 @@ module Inspec
|
|||
end
|
||||
|
||||
def to_ruby
|
||||
"tag #{key.inspect}: #{value.inspect}"
|
||||
"tag #{key}: #{value.inspect}"
|
||||
end
|
||||
|
||||
def to_s
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 =
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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
|
||||
|
|
35
lib/inspec/resources/postfix_conf.rb
Normal file
35
lib/inspec/resources/postfix_conf.rb
Normal 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
|
|
@ -47,6 +47,10 @@ module Inspec::Resources
|
|||
@sids.key?(@name)
|
||||
end
|
||||
|
||||
def to_s
|
||||
"Security Identifier"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def fetch_sids
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
1
lib/inspec/resources/user.rb
Normal file
1
lib/inspec/resources/user.rb
Normal file
|
@ -0,0 +1 @@
|
|||
require "inspec/resources/users"
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
require "inspec/objects/input"
|
||||
require "inspec/input"
|
||||
|
||||
module PkeyReader
|
||||
def read_pkey(filecontent, passphrase)
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
module Inspec
|
||||
VERSION = "4.10.4".freeze
|
||||
VERSION = "4.17.5".freeze
|
||||
end
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 ""
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
Loading…
Reference in a new issue