Merge pull request #6601 from inspec/inspec-6

Make InSpec 6 the main branch
This commit is contained in:
Clinton Wolfe 2023-08-17 11:59:41 -04:00 committed by GitHub
commit 5a4437a201
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
82 changed files with 2950 additions and 798 deletions

View file

@ -4,6 +4,67 @@ Get-CimInstance Win32_OperatingSystem | Select-Object $Properties | Format-Table
ruby -v
bundle --version
echo "--- download and install vault"
# Define the version of Vault to install
$VaultVersion = "1.13.0"
# Define the installation directory for Vault
$VaultDirectory = "$env:USERPROFILE\Vault"
# Create the installation directory if it doesn't exist
if (!(Test-Path $VaultDirectory))
{
New-Item -ItemType Directory -Path $VaultDirectory | Out-Null
}
$VaultDownloadUrl = "https://releases.hashicorp.com/vault/$VaultVersion/vault_${VaultVersion}_windows_amd64.zip"
$VaultZipFilePath = Join-Path $VaultDirectory "vault.zip"
Invoke-WebRequest -Uri $VaultDownloadUrl -OutFile $VaultZipFilePath
# Extract the Vault binary from the zip file
$VaultExtractPath = Join-Path $VaultDirectory "vault"
Expand-Archive -Path $VaultZipFilePath -DestinationPath $VaultExtractPath
# Add the Vault binary to the system PATH environment variable
$env:Path += ";$VaultExtractPath"
# Verify the installation
echo "--- vault version installed is:"
vault version
echo "--- fetching License serverl url and keys from vault"
$Env:CHEF_LICENSE_SERVER=vault kv get -field acceptance secret/inspec/licensing/server
$Env:CHEF_LICENSE_KEY=vault kv get -field acceptance secret/inspec/licensing/license-key
echo "--- verifying if environment variables are set"
function CheckIfEnvVarIsSet {
param (
[string]$envVarName
)
if (Test-Path "env:\$envVarName") {
Write-Host " ++ $envVarName set successfully"
} else {
Write-Host " !! $envVarName is not set."
}
}
$envVarName = "CHEF_LICENSE_SERVER"
CheckIfEnvVarIsSet -envVarName $envVarName
$envVarName = "CHEF_LICENSE_SERVER_API_KEY"
CheckIfEnvVarIsSet -envVarName $envVarName
$envVarName = "CHEF_LICENSE_KEY"
CheckIfEnvVarIsSet -envVarName $envVarName
if ($Env:CI_ENABLE_COVERAGE)
{
echo "--- fetching Sonar token from vault"
$Env:SONAR_TOKEN=vault kv get -field token secret/inspec/sonar
}
echo "--- bundle install"
bundle config set --local without deploy kitchen
bundle install --jobs=7 --retry=3

View file

@ -20,14 +20,25 @@ mount
df /tmp
echo ${TMPDIR:-unknown}
if [ -n "${CI_ENABLE_COVERAGE:-}" ]; then
# Fetch token from vault ASAP so that long-running tests don't cause our vault token to expire
echo "--- installing vault"
export VAULT_VERSION=1.9.3
export VAULT_HOME=$HOME/vault
curl --create-dirs -sSLo $VAULT_HOME/vault.zip https://releases.hashicorp.com/vault/$VAULT_VERSION/vault_${VAULT_VERSION}_linux_amd64.zip
unzip -o $VAULT_HOME/vault.zip -d $VAULT_HOME
# Fetch tokens from vault ASAP so that long-running tests don't cause our vault token to expire
echo "--- installing vault"
export VAULT_VERSION=1.13.0
export VAULT_HOME=$HOME/vault
curl --create-dirs -sSLo $VAULT_HOME/vault.zip https://releases.hashicorp.com/vault/$VAULT_VERSION/vault_${VAULT_VERSION}_linux_amd64.zip
unzip -o $VAULT_HOME/vault.zip -d $VAULT_HOME
echo "--- fetching License serverl url and keys from vault"
export CHEF_LICENSE_SERVER=$($VAULT_HOME/vault kv get -field acceptance secret/inspec/licensing/server)
export CHEF_LICENSE_KEY=$($VAULT_HOME/vault kv get -field acceptance secret/inspec/licensing/license-key)
if [ -n "${CHEF_LICENSE_KEY:-}" ]; then
echo " ++ License Key set successfully"
else
echo " !! License Key not set - exiting "
exit 1
fi
if [ -n "${CI_ENABLE_COVERAGE:-}" ]; then
echo "--- fetching Sonar token from vault"
export SONAR_TOKEN=$($VAULT_HOME/vault kv get -field token secret/inspec/sonar)
fi

View file

@ -3,25 +3,23 @@
product_key: inspec
rubygems:
- inspec
- inspec-core
- inspec-bin:
gemspec_path: ./inspec-bin/
- inspec-core-bin:
gemspec_path: ./inspec-bin/
pipelines:
- habitat/build:
env:
- HAB_NONINTERACTIVE: "true"
- HAB_NOCOLORING: "true"
- HAB_STUDIO_SECRET_HAB_NONINTERACTIVE: "true"
- docker/build
- verify:
definition: .expeditor/verify_public_dummy.pipeline.yml
description: Keeping the verify pipeline alive to preserve the history
public: true
- verify/private:
definition: .expeditor/verify_private.pipeline.yml
public: false
description: Pull Request validation tests
env:
- LANG: "C.UTF-8"
- SLOW: 1
- NO_AWS: 1
- MT_CPU: 5
- omnibus/release:
env:
# The git cache is corrupt more often than not. This always purges the cache.
# https://chefio.atlassian.net/wiki/spaces/RELENGKB/pages/2204336129/Resolving+git+cache+build+errors+in+Omnibus
- EXPIRE_CACHE: 1
- IGNORE_ARTIFACTORY_RUBY_PROXY: true # Artifactory is throwing 500's when downloading some gems like ffi.
- omnibus/adhoc:
@ -29,41 +27,7 @@ pipelines:
env:
- ADHOC: true
- EXPIRE_CACHE: 1
- verify:
description: Pull Request validation tests
public: true
env:
- LANG: "C.UTF-8"
- SLOW: 1
- NO_AWS: 1
- MT_CPU: 5
- coverage:
description: Unit test coverage
# Private due to use of tokens
trigger: pull_request
env:
- LANG: "C.UTF-8"
- SLOW: 1
- NO_AWS: 1
- MT_CPU: 5
# This has been disabled because it regularly hits Docker API rate limits and fails
# - integration/resources:
# description: Test core resources with test-kitchen.
# definition: .expeditor/integration.resources.yml
# trigger: pull_request
- artifact/habitat:
description: Execute tests against the habitat artifact
definition: .expeditor/artifact.habitat.yml
env:
- HAB_NONINTERACTIVE: "true"
- HAB_NOCOLORING: "true"
- HAB_STUDIO_SECRET_HAB_NONINTERACTIVE: "true"
trigger: pull_request
schedules:
- name: integration_schedule
description: Periodic Integration Testing
cronline: "0 8 * * *"
- IGNORE_ARTIFACTORY_RUBY_PROXY: true # Artifactory is throwing 500's when downloading some gems like ffi.
slack:
notify_channel: inspec-notify
@ -79,8 +43,6 @@ github:
release_branches:
- main:
version_constraint: 99.*
- inspec-6:
version_constraint: 6.*
- inspec-5:
version_constraint: 5.*
@ -122,56 +84,11 @@ subscriptions:
ignore_labels:
- "Expeditor: Skip All"
- "Expeditor: Skip Changelog"
- trigger_pipeline:omnibus/adhoc:
not_if: built_in:bump_version
ignore_labels:
- "Expeditor: Skip Omnibus"
- "Expeditor: Skip All"
- trigger_pipeline:artifact/habitat:
only_if: built_in:bump_version
ignore_labels:
- "Expeditor: Skip Habitat"
- "Expeditor: Skip All"
- trigger_pipeline:omnibus/release:
only_if: built_in:bump_version
ignore_labels:
- "Expeditor: Skip Omnibus"
- "Expeditor: Skip All"
- trigger_pipeline:habitat/build:
only_if: built_in:bump_version
ignore_labels:
- "Expeditor: Skip Habitat"
- "Expeditor: Skip All"
- built_in:build_gem:
only_if:
- built_in:bump_version
- workload: artifact_published:unstable:inspec:{{version_constraint}}
actions:
- trigger_pipeline:docker/build
- bash:.expeditor/buildkite/wwwrelease.sh:
post_commit: true
- workload: artifact_published:current:inspec:{{version_constraint}}
actions:
- built_in:promote_docker_images
- built_in:promote_habitat_packages
- workload: project_promoted:{{agent_id}}:*
actions:
- built_in:promote_artifactory_artifact
- workload: artifact_published:stable:inspec:{{version_constraint}}
actions:
- bash:.expeditor/update_dockerfile.sh
- built_in:rollover_changelog
- built_in:publish_rubygems
- built_in:create_github_release
- built_in:promote_docker_images
- built_in:promote_habitat_packages
- bash:.expeditor/publish-release-notes.sh:
post_commit: true
- purge_packages_chef_io_fastly:{{target_channel}}/inspec/latest:
post_commit: true
- bash:.expeditor/announce-release.sh:
post_commit: true
- built_in:notify_chefio_slack_channels
- workload: pull_request_opened:{{github_repo}}:{{release_branch}}:*
actions:
- post_github_comment:.expeditor/templates/pull_request.mustache:

View file

@ -13,22 +13,16 @@ steps:
command:
- RAKE_TASK=test:lint /workdir/.expeditor/buildkite/verify.sh
expeditor:
secrets: true
executor:
docker:
image: ruby:3.0
- label: run-tests-ruby-2.7
command:
- /workdir/.expeditor/buildkite/verify.sh
expeditor:
executor:
docker:
image: ruby:2.7
- label: run-tests-ruby-3.0
command:
- /workdir/.expeditor/buildkite/verify.sh
expeditor:
secrets: true
executor:
docker:
image: ruby:3.0
@ -37,6 +31,7 @@ steps:
command:
- /workdir/.expeditor/buildkite/verify.sh
expeditor:
secrets: true
executor:
docker:
image: ruby:3.1
@ -45,6 +40,7 @@ steps:
command:
- RAKE_TASK=test:isolated /workdir/.expeditor/buildkite/verify.sh
expeditor:
secrets: true
executor:
docker:
image: ruby:3.0
@ -53,6 +49,7 @@ steps:
command:
- RAKE_TASK=test:isolated /workdir/.expeditor/buildkite/verify.sh
expeditor:
secrets: true
executor:
docker:
image: ruby:3.1
@ -61,6 +58,7 @@ steps:
command:
- /workdir/.expeditor/buildkite/verify.ps1
expeditor:
secrets: true
executor:
docker:
environment:

View file

@ -0,0 +1,13 @@
# This pipeline is a dummy pipeline that does nothing. It exists simply
# because removing the config from expeditor
# will delete the pipeline, and we want to preserve the
# pipeline history.
# TODO: Simplify the pipeline in future
steps:
- label: "keeping-verify-pipeline-alive"
expeditor:
executor:
docker:
commands:
- "echo ## This pipeline does nothing. The actual verify pipeline is verify/private. This exists as a placeholder to prevent deletion of the historical main verify pipeline."

View file

@ -35,17 +35,4 @@ updates:
directory: "/omnibus"
schedule:
interval: daily
open-pull-requests-limit: 10
- package-ecosystem: bundler
target-branch: "inspec-6"
directory: "/"
schedule:
interval: daily
open-pull-requests-limit: 10
- package-ecosystem: bundler
target-branch: "inspec-6"
directory: "/omnibus"
schedule:
interval: daily
open-pull-requests-limit: 10
open-pull-requests-limit: 10

View file

@ -1,17 +1,132 @@
# Change Log
<!-- usage documentation: http://expeditor-docs.es.chef.io/configuration/changelog/ -->
<!-- latest_release 5.22.13 -->
<!-- latest_release 6.4.45 -->
## [v6.4.45](https://github.com/inspec/inspec/tree/v6.4.45) (2023-08-17)
#### Merged Pull Requests
- Bump omnibus-software from `4b08f0b` to `3268356` in /omnibus [#6587](https://github.com/inspec/inspec/pull/6587) ([dependabot[bot]](https://github.com/dependabot[bot]))
<!-- latest_release -->
<!-- release_rollup since=5.22.13 -->
### Changes since 5.22.13 release
#### Merged Pull Requests
- Bump omnibus-software from `4b08f0b` to `3268356` in /omnibus [#6587](https://github.com/inspec/inspec/pull/6587) ([dependabot[bot]](https://github.com/dependabot[bot])) <!-- 6.4.45 -->
- Missing changes from main [#6564](https://github.com/inspec/inspec/pull/6564) ([sathish-progress](https://github.com/sathish-progress)) <!-- 6.4.44 -->
- CHEF-4010 make a clean exit for License list command [#6552](https://github.com/inspec/inspec/pull/6552) ([sathish-progress](https://github.com/sathish-progress)) <!-- 6.4.43 -->
- CHEF-4818 revise inspec parallel docs content [#6586](https://github.com/inspec/inspec/pull/6586) ([IanMadd](https://github.com/IanMadd)) <!-- 6.4.42 -->
- CHEF-3916 Add section on inspec license subcommand to online docs [#6583](https://github.com/inspec/inspec/pull/6583) ([ahasunos](https://github.com/ahasunos)) <!-- 6.4.41 -->
- Revert omnibus-software bump (6576) - ffi-yajl issue [#6585](https://github.com/inspec/inspec/pull/6585) ([clintoncwolfe](https://github.com/clintoncwolfe)) <!-- 6.4.40 -->
- Disable git caching in omnibus builds [#6584](https://github.com/inspec/inspec/pull/6584) ([clintoncwolfe](https://github.com/clintoncwolfe)) <!-- 6.4.39 -->
- CHEF-4559 Extended support to inspec parallel for reporters using config [#6578](https://github.com/inspec/inspec/pull/6578) ([Nik08](https://github.com/Nik08)) <!-- 6.4.38 -->
- Bump omnibus-software from `4b08f0b` to `3268356` in /omnibus [#6576](https://github.com/inspec/inspec/pull/6576) ([dependabot[bot]](https://github.com/dependabot[bot])) <!-- 6.4.37 -->
- Foreport 6568 [#6579](https://github.com/inspec/inspec/pull/6579) ([clintoncwolfe](https://github.com/clintoncwolfe)) <!-- 6.4.36 -->
- CHEF-4080: Point to latest EULA in GUI installers [#6580](https://github.com/inspec/inspec/pull/6580) ([ahasunos](https://github.com/ahasunos)) <!-- 6.4.35 -->
- CHEF-3832: Fix for InSpec Parallel fails to fetch remote profiles due to cache contention. [#6546](https://github.com/inspec/inspec/pull/6546) ([Vasu1105](https://github.com/Vasu1105)) <!-- 6.4.34 -->
- Add chef-licensing, syncing from inspec-prime repo inspec-6 branch [#6559](https://github.com/inspec/inspec/pull/6559) ([clintoncwolfe](https://github.com/clintoncwolfe)) <!-- 6.4.33 -->
- inspec-6 CI - Add secrets: true to private verify pipeline, delete ruby 2.7 config [#6558](https://github.com/inspec/inspec/pull/6558) ([clintoncwolfe](https://github.com/clintoncwolfe))
- forcing private in the configuration file [#6556](https://github.com/inspec/inspec/pull/6556) ([sean-simmons-progress](https://github.com/sean-simmons-progress))
- Adds test for licensing_config [#57](https://github.com/inspec/inspec-prime/pull/57) ([Vasu1105](https://github.com/Vasu1105)) <!-- 6.4.33 -->
- Configure to use `Inspec::Log` in Chef Licensing [#67](https://github.com/inspec/inspec-prime/pull/67) ([ahasunos](https://github.com/ahasunos)) <!-- 6.4.32 -->
- Crossport public 6549: Drop testing on EOL ruby 2.7, and run linter on Ruby 3.1 [#76](https://github.com/inspec/inspec-prime/pull/76) ([clintoncwolfe](https://github.com/clintoncwolfe)) <!-- 6.4.31 -->
- Case correction of product name in licensing config [#78](https://github.com/inspec/inspec-prime/pull/78) ([ahasunos](https://github.com/ahasunos)) <!-- 6.4.30 -->
- Foreport - Add postgres support for custom port with a socket connection [#40](https://github.com/inspec/inspec-prime/pull/40) ([clintoncwolfe](https://github.com/clintoncwolfe)) <!-- 6.4.29 -->
- Bump omnibus-software from `88169e3` to `4b08f0b` in /omnibus [#73](https://github.com/inspec/inspec-prime/pull/73) ([dependabot[bot]](https://github.com/dependabot[bot])) <!-- 6.4.28 -->
- CHEF-3759 Crossport public 6540 Fix for inspec parallel on windows crashing due to error log rename [#74](https://github.com/inspec/inspec-prime/pull/74) ([clintoncwolfe](https://github.com/clintoncwolfe)) <!-- 6.4.27 -->
- Foreports #6526 and #6541: Update Docker base image to be ubuntu 22.04 [#64](https://github.com/inspec/inspec-prime/pull/64) ([ahasunos](https://github.com/ahasunos)) <!-- 6.4.26 -->
- Crossport Public 6545 Fix for InSpec Parallel hangs on certain CIS profiles [#71](https://github.com/inspec/inspec-prime/pull/71) ([clintoncwolfe](https://github.com/clintoncwolfe)) <!-- 6.4.26 -->
- Bump omnibus from `15122f2` to `9c0643a` in /omnibus [#70](https://github.com/inspec/inspec-prime/pull/70) ([dependabot[bot]](https://github.com/dependabot[bot])) <!-- 6.4.25 -->
- Bump berkshelf from 8.0.2 to 8.0.7 in /omnibus [#63](https://github.com/inspec/inspec-prime/pull/63) ([dependabot[bot]](https://github.com/dependabot[bot])) <!-- 6.4.24 -->
- Foreport #6523: Update RSpec to 3.12 [#65](https://github.com/inspec/inspec-prime/pull/65) ([ahasunos](https://github.com/ahasunos)) <!-- 6.4.23 -->
- Bump omnibus from `cf97613` to `15122f2` in /omnibus [#62](https://github.com/inspec/inspec-prime/pull/62) ([dependabot[bot]](https://github.com/dependabot[bot])) <!-- 6.4.22 -->
- Bump omnibus-software from `225e357` to `88169e3` in /omnibus [#61](https://github.com/inspec/inspec-prime/pull/61) ([dependabot[bot]](https://github.com/dependabot[bot])) <!-- 6.4.21 -->
- CHEF-3704 Modify help for local licensing service mode and other distros [#59](https://github.com/inspec/inspec-prime/pull/59) ([Nik08](https://github.com/Nik08)) <!-- 6.4.20 -->
- restrict license commands only to inspec distro [#58](https://github.com/inspec/inspec-prime/pull/58) ([sathish-progress](https://github.com/sathish-progress)) <!-- 6.4.19 -->
- CHEF-3184 Error handling for inspec license add command - disabled in local mode [#52](https://github.com/inspec/inspec-prime/pull/52) ([Nik08](https://github.com/Nik08)) <!-- 6.4.18 -->
- CHEF-3403: Default server URL to production value [#50](https://github.com/inspec/inspec-prime/pull/50) ([ahasunos](https://github.com/ahasunos)) <!-- 6.4.17 -->
- CHEF-3186: Remove fetching of bearer auth token from vault [#48](https://github.com/inspec/inspec-prime/pull/48) ([ahasunos](https://github.com/ahasunos)) <!-- 6.4.16 -->
- CHEF 83 Revert attestations changes [#47](https://github.com/inspec/inspec-prime/pull/47) ([sathish-progress](https://github.com/sathish-progress)) <!-- 6.4.15 -->
- Foreports 6489 (CHEF-1458 Multiple values changes in SimpleConfig library) [#28](https://github.com/inspec/inspec-prime/pull/28) ([ahasunos](https://github.com/ahasunos)) <!-- 6.4.14 -->
- Foreport - Add nftables resources (#6499) [#44](https://github.com/inspec/inspec-prime/pull/44) ([clintoncwolfe](https://github.com/clintoncwolfe)) <!-- 6.4.13 -->
- Foreport - Update host resource to resolve all ipaddresses (#6481) [#39](https://github.com/inspec/inspec-prime/pull/39) ([clintoncwolfe](https://github.com/clintoncwolfe)) <!-- 6.4.12 -->
- Foreport - Bump rack from 2.2.6.2 to 2.2.6.4 in /omnibus (#6490) [#42](https://github.com/inspec/inspec-prime/pull/42) ([clintoncwolfe](https://github.com/clintoncwolfe)) <!-- 6.4.11 -->
- Foreport - fix: ensure Invoke-WebRequest headers can be configured (#6484) [#41](https://github.com/inspec/inspec-prime/pull/41) ([clintoncwolfe](https://github.com/clintoncwolfe)) <!-- 6.4.10 -->
- Foreport - CHEF-2438 Add train-kubernetes to inspec gemspec (#6512) [#43](https://github.com/inspec/inspec-prime/pull/43) ([clintoncwolfe](https://github.com/clintoncwolfe)) <!-- 6.4.9 -->
- Foreport - Clarify key_rsa docs regarding SSH keys (#6507) [#45](https://github.com/inspec/inspec-prime/pull/45) ([clintoncwolfe](https://github.com/clintoncwolfe)) <!-- 6.4.8 -->
- CHEF-3105 Fix windows openssl issue [#37](https://github.com/inspec/inspec-prime/pull/37) ([clintoncwolfe](https://github.com/clintoncwolfe)) <!-- 6.4.7 -->
- CHEF-2743: Set chef executable name to display in help messages of chef-licensing [#34](https://github.com/inspec/inspec-prime/pull/34) ([ahasunos](https://github.com/ahasunos)) <!-- 6.4.6 -->
- CHEF-2994: Add license command to list of allowed commands [#35](https://github.com/inspec/inspec-prime/pull/35) ([ahasunos](https://github.com/ahasunos)) <!-- 6.4.5 -->
- CHEF-1957: Update chef-licesing api call `license_keys` to `fetch_and_persist` [#30](https://github.com/inspec/inspec-prime/pull/30) ([ahasunos](https://github.com/ahasunos)) <!-- 6.4.4 -->
- Remove kitchen group from Gemfile [#31](https://github.com/inspec/inspec-prime/pull/31) ([ahasunos](https://github.com/ahasunos)) <!-- 6.4.3 -->
- CHEF-52: Add licensing information to help output [#27](https://github.com/inspec/inspec-prime/pull/27) ([ahasunos](https://github.com/ahasunos)) <!-- 6.4.2 -->
- Add command to list license information [#10](https://github.com/inspec/inspec-prime/pull/10) ([ahasunos](https://github.com/ahasunos)) <!-- 6.4.1 -->
- Licensing - Integrates Software Entitlement [#13](https://github.com/inspec/inspec-prime/pull/13) ([Vasu1105](https://github.com/Vasu1105)) <!-- 6.4.0 -->
- Integration of chef licensing with inspec [#12](https://github.com/inspec/inspec-prime/pull/12) ([Nik08](https://github.com/Nik08)) <!-- 6.3.0 -->
- CI - Use License Key and API Key Secrets from Vault [#26](https://github.com/inspec/inspec-prime/pull/26) ([clintoncwolfe](https://github.com/clintoncwolfe)) <!-- 6.2.49 -->
- Update Gemfile to add artifactory as source for chef-licensing gem dependency [#25](https://github.com/inspec/inspec-prime/pull/25) ([Vasu1105](https://github.com/Vasu1105)) <!-- 6.2.48 -->
- testing version bump and pipeline creation [#16](https://github.com/inspec/inspec-prime/pull/16) ([sean-simmons-progress](https://github.com/sean-simmons-progress)) <!-- 6.2.47 -->
- CHEF-1267 Add omnibus release and adhoc pipelines [#15](https://github.com/inspec/inspec-prime/pull/15) ([clintoncwolfe](https://github.com/clintoncwolfe)) <!-- 6.2.46 -->
- testing version bump [#9](https://github.com/inspec/inspec-prime/pull/9) ([sean-simmons-progress](https://github.com/sean-simmons-progress)) <!-- 6.2.45 -->
- Forport 6388 [#6477](https://github.com/inspec/inspec/pull/6477) ([Vasu1105](https://github.com/Vasu1105)) <!-- 6.2.44 -->
- Foreport 6360 [#6476](https://github.com/inspec/inspec/pull/6476) ([Vasu1105](https://github.com/Vasu1105)) <!-- 6.2.43 -->
- Foreport-6423 [#6474](https://github.com/inspec/inspec/pull/6474) ([Vasu1105](https://github.com/Vasu1105)) <!-- 6.2.42 -->
- Foreport 6442 [#6473](https://github.com/inspec/inspec/pull/6473) ([Vasu1105](https://github.com/Vasu1105)) <!-- 6.2.41 -->
- Foreport 6403 [#6470](https://github.com/inspec/inspec/pull/6470) ([Vasu1105](https://github.com/Vasu1105)) <!-- 6.2.40 -->
- Foreport 6386 [#6469](https://github.com/inspec/inspec/pull/6469) ([Vasu1105](https://github.com/Vasu1105)) <!-- 6.2.39 -->
- Foreport-6410 [#6468](https://github.com/inspec/inspec/pull/6468) ([Vasu1105](https://github.com/Vasu1105)) <!-- 6.2.38 -->
- Foreport-6408 Fix profile gem dependency loading issue when dependent gem is required inside profile libraries. [#6467](https://github.com/inspec/inspec/pull/6467) ([Vasu1105](https://github.com/Vasu1105)) <!-- 6.2.37 -->
- Foreport #6398: Updates release process docs as per current changes (#6398) [#6439](https://github.com/inspec/inspec/pull/6439) ([Vasu1105](https://github.com/Vasu1105)) <!-- 6.2.36 -->
- Foreport 6401: Fix for mongodb_session resource prints debug level of information in profile run result. [#6438](https://github.com/inspec/inspec/pull/6438) ([Vasu1105](https://github.com/Vasu1105)) <!-- 6.2.35 -->
- Foreport-6384 [#6466](https://github.com/inspec/inspec/pull/6466) ([Vasu1105](https://github.com/Vasu1105)) <!-- 6.2.34 -->
- Foreport #6384 RAKE TEST: Fix rake task for docs:cli [#6437](https://github.com/inspec/inspec/pull/6437) ([Vasu1105](https://github.com/Vasu1105)) <!-- 6.2.33 -->
- Foreport #6367 CFINSPEC-522: Remove rake tasks which are no longer used [#6436](https://github.com/inspec/inspec/pull/6436) ([Vasu1105](https://github.com/Vasu1105)) <!-- 6.2.32 -->
- Foreport-6395 [#6444](https://github.com/inspec/inspec/pull/6444) ([ahasunos](https://github.com/ahasunos)) <!-- 6.2.31 -->
- Foreport-6385 [#6447](https://github.com/inspec/inspec/pull/6447) ([ahasunos](https://github.com/ahasunos)) <!-- 6.2.30 -->
- Foreport #6377 CFINSPEC-542 Bug fix for profiles with dependent profiles (#6377) [#6435](https://github.com/inspec/inspec/pull/6435) ([Vasu1105](https://github.com/Vasu1105)) <!-- 6.2.29 -->
- Foreport 6369 to inspec-6 Extended file format support for waivers: JSON &amp; CSV only [#6371](https://github.com/inspec/inspec/pull/6371) ([Nik08](https://github.com/Nik08)) <!-- 6.2.28 -->
- Foreport-6381 [#6451](https://github.com/inspec/inspec/pull/6451) ([ahasunos](https://github.com/ahasunos)) <!-- 6.2.27 -->
- Foreport-6378 [#6453](https://github.com/inspec/inspec/pull/6453) ([ahasunos](https://github.com/ahasunos)) <!-- 6.2.26 -->
- Foreport 6341: Use Ruby 3.1.2 in Omnibus build [#6441](https://github.com/inspec/inspec/pull/6441) ([Vasu1105](https://github.com/Vasu1105)) <!-- 6.2.25 -->
- Foreport #6337 Fix undefined method &#39;summary&#39; for Gem::SourceFetchProblem (NoMethodError) when air gapped [#6434](https://github.com/inspec/inspec/pull/6434) ([Vasu1105](https://github.com/Vasu1105)) <!-- 6.2.25 -->
- Foreport 6342 Fix env smoke test by updating ERB.new in `inspec env`; add additional test [#6440](https://github.com/inspec/inspec/pull/6440) ([Vasu1105](https://github.com/Vasu1105)) <!-- 6.2.24 -->
- Foreport-6344 Fixing typo in user_permissions [#6465](https://github.com/inspec/inspec/pull/6465) ([Vasu1105](https://github.com/Vasu1105)) <!-- 6.2.23 -->
- Foreport 6340: Fix for habitat build failure [#6461](https://github.com/inspec/inspec/pull/6461) ([Nik08](https://github.com/Nik08)) <!-- 6.2.22 -->
- Foreport-6334: CFINSPEC-393 - Fix train-kubernetes plugin load issue [#6464](https://github.com/inspec/inspec/pull/6464) ([Vasu1105](https://github.com/Vasu1105)) <!-- 6.2.21 -->
- Foreport 6240: Update chefstyle from 2.0.x to 2.2.2 to use RuboCop 1.25.1 [#6458](https://github.com/inspec/inspec/pull/6458) ([Nik08](https://github.com/Nik08)) <!-- 6.2.20 -->
- Foreport #6262 Prevent negative status from crashing launchctl service resource [#6433](https://github.com/inspec/inspec/pull/6433) ([Vasu1105](https://github.com/Vasu1105)) <!-- 6.2.19 -->
- Foreport-6330: Remove Windows Ruby 3.0 testing [#6452](https://github.com/inspec/inspec/pull/6452) ([Vasu1105](https://github.com/Vasu1105)) <!-- 6.2.18 -->
- Foreport #6298: CFINSPEC-493 update signing_identity [#6448](https://github.com/inspec/inspec/pull/6448) ([Vasu1105](https://github.com/Vasu1105)) <!-- 6.2.17 -->
- Foreport #6324 Fix Ruby 2.7 Bundle Installs on CI Verify Pipeline [#6446](https://github.com/inspec/inspec/pull/6446) ([Vasu1105](https://github.com/Vasu1105)) <!-- 6.2.16 -->
- Foreport-6289: Fix for omnibus build failure on Windows [#6463](https://github.com/inspec/inspec/pull/6463) ([Vasu1105](https://github.com/Vasu1105)) <!-- 6.2.15 -->
- Foreport-6274 Bump omnibus-software from `1d540dc` to `7d0e0fe` in /omnibus [#6462](https://github.com/inspec/inspec/pull/6462) ([Vasu1105](https://github.com/Vasu1105)) <!-- 6.2.14 -->
- Foreport - 6227 [#6460](https://github.com/inspec/inspec/pull/6460) ([Vasu1105](https://github.com/Vasu1105)) <!-- 6.2.13 -->
- Foreport #6239: Sync up the default branch as main [#6455](https://github.com/inspec/inspec/pull/6455) ([Vasu1105](https://github.com/Vasu1105)) <!-- 6.2.12 -->
- Foreport 6304 RESOURCE-527 Add an inspec init template for alicloud [#6432](https://github.com/inspec/inspec/pull/6432) ([Vasu1105](https://github.com/Vasu1105)) <!-- 6.2.11 -->
- Foreport #6329 to inspec-6 - Update readme for usage via Docker (CFINSPEC-516) [#6333](https://github.com/inspec/inspec/pull/6333) ([clintoncwolfe](https://github.com/clintoncwolfe)) <!-- 6.2.10 -->
- CFINSPEC-479 Inspec parallel breaking fix [#6272](https://github.com/inspec/inspec/pull/6272) ([Nik08](https://github.com/Nik08)) <!-- 6.2.9 -->
- Added child-status reporter in features.yaml list [#6288](https://github.com/inspec/inspec/pull/6288) ([Nik08](https://github.com/Nik08)) <!-- 6.2.8 -->
- Foreport #6267 to inspec-6 [#6283](https://github.com/inspec/inspec/pull/6283) ([Vasu1105](https://github.com/Vasu1105)) <!-- 6.2.7 -->
- Foreport 6257 to inspec 6 [#6279](https://github.com/inspec/inspec/pull/6279) ([Nik08](https://github.com/Nik08)) <!-- 6.2.6 -->
- Foreport 6229 to inspec 6 [#6277](https://github.com/inspec/inspec/pull/6277) ([Nik08](https://github.com/Nik08)) <!-- 6.2.5 -->
- Foreport 6261 to InSpec 6 [#6276](https://github.com/inspec/inspec/pull/6276) ([ahasunos](https://github.com/ahasunos)) <!-- 6.2.4 -->
- Foreport 6043 to inspec-6 [#6278](https://github.com/inspec/inspec/pull/6278) ([clintoncwolfe](https://github.com/clintoncwolfe)) <!-- 6.2.3 -->
- Foreport 6243 to inspec-6 [#6275](https://github.com/inspec/inspec/pull/6275) ([clintoncwolfe](https://github.com/clintoncwolfe)) <!-- 6.2.2 -->
- Foreport 6238 to inspec-6 [#6280](https://github.com/inspec/inspec/pull/6280) ([Vasu1105](https://github.com/Vasu1105)) <!-- 6.2.1 -->
- Feature Config File and Logger Support [#6260](https://github.com/inspec/inspec/pull/6260) ([clintoncwolfe](https://github.com/clintoncwolfe)) <!-- 6.2.0 -->
- CFINSPEC-246/CFINSPEC-247 Attestation changes for N/R outcomes [#6222](https://github.com/inspec/inspec/pull/6222) ([Nik08](https://github.com/Nik08)) <!-- 6.1.0 -->
- CFINSPEC-452 Added Inspec parallel logging for warn/error [#6245](https://github.com/inspec/inspec/pull/6245) ([Nik08](https://github.com/Nik08)) <!-- 6.0.1 -->
<!-- latest_stable_release 5.22.13 -->
## [v5.22.13](https://github.com/inspec/inspec/tree/v5.22.13) (2023-08-16)
#### Merged Pull Requests
- CHEF-65: Create inspec-5 release branch in Expeditor and Dependabot configuration [#6591](https://github.com/inspec/inspec/pull/6591) ([Vasu1105](https://github.com/Vasu1105))
<!-- latest_release -->
<!-- latest_stable_release -->
<!-- release_rollup since=5.22.3 -->
### Changes since 5.22.3 release
## [v5.22.12](https://github.com/inspec/inspec/tree/v5.22.12) (2023-08-09)
#### Merged Pull Requests
- CHEF-65: Create inspec-5 release branch in Expeditor and Dependabot configuration [#6591](https://github.com/inspec/inspec/pull/6591) ([Vasu1105](https://github.com/Vasu1105)) <!-- 5.22.13 -->
- CHEF-5200 Waived controls are not getting waived (skipped) in case of failure at resource level. [#6588](https://github.com/inspec/inspec/pull/6588) ([Vasu1105](https://github.com/Vasu1105)) <!-- 5.22.12 -->
- CHEF-4080: Point to latest EULA in GUI installers for InSpec-5 [#6582](https://github.com/inspec/inspec/pull/6582) ([ahasunos](https://github.com/ahasunos)) <!-- 5.22.11 -->
- CHEF-4115 Added ability to merge reporter configurations from both CLI and config [#6568](https://github.com/inspec/inspec/pull/6568) ([Nik08](https://github.com/Nik08)) <!-- 5.22.10 -->
@ -24,9 +139,7 @@
- Update Docker base image to be ubuntu 22.04 [#6526](https://github.com/inspec/inspec/pull/6526) ([clintoncwolfe](https://github.com/clintoncwolfe)) <!-- 5.22.5 -->
- Update RSpec to 3.12 [#6523](https://github.com/inspec/inspec/pull/6523) ([pirj](https://github.com/pirj)) <!-- 5.22.5 -->
- CHEF-1631 Clarify that command timeout default was withdrawn [#6511](https://github.com/inspec/inspec/pull/6511) ([clintoncwolfe](https://github.com/clintoncwolfe)) <!-- 5.22.4 -->
<!-- release_rollup -->
<!-- latest_stable_release -->
## [v5.22.3](https://github.com/inspec/inspec/tree/v5.22.3) (2023-05-18)
#### Merged Pull Requests
@ -49,7 +162,7 @@
- Bump rack from 2.2.6.2 to 2.2.6.4 in /omnibus [#6490](https://github.com/inspec/inspec/pull/6490) ([dependabot[bot]](https://github.com/dependabot[bot]))
- fix: ensure Invoke-WebRequest headers can be configured [#6484](https://github.com/inspec/inspec/pull/6484) ([amlodzianowski](https://github.com/amlodzianowski))
- For #6493 : Add postgres_session support for custom port with a socket connection [#6494](https://github.com/inspec/inspec/pull/6494) ([Taknok](https://github.com/Taknok))
<!-- latest_stable_release -->
## [v5.21.29](https://github.com/inspec/inspec/tree/v5.21.29) (2023-01-24)

17
Gemfile
View file

@ -48,19 +48,6 @@ group :deploy do
gem "inquirer"
end
group :kitchen do
gem "berkshelf"
# Chef 18 requires ruby 3
if Gem.ruby_version >= Gem::Version.new("3.0.0")
gem "chef", ">= 17.0"
else
# Ruby 2.7 presumably - TODO remove this when 2.7 is sunsetted
gem "chef", "~> 16.0"
end
gem "test-kitchen", ">= 2.8"
gem "kitchen-inspec", ">= 2.0"
gem "kitchen-dokken", ">= 2.11"
gem "git"
source "https://artifactory-internal.ps.chef.co/artifactory/api/gems/omnibus-gems-local/" do
gem "chef-licensing"
end

View file

@ -1,5 +1,5 @@
# Chef InSpec: Inspect Your Infrastructure
* **Project State: Active**
* **Issues Response SLA: 14 business days**
* **Pull Request Response SLA: 14 business days**

View file

@ -1 +1 @@
5.22.13
6.4.45

View file

@ -1,82 +0,0 @@
# Attestations
## Use Cases
As a compliance officer,
I want to mark skipped controls as manually passed or failed
so that I can manually complete the profile.
As a compliance officer,
I want to set an expiration date and a justification for my attestations
so that I can control their application.
As a compliance officer,
I want flexibility in the file format accepted by the attestations system (XLSX, YAML, CSV, JSON),
so that I can use a familiar file format.
When used with Enhanced Outcomes, this becomes handling `Not Reviewed` controls.
## Mechanism
### CLI option desirable
`inspec exec profilename --attestation-file file.???`
The new option is named like `--waiver-file` - singular, with `-file`. You may provide multiple arguments for the option.
The file can be any of the following formats: `YAML`, `XLSX`, `CSV`, or `JSON`.
#### YAML and JSON
An array of Hashes.
#### XLSX and CSV
XLSX is the first sheet in the file.
Both formats assume a header row.
### Fields in the file
#### control_id
_Required_. Matches control ID of the control.
#### justification
_Required_. Free text field, used as an explanation for the control when displayed.
#### evidence_url
_Optional_. URL to some evidence, determined by the user, supports the justification.
#### expiration_date
_Optional_. If present, the attestation expires at the end of the date given.
#### status
_Optional_.
Default `passed`. If the attestation should indicate that the control is a failure, set this to `failed`.
### Implementation
When running, at the **RunData** stage, attestations are handled by the following process:
1. Locate matching controls by matching the control ID.
2. Inject an artificial test result into the control. Use the attestation justification as the result message.
3. If the attestation is expired, set the new test result to Skip.
4. If the attestation is not expired, set the new test result to the status given on the attestation data (default pass).
5. Record a copy of the attestation data structure in the Control RunData structure.
### Compatibility
To support backward compatibility with existing MITRE work, support will be added (but not otherwise documented) for the following fields:
* explanation - the equivalent of justification
* updated (Date) and frequency (string enum) - together, the equivalent of the expiration date.

View file

@ -633,6 +633,30 @@ This subcommand has the following additional options:
</dl>
## license
Subcommands for interacting with the Chef licensing system.
`inspec license` supports two subcommands, `add` and `list`.
#### Add
Add a Chef license.
Not applicable for users running the local licensing service.
```bash
inspec license add
```
#### List
Run license diagnostics and output the details of your current Chef license configuration.
```bash
inspec license list
```
## run_context
Used to test run-context detection

View file

@ -0,0 +1,237 @@
+++
title = "InSpec Parallel"
draft = false
gh_repo = "inspec"
[menu]
[menu.inspec]
title = "InSpec Parallel"
identifier = "inspec/parallel.md InSpec Parallel"
parent = "inspec"
weight = 25
+++
Chef InSpec Parallel can automatically manage multiple profile executions in parallel on a system targeting several remote systems and environments.
It manages multiple processes, their status updates, their exit codes, and user updates.
All target operating systems and environments that can be addressed using `--target` are supported, and it is supported on Windows, MacOS, and Linux environments.
InSpec Parallel is a new feature in **Chef InSpec 6**.
{{< note >}}
Currently, `inspec parallel` only supports the `exec` command.
{{< /note >}}
## How to use InSpec Parallel
The following example shows you how to execute the **Dev-Sec SSH Baseline** profile against five servers in parallel using `inpec parallel exec`.
1. Create an [option file](#option-file) that contains the CLI options that are passed to `inspec exec parallel`.
The option file contains one invocation per line and specifies all options in each invocation.
```text
# five-servers.txt
# Option file for running against multiple SSH targets
-t ssh://server1 --reporter cli:server1.out
-t ssh://server2 --reporter cli:server2.out
-t ssh://server3 --reporter cli:server3.out
-t ssh://server4 --reporter cli:server4.out
-t ssh://server5 --reporter cli:server5.out
```
1. Specify the option file that InSpec Parallel executes using the `-o` or `--option_file` flag in the InSpec CLI.
```bash
inspec parallel exec https://github.com/dev-sec/ssh-baseline -o five-servers.txt -i file_name.pem
```
As InSpec Parallel runs, it shows the progress (percentage of controls completed) of each invocation, the process ID of each job, and writes log and error data to the `logs/` directory with each log file named after the process ID.
```bash
Press CTL+C to stop
InSpec Parallel
Running 5 invocations in 4 slots
-----------------------------------------------------------------------------------------------------------------------------------------
Slot 1 Slot 2 Slot 3 Slot 4
-----------------------------------------------------------------------------------------------------------------------------------------
50132: 0.0% 50133: 12.5% 50134: 12.5% Done
```
## Option file
An option file is a text file that contains options passed to `inspec parallel`.
Chef InSpec ignores comments (starting with a `#`) and blank lines in an option file.
Chef InSpec invokes `inspec parallel` on each non-commented and non-blank line.
The only requirement is that every invocation in an option file must have a `--reporter` option.
The reporter option must write to a file or use the `automate` reporter to send an API post to a Chef Automate service.
For details of the available reporters and the full syntax of the reporter option, see the [Chef InSpec Reporter documentation]({{< relref "/inspec/reporters" >}}).
The simplest option file might look like this:
```text
# simple.txt
# Run five invocations, saving the output as ordinal names
--reporter cli:first.out
--reporter cli:second.out
--reporter cli:third.out
--reporter cli:fourth.out
--reporter cli:fifth.out
```
For this example, InSpec Parallel would run the same profile on the same target five times, it would send the output to each of the five reporters listed in the option file, and you would specify the target and profile when you invoke `inspec parallel exec` in the command line.
You can pass any options on the invocation line, including `--controls` (to divide a profile into sections), `--input` (to parameterize a profile and possibly target different resources), and `--target` (to target different machines or environments).
See the [Examples section](#examples) for more detail on how you can use an option file.
### Embedded Ruby templating
You can add Embedded RuBy (ERB) template escapes and Chef InSpec will evaluate it as an ERB template.
You can directly embed Ruby code into your option file, including loops and conditionals.
The rendered output of the option file is used as invocations.
This is especially useful with the `--dry-run` option.
The most common ERB templating is to use the `pid` variable to reference the process ID of the child process.
See the [Examples](#name-json-output-files-with-process-id) section for more information.
### Executable script
If the name of the option file ends in `.sh` (MacOS, Linux) or `.ps1` (Windows), InSpec Parallel executes the script and uses the standard output as the option file.
{{< note >}}
This feature is experimental and we would love to hear [feedback](https://github.com/inspec/inspec/issues/new/choose) from you.
{{< /note >}}
## Options
InSpec Parallel accepts options from the subcommand that it's managing. It also accepts the following options:
`--bg`
: The `--bg` option silences all output from the command and runs it in the background. InSpec Parallel will still write log files with the `--bg` option.
`--dry-run`
: The `--dry-run` option interprets the option file but does not execute it. Chef InSpec outputs the lines that would have been executed to the standard output. If you add `--verbose`, you can see all the CLI defaults that implicitly get added.
: {{< note >}}
When calling `--dry-run`, you may notice that an extra reporter gets added to your invocation, `--reporter child-status`. This reporter is a special streaming reporter used to report status from the running child processes to the parent process and is a necessary part of the plumbing of InSpec Parallel.
{{< /note >}}
`-j`
`--jobs`
: Use the `-j` or `--jobs` option to specify how many job slots InSpec Parallel uses.
InSpec Parallel defaults to the number of hyperthreaded cores on your machine (for example, a dual-core machine with hyperthreading defaults to four jobs).
The default is usually reasonable, but experimentation may be rewarding.
`-o`
`--option_file`
: Use the `-o` or `--option_file` option in the command line to specify the option file that InSpec Parallel will run.
## Examples
### Use the same options for each invocation
`inspec parallel exec` accepts all options that `inspec exec` does and passes them to each invocation as defaults.
This means that you do not have to specify repetitive options that are constant across all the invocations in an option file.
For example, if all machines take the same SSH key, you can specify it once on the top-level command line.
```text
# three-servers.txt
# Option file for running against multiple SSH targets
-t ssh://server1 --reporter cli:server1.out
-t ssh://server2 --reporter cli:server2.out
-t ssh://server3 --reporter cli:server3.out
```
```bash
inspec parallel exec profile_name -o three-servers.txt -i file_name.pem
```
### Name JSON output files with process ID
In this example, the `json` reporter saves output log files in the `logs` directory and names each one after the process ID using the `pid` ERB variable.
This technique would work with any [reporter]({{< relref "/inspec/reporters" >}}) that can write to a file.
```text
# pid-named-output.txt
# Option file in which the output is named after the PID of the process
--reporter json:logs/<%= pid %>.json
--reporter json:logs/<%= pid %>.json
--reporter json:logs/<%= pid %>.json
--reporter json:logs/<%= pid %>.json
```
After this profile is executed, the `logs` directory would have the following files:
- 1000.log
- 1000.json
- 1001.log
- 1001.json
- 1002.log
- 1002.json
- 1003.log
- 1003.json
### Run the same profile on different targets
You can run the same profile on multiple targets by specifying each target in the option file using the `-t` or `--target` option.
```text
# five-servers.txt
# Option file for running against multiple SSH targets
-t ssh://server1 --reporter cli:server1.out
-t ssh://server2 --reporter cli:server2.out
-t ssh://server3 --reporter cli:server3.out
-t ssh://server4 --reporter cli:server4.out
-t ssh://server5 --reporter cli:server5.out
```
Then specify the profile and the option file in the command line.
```bash
inspec parallel exec https://github.com/dev-sec/ssh-baseline -o five-servers.txt -i file_name.pem
```
If you have many or variable targets to run against, consider using ERB templating to read the list of targets after reading them from a CSV file or connecting to an API. You can also use a script to list your targets.
### Run different profiles on the same target
To run different profiles on the same target, specify the profile at the front of the invocation in the option file.
```text
# multi-profile.txt
https://github.com/dev-sec/ssh-baseline --reporter cli:ssh-baseline.out
https://github.com/dev-sec/linux-baseline --reporter cli:linux-baseline.out
```
Then invoke InSpec parallel by passing the target as a top-level option and a dummy name for the profile.
```bash
inspec parallel exec dummy -o multi-profile.txt -t ssh://server
```
### Run different parts of a profile in parallel
If your profile has well-named control IDs, you can use the `--controls` option to divide the profile into sections.
Suppose that your profile has sections named **C**, **S**, and **N** and the controls in each section have control IDs that start with the given letter,
then you can create an option file that divides the profile as follows:
```text
# divide-aws-bp.txt
--reporter cli:C.out --controls /^C/
--reporter cli:S.out --controls /^S/
--reporter cli:N.out --controls /^N/
```
When you run the following command, `inspec exec` runs three times, once for each of the **C**, **S**, and **N** sections of the profile.
```bash
inspec parallel exec aws-best-practices -o divide-aws-bp.txt -t aws://profile_name@us-east-2
```

6
etc/features.sig Normal file
View file

@ -0,0 +1,6 @@
wNEzKHmtSf1pIdciEC6DOs5SlOs3IbW1psVFLlmZc0NbnHe6MEahAnKWmHUP
9YrDv2JMQo1I8MM/cez8XDxpK4O5y4HT66RqoAlfBkg82LmYC7f1Cy34ByCj
LBZg5o/IVBGnY+Ksbhtp0mQYEyU048FnXIfh9uOfbKahU8HkPJssTkIw3fjL
Vrd5GQ4ssfW1XXFaxx7DjxWlPmWBVhd8c1Y2RlACZyI+w1DQNYimrWvgiFym
0VbnndiSX+2x84AZHE9AmsebcAYk9QlqO1N0VeYqBZj45FXLtpsNwYo0amDa
D/wyKGxRQLUYXyd2tDVJMWbeHPHy8UIK17RoSctrEg==

88
etc/features.yaml Normal file
View file

@ -0,0 +1,88 @@
---
features:
inspec-cli-exec:
description: Run InSpec profile code at the command line.
inspec-cli-shell:
description: Experiment with InSpec Language interactively.
inspec-cli-check:
description: Examine a profile for problems.
inspec-cli-json:
description: Generate JSON summary for inspec profile/s.
inspec-cli-export:
description: Generate summary in specified formats for profile/s.
inspec-cli-vendor:
description: Download all profile dependencies and generate a lockfile in vendor directory.
inspec-cli-archive:
description: Archive a profile to tar.gz (default) or zip.
inspec-cli-detect:
description: Detect the target OS.
inspec-cli-env:
description: Output shell-appropriate completion configuration.
inspec-cli-schema:
description: Print the JSON schema.
inspec-cli-run-context:
description: Test run-context detection.
inspec-cli-version:
description: Print the version of InSpec.
inspec-cli-clear-cache:
description: Clear InSpec cache stored in ~/.inspec/cache or specific vendor cache path.
inspec-cli-compliance-login:
description: Login to Automate Server using InSpec.
inspec-cli-compliance-profiles:
description: Lists all uploaded profiles from automate server.
inspec-cli-compliance-exec:
description: Run InSpec profile from a list of profiles in automate server.
inspec-cli-compliance-download:
description: Download the InSpec profile from automate server.
inspec-cli-compliance-upload:
description: Upload InSpec profile to automate server.
inspec-cli-compliance-version:
description: Print the version of Automate Server.
inspec-cli-compliance-logout:
description: Logout from Automate Server.
inspec-cli-habitat-profile-create:
description: Create Habitat Artifact for the InSpec profile.
inspec-cli-habitat-profile-setup:
description: Configure Habitat Artifact.
inspec-cli-habitat-profile-upload:
description: Upload Habitat Artifact for the InSpec profile to Habitat Builder Depot.
inspec-cli-init-profile:
description: Generate a new InSpec profile.
inspec-cli-init-plugin:
description: Generate a new InSpec plugin.
inspec-cli-init-resource:
description: Generate a new InSpec resource.
inspec-cli-parallel-exec:
description: Run list of InSpec exec operations parallely.
inspec-cli-sign-generate-keys:
description: Generate a RSA key pair for signing and verification.
inspec-cli-sign-profile:
description: Sign InSpec profile and generate .iaf artifact.
inspec-cli-sign-verify:
description: Verify a signed profile .iaf artifact.
inspec-enhanced-outcomes:
description: Use enhanced outcomes in reporters
inspec-waivers:
description: Use waivers mechanism with one or more waiver files.
inspec-reporter-cli:
description: Use CLI reporter.
inspec-reporter-json:
description: Use JSON reporter.
inspec-reporter-json-automate:
description: Use JSON automate reporter.
inspec-reporter-automate:
description: Use automate reporter.
inspec-reporter-yaml:
description: Use YAML reporter.
inspec-reporter-json-min:
description: Use JSON min reporter for minimal JSON output.
inspec-reporter-junit:
description: Use JUnit reporter.
inspec-reporter-junit2:
description: Use JUnit2 reporter.
inspec-reporter-html2:
description: Use HTML reporter.
inspec-reporter-progress-bar:
description: Use progress bar streaming reporter
inspec-reporter-child-status:
description: Child status reporter used in inspec parallel reporting.

View file

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

View file

@ -4,6 +4,7 @@ libdir = File.dirname(__FILE__)
$LOAD_PATH.unshift(libdir) unless $LOAD_PATH.include?(libdir)
require "inspec/version"
require "inspec/utils/licensing_config"
require "inspec/exceptions"
require "inspec/utils/deprecation"
require "inspec/profile"
@ -30,4 +31,4 @@ require "inspec/source_reader"
require "inspec/resource"
require "inspec/dependency_loader"
require "inspec/dependency_installer"
require "inspec/dependency_installer"

View file

@ -1,9 +1,11 @@
require "thor" # rubocop:disable Chef/Ruby/UnlessDefinedRequire
require "chef-licensing"
require "inspec/log"
require "inspec/ui"
require "inspec/config"
require "inspec/dist"
require "inspec/utils/deprecation/global_method"
require "inspec/utils/licensing_config"
# Allow end of options during array type parsing
# https://github.com/erikhuda/thor/issues/631
@ -30,11 +32,34 @@ module Inspec
end
def self.start(given_args = ARGV, config = {})
check_license! if config[:enforce_license] || config[:enforce_license].nil?
if Inspec::Dist::EXEC_NAME == "inspec"
check_license! if config[:enforce_license] || config[:enforce_license].nil?
fetch_and_persist_license
end
super(given_args, config)
end
def self.fetch_and_persist_license
allowed_commands = ["-h", "--help", "help", "-v", "--version", "version", "license"]
begin
if (allowed_commands & ARGV.map(&:downcase)).empty? && !ARGV.empty?
license_keys = ChefLicensing.fetch_and_persist
# Only if EULA acceptance or license key args are present. And licenses are successfully persisted, do clean exit.
if ARGV.select { |arg| !(arg.include? "--chef-license") }.empty? && !license_keys.blank?
Inspec::UI.new.exit
end
end
rescue ChefLicensing::LicenseKeyFetcher::LicenseKeyNotFetchedError
Inspec::Log.error "#{Inspec::Dist::PRODUCT_NAME} cannot execute without valid licenses."
Inspec::UI.new.exit(:usage_error)
rescue ChefLicensing::Error => e
Inspec::Log.error e.message
Inspec::UI.new.exit(:usage_error)
end
end
# EULA acceptance
def self.check_license!
allowed_commands = ["-h", "--help", "help", "-v", "--version", "version"]
@ -48,9 +73,6 @@ module Inspec
Inspec::VERSION,
logger: Inspec::Log
)
if license_acceptor_output && ARGV.count == 1 && (ARGV.first.include? "--chef-license")
Inspec::UI.new.exit
end
license_acceptor_output
end
rescue LicenseAcceptance::LicenseNotAcceptedError
@ -211,6 +233,30 @@ module Inspec
def self.help(*args)
super(*args)
if Inspec::Dist::EXEC_NAME == "inspec"
puts <<~CHEF_LICENSE_HELP
Chef Compliance has three tiers of licensing:
* Free-Tier
Users are limited to audit maximum of 10 targets
Entitled for personal or non-commercial use
* Trial
Entitled for unlimited number of targets
Entitled for 30 days only
Entitled for commercial use
* Commercial
Entitled for purchased number of targets
Entitled for period of subscription purchased
Entitled for commercial use
inspec license add: This command helps users to generate or add an additional license (not applicable to local licensing service)
For more information please visit:
www.chef.io/licensing/faqs
CHEF_LICENSE_HELP
end
puts "\nAbout #{Inspec::Dist::PRODUCT_NAME}:"
puts " Patents: chef.io/patents\n\n"
end

View file

@ -39,12 +39,33 @@ module Inspec
end
def fetch
if cache.exists?(cache_key)
if cache.exists?(cache_key) && cache.locked?(cache_key)
Inspec::Log.debug "Waiting for lock to be released on the cache dir ...."
counter = 0
until cache.locked?(cache_key) == false
if (counter += 1) > 300
Inspec::Log.warn "Giving up waiting on cache lock at #{cache_key}"
exit 1
end
sleep 0.1
end
fetch
elsif cache.exists?(cache_key) && !cache.locked?(cache_key)
Inspec::Log.debug "Using cached dependency for #{target}"
[cache.prefered_entry_for(cache_key), false]
else
Inspec::Log.debug "Dependency does not exist in the cache #{target}"
fetcher.fetch(cache.base_path_for(fetcher.cache_key))
begin
Inspec::Log.debug "Dependency does not exist in the cache #{target}"
cache.lock(cache.base_path_for(fetcher.cache_key)) if fetcher.requires_locking?
fetcher.fetch(cache.base_path_for(fetcher.cache_key))
rescue SystemExit => e
exit_code = e.status || 1
Inspec::Log.error "Error while creating cache for dependency ... #{e.message}"
FileUtils.rm_rf(cache.base_path_for(fetcher.cache_key))
exit(exit_code)
ensure
cache.unlock(cache.base_path_for(fetcher.cache_key)) if fetcher.requires_locking?
end
assert_cache_sanity!
[fetcher.archive_path, fetcher.writable?]
end

View file

@ -6,6 +6,7 @@ require "inspec/backend"
require "inspec/dependencies/cache"
require "inspec/utils/json_profile_summary"
require "inspec/utils/yaml_profile_summary"
require "inspec/feature"
module Inspec # TODO: move this somewhere "better"?
autoload :BaseCLI, "inspec/base_cli"
@ -61,6 +62,11 @@ class Inspec::InspecCLI < Inspec::BaseCLI
require "license_acceptance/cli_flags/thor"
include LicenseAcceptance::CLIFlags::Thor
if Inspec::Dist::EXEC_NAME == "inspec"
require "chef-licensing/cli_flags/thor"
include ChefLicensing::CLIFlags::Thor
end
desc "json PATH", "read all tests in the PATH and generate a JSON summary."
option :output, aliases: :o, type: :string,
desc: "Save the created profile to a path."
@ -70,9 +76,11 @@ class Inspec::InspecCLI < Inspec::BaseCLI
desc: "A list of tags to filter controls and include only those. Ignore all other tests."
profile_options
def json(target)
# This deprecation warning is ignored currently.
Inspec.deprecate(:renamed_to_inspec_export)
export(target, true)
Inspec.with_feature("inspec-cli-json") {
# This deprecation warning is ignored currently.
Inspec.deprecate(:renamed_to_inspec_export)
export(target, true)
}
end
desc "export PATH", "read the profile in PATH and generate a summary in the given format."
@ -88,61 +96,65 @@ class Inspec::InspecCLI < Inspec::BaseCLI
desc: "For --what=profile, a list of tags to filter controls and include only those. Ignore all other tests."
profile_options
def export(target, as_json = false)
o = config
diagnose(o)
o["log_location"] = $stderr
configure_logger(o)
Inspec.with_feature("inspec-cli-export") {
begin
o = config
diagnose(o)
o["log_location"] = $stderr
configure_logger(o)
# using dup to resolve "can't modify frozen String" error.
what = o[:what].dup || "profile"
what.downcase!
raise Inspec::Error.new("Unrecognized option '#{what}' for --what - expected one of profile, readme, or metadata.") unless %w{profile readme metadata}.include?(what)
# using dup to resolve "can't modify frozen String" error.
what = o[:what].dup || "profile"
what.downcase!
raise Inspec::Error.new("Unrecognized option '#{what}' for --what - expected one of profile, readme, or metadata.") unless %w{profile readme metadata}.include?(what)
default_format_for_what = {
"profile" => "yaml",
"metadata" => "raw",
"readme" => "raw",
}
valid_formats_for_what = {
"profile" => %w{yaml json},
"metadata" => %w{yaml raw}, # not going to argue
"readme" => ["raw"],
}
format = o[:format] || default_format_for_what[what]
# default is json if we were called as old json command
format = "json" if as_json
raise Inspec::Error.new("Invalid option '#{format}' for --format and --what combination") unless format && valid_formats_for_what[what].include?(format)
default_format_for_what = {
"profile" => "yaml",
"metadata" => "raw",
"readme" => "raw",
}
valid_formats_for_what = {
"profile" => %w{yaml json},
"metadata" => %w{yaml raw}, # not going to argue
"readme" => ["raw"],
}
format = o[:format] || default_format_for_what[what]
# default is json if we were called as old json command
format = "json" if as_json
raise Inspec::Error.new("Invalid option '#{format}' for --format and --what combination") unless format && valid_formats_for_what[what].include?(format)
o[:backend] = Inspec::Backend.create(Inspec::Config.mock)
o[:check_mode] = true
o[:vendor_cache] = Inspec::Cache.new(o[:vendor_cache])
profile = Inspec::Profile.for_target(target, o)
dst = o[:output].to_s
o[:backend] = Inspec::Backend.create(Inspec::Config.mock)
o[:check_mode] = true
o[:vendor_cache] = Inspec::Cache.new(o[:vendor_cache])
profile = Inspec::Profile.for_target(target, o)
dst = o[:output].to_s
case what
when "profile"
if format == "json"
require "json" unless defined?(JSON)
# Write JSON
Inspec::Utils::JsonProfileSummary.produce_json(
info: profile.info,
write_path: dst
)
elsif format == "yaml"
Inspec::Utils::YamlProfileSummary.produce_yaml(
info: profile.info,
write_path: dst
)
case what
when "profile"
if format == "json"
require "json" unless defined?(JSON)
# Write JSON
Inspec::Utils::JsonProfileSummary.produce_json(
info: profile.info,
write_path: dst
)
elsif format == "yaml"
Inspec::Utils::YamlProfileSummary.produce_yaml(
info: profile.info,
write_path: dst
)
end
when "readme"
out = dst.empty? ? $stdout : File.open(dst, "w")
out.write(profile.readme)
when "metadata"
out = dst.empty? ? $stdout : File.open(dst, "w")
out.write(profile.metadata_src)
end
rescue StandardError => e
pretty_handle_exception(e)
end
when "readme"
out = dst.empty? ? $stdout : File.open(dst, "w")
out.write(profile.readme)
when "metadata"
out = dst.empty? ? $stdout : File.open(dst, "w")
out.write(profile.metadata_src)
end
rescue StandardError => e
pretty_handle_exception(e)
}
end
desc "check PATH", "Verify the metadata in the `inspec.yml` file,\
@ -154,80 +166,85 @@ class Inspec::InspecCLI < Inspec::BaseCLI
desc: "Enable or disable cookstyle checks.", default: false
profile_options
def check(path) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
o = config
diagnose(o)
o["log_location"] ||= STDERR if o["format"] == "json"
o["log_level"] ||= "warn"
configure_logger(o)
Inspec.with_feature("inspec-cli-check") {
begin
o = config
diagnose(o)
o["log_location"] ||= STDERR if o["format"] == "json"
o["log_level"] ||= "warn"
configure_logger(o)
o[:backend] = Inspec::Backend.create(Inspec::Config.mock)
o[:check_mode] = true
o[:vendor_cache] = Inspec::Cache.new(o[:vendor_cache])
o[:backend] = Inspec::Backend.create(Inspec::Config.mock)
o[:check_mode] = true
o[:vendor_cache] = Inspec::Cache.new(o[:vendor_cache])
# run check
profile = Inspec::Profile.for_target(path, o)
result = profile.check
# run check
profile = Inspec::Profile.for_target(path, o)
result = profile.check
if o["format"] == "json"
puts JSON.generate(result)
else
%w{location profile controls timestamp valid}.each do |item|
prepared_string = format("%-12s %s",
"#{item.to_s.capitalize} :",
result[:summary][item.to_sym])
ui.plain_line(prepared_string)
end
puts
enable_offenses = !Inspec.locally_windows? # See 5723
if result[:errors].empty? && result[:warnings].empty? && result[:offenses].empty?
if enable_offenses
ui.plain_line("No errors, warnings, or offenses")
if o["format"] == "json"
puts JSON.generate(result)
else
ui.plain_line("No errors or warnings")
end
else
item_msg = lambda { |item|
pos = [item[:file], item[:line], item[:column]].compact.join(":")
pos.empty? ? item[:msg] : pos + ": " + item[:msg]
}
result[:errors].each { |item| ui.red " #{Inspec::UI::GLYPHS[:script_x]} #{item_msg.call(item)}\n" }
result[:warnings].each { |item| ui.yellow " ! #{item_msg.call(item)}\n" }
puts
if enable_offenses && !result[:offenses].empty?
puts "Offenses:\n"
result[:offenses].each { |item| ui.cyan(" #{Inspec::UI::GLYPHS[:script_x]} #{item_msg.call(item)}\n\n") }
end
offenses = ui.cyan("#{result[:offenses].length} offenses", print: false)
errors = ui.red("#{result[:errors].length} errors", print: false)
warnings = ui.yellow("#{result[:warnings].length} warnings", print: false)
if enable_offenses
ui.plain_line("Summary: #{errors}, #{warnings}, #{offenses}")
else
ui.plain_line("Summary: #{errors}, #{warnings}")
%w{location profile controls timestamp valid}.each do |item|
prepared_string = format("%-12s %s",
"#{item.to_s.capitalize} :",
result[:summary][item.to_sym])
ui.plain_line(prepared_string)
end
puts
enable_offenses = !Inspec.locally_windows? # See 5723
if result[:errors].empty? && result[:warnings].empty? && result[:offenses].empty?
if enable_offenses
ui.plain_line("No errors, warnings, or offenses")
else
ui.plain_line("No errors or warnings")
end
else
item_msg = lambda { |item|
pos = [item[:file], item[:line], item[:column]].compact.join(":")
pos.empty? ? item[:msg] : pos + ": " + item[:msg]
}
result[:errors].each { |item| ui.red " #{Inspec::UI::GLYPHS[:script_x]} #{item_msg.call(item)}\n" }
result[:warnings].each { |item| ui.yellow " ! #{item_msg.call(item)}\n" }
puts
if enable_offenses && !result[:offenses].empty?
puts "Offenses:\n"
result[:offenses].each { |item| ui.cyan(" #{Inspec::UI::GLYPHS[:script_x]} #{item_msg.call(item)}\n\n") }
end
offenses = ui.cyan("#{result[:offenses].length} offenses", print: false)
errors = ui.red("#{result[:errors].length} errors", print: false)
warnings = ui.yellow("#{result[:warnings].length} warnings", print: false)
if enable_offenses
ui.plain_line("Summary: #{errors}, #{warnings}, #{offenses}")
else
ui.plain_line("Summary: #{errors}, #{warnings}")
end
end
end
ui.exit Inspec::UI::EXIT_USAGE_ERROR unless result[:summary][:valid]
rescue StandardError => e
pretty_handle_exception(e)
end
end
ui.exit Inspec::UI::EXIT_USAGE_ERROR unless result[:summary][:valid]
rescue StandardError => e
pretty_handle_exception(e)
}
end
desc "vendor PATH", "Download all dependencies and generate a lockfile in a `vendor` directory"
option :overwrite, type: :boolean, default: false,
desc: "Overwrite existing vendored dependencies and lockfile."
def vendor(path = nil)
o = config
configure_logger(o)
o[:logger] = Logger.new($stdout)
o[:logger].level = get_log_level(o[:log_level])
Inspec.with_feature("inspec-cli-vendor") {
o = config
configure_logger(o)
o[:logger] = Logger.new($stdout)
o[:logger].level = get_log_level(o[:log_level])
vendor_deps(path, o)
vendor_deps(path, o)
}
end
desc "archive PATH", "Archive a profile to a tar file (default) or zip file."
@ -245,36 +262,40 @@ class Inspec::InspecCLI < Inspec::BaseCLI
option :ignore_errors, type: :boolean, default: false,
desc: "Ignore profile warnings."
def archive(path, log_level = nil)
o = config
diagnose(o)
Inspec.with_feature("inspec-cli-archive") {
begin
o = config
diagnose(o)
o[:logger] = Logger.new($stdout)
o[:logger].level = get_log_level(log_level || o[:log_level])
o[:backend] = Inspec::Backend.create(Inspec::Config.mock)
o[:logger] = Logger.new($stdout)
o[:logger].level = get_log_level(log_level || o[:log_level])
o[:backend] = Inspec::Backend.create(Inspec::Config.mock)
# Force vendoring with overwrite when archiving
vendor_options = o.dup
vendor_options[:overwrite] = true
vendor_deps(path, vendor_options)
# Force vendoring with overwrite when archiving
vendor_options = o.dup
vendor_options[:overwrite] = true
vendor_deps(path, vendor_options)
profile = Inspec::Profile.for_target(path, o)
gem_deps = profile.metadata.gem_dependencies + \
profile.locked_dependencies.list.map { |_k, v| v.profile.metadata.gem_dependencies }.flatten
unless gem_deps.empty?
o[:logger].warn "Archiving a profile that contains gem dependencies, but InSpec cannot package gems with the profile! Please archive your ~/.inspec/gems directory separately."
end
profile = Inspec::Profile.for_target(path, o)
gem_deps = profile.metadata.gem_dependencies + \
profile.locked_dependencies.list.map { |_k, v| v.profile.metadata.gem_dependencies }.flatten
unless gem_deps.empty?
o[:logger].warn "Archiving a profile that contains gem dependencies, but InSpec cannot package gems with the profile! Please archive your ~/.inspec/gems directory separately."
end
result = profile.check
result = profile.check
if result && !o[:ignore_errors] == false
o[:logger].info "Profile check failed. Please fix the profile before generating an archive."
return ui.exit Inspec::UI::EXIT_USAGE_ERROR
end
if result && !o[:ignore_errors] == false
o[:logger].info "Profile check failed. Please fix the profile before generating an archive."
return ui.exit Inspec::UI::EXIT_USAGE_ERROR
end
# generate archive
ui.exit Inspec::UI::EXIT_USAGE_ERROR unless profile.archive(o)
rescue StandardError => e
pretty_handle_exception(e)
# generate archive
ui.exit Inspec::UI::EXIT_USAGE_ERROR unless profile.archive(o)
rescue StandardError => e
pretty_handle_exception(e)
end
}
end
desc "exec LOCATIONS", "Run all test files at the specified locations."
@ -353,45 +374,53 @@ class Inspec::InspecCLI < Inspec::BaseCLI
EOT
exec_options
def exec(*targets)
o = config
diagnose(o)
deprecate_target_id(config)
configure_logger(o)
Inspec.with_feature("inspec-cli-exec") {
begin
o = config
diagnose(o)
deprecate_target_id(config)
configure_logger(o)
runner = Inspec::Runner.new(o)
targets.each { |target| runner.add_target(target) }
runner = Inspec::Runner.new(o)
targets.each { |target| runner.add_target(target) }
ui.exit runner.run
rescue ArgumentError, RuntimeError, Train::UserError => e
$stderr.puts e.message
ui.exit Inspec::UI::EXIT_USAGE_ERROR
rescue StandardError => e
pretty_handle_exception(e)
ui.exit runner.run
rescue ArgumentError, RuntimeError, Train::UserError => e
$stderr.puts e.message
ui.exit Inspec::UI::EXIT_USAGE_ERROR
rescue StandardError => e
pretty_handle_exception(e)
end
}
end
desc "detect", "detects the target OS."
target_options
option :format, type: :string
def detect
o = config
deprecate_target_id(config)
o[:command] = "platform.params"
Inspec.with_feature("inspec-cli-detect") {
begin
o = config
deprecate_target_id(config)
o[:command] = "platform.params"
configure_logger(o)
configure_logger(o)
(_, res) = run_command(o)
(_, res) = run_command(o)
if o["format"] == "json"
puts res.to_json
else
ui.headline("Platform Details")
ui.plain Inspec::BaseCLI.format_platform_info(params: res, indent: 0, color: 36, enable_color: ui.color?)
end
rescue ArgumentError, RuntimeError, Train::UserError => e
$stderr.puts e.message
ui.exit Inspec::UI::EXIT_USAGE_ERROR
rescue StandardError => e
pretty_handle_exception(e)
if o["format"] == "json"
puts res.to_json
else
ui.headline("Platform Details")
ui.plain Inspec::BaseCLI.format_platform_info(params: res, indent: 0, color: 36, enable_color: ui.color?)
end
rescue ArgumentError, RuntimeError, Train::UserError => e
$stderr.puts e.message
ui.exit Inspec::UI::EXIT_USAGE_ERROR
rescue StandardError => e
pretty_handle_exception(e)
end
}
end
desc "shell", "open an interactive debugging shell."
@ -416,78 +445,94 @@ class Inspec::InspecCLI < Inspec::BaseCLI
option :enhanced_outcomes, type: :boolean,
desc: "Show enhanced outcomes in output"
def shell_func
o = config
deprecate_target_id(config)
diagnose(o)
o[:debug_shell] = true
Inspec.with_feature("inspec-cli-shell") {
begin
o = config
deprecate_target_id(config)
diagnose(o)
o[:debug_shell] = true
Inspec::Resource.toggle_inspect unless o[:inspect]
Inspec::Resource.toggle_inspect unless o[:inspect]
log_device = suppress_log_output?(o) ? nil : $stdout
o[:logger] = Logger.new(log_device)
o[:logger].level = get_log_level(o[:log_level])
log_device = suppress_log_output?(o) ? nil : $stdout
o[:logger] = Logger.new(log_device)
o[:logger].level = get_log_level(o[:log_level])
if o[:command].nil?
runner = Inspec::Runner.new(o)
return Inspec::Shell.new(runner).start
end
if o[:command].nil?
runner = Inspec::Runner.new(o)
return Inspec::Shell.new(runner).start
end
run_type, res = run_command(o)
ui.exit res unless run_type == :ruby_eval
run_type, res = run_command(o)
ui.exit res unless run_type == :ruby_eval
# No InSpec tests - just print evaluation output.
reporters = o["reporter"] || {}
if reporters.keys.include?("json")
res = if res.respond_to?(:to_json)
res.to_json
else
JSON.dump(res)
end
end
# No InSpec tests - just print evaluation output.
reporters = o["reporter"] || {}
if reporters.keys.include?("json")
res = if res.respond_to?(:to_json)
res.to_json
else
JSON.dump(res)
end
end
puts res
ui.exit Inspec::UI::EXIT_NORMAL
rescue RuntimeError, Train::UserError => e
$stderr.puts e.message
rescue StandardError => e
pretty_handle_exception(e)
puts res
ui.exit Inspec::UI::EXIT_NORMAL
rescue RuntimeError, Train::UserError => e
$stderr.puts e.message
rescue StandardError => e
pretty_handle_exception(e)
end
}
end
desc "env", "Outputs shell-appropriate completion configuration."
def env(shell = nil)
p = Inspec::EnvPrinter.new(self.class, shell)
p.print_and_exit!
rescue StandardError => e
pretty_handle_exception(e)
Inspec.with_feature("inspec-cli-env") {
begin
p = Inspec::EnvPrinter.new(self.class, shell)
p.print_and_exit!
rescue StandardError => e
pretty_handle_exception(e)
end
}
end
option :enhanced_outcomes, type: :boolean,
desc: "Show enhanced outcomes output"
desc "schema NAME", "print the JSON schema", hide: true
def schema(name)
require "inspec/schema/output_schema"
o = config
puts Inspec::Schema::OutputSchema.json(name, o)
rescue StandardError => e
puts e
puts "Valid schemas are #{Inspec::Schema::OutputSchema.names.join(", ")}"
Inspec.with_feature("inspec-cli-schema") {
begin
require "inspec/schema/output_schema"
o = config
puts Inspec::Schema::OutputSchema.json(name, o)
rescue StandardError => e
puts e
puts "Valid schemas are #{Inspec::Schema::OutputSchema.names.join(", ")}"
end
}
end
desc "run_context", "used to test run-context detection", hide: true
def run_context
require "inspec/utils/telemetry/run_context_probe"
puts Inspec::Telemetry::RunContextProbe.guess_run_context
Inspec.with_feature("inspec-cli-run-context") {
require "inspec/utils/telemetry/run_context_probe"
puts Inspec::Telemetry::RunContextProbe.guess_run_context
}
end
desc "version", "prints the version of this tool."
option :format, type: :string
def version
if config["format"] == "json"
v = { version: Inspec::VERSION }
puts v.to_json
else
puts Inspec::VERSION
end
Inspec.with_feature("inspec-cli-version") {
if config["format"] == "json"
v = { version: Inspec::VERSION }
puts v.to_json
else
puts Inspec::VERSION
end
}
end
map %w{-v --version} => :version
@ -495,14 +540,16 @@ class Inspec::InspecCLI < Inspec::BaseCLI
option :vendor_cache, type: :string,
desc: "Use the given path for caching dependencies, (default: `~/.inspec/cache`)."
def clear_cache
o = config
configure_logger(o)
cache_path = o[:vendor_cache] || "~/.inspec/cache"
FileUtils.rm_r Dir.glob(File.expand_path(cache_path))
Inspec.with_feature("inspec-cli-clear-cache") {
o = config
configure_logger(o)
cache_path = o[:vendor_cache] || "~/.inspec/cache"
FileUtils.rm_r Dir.glob(File.expand_path(cache_path))
o[:logger] = Logger.new($stdout)
o[:logger].level = get_log_level(o[:log_level])
o[:logger].info "== InSpec cache cleared successfully =="
o[:logger] = Logger.new($stdout)
o[:logger].level = get_log_level(o[:log_level])
o[:logger].info "== InSpec cache cleared successfully =="
}
end
private

View file

@ -70,5 +70,38 @@ module Inspec
def base_path_for(cache_key)
File.join(@path, cache_key)
end
#
# For given cache key, return true if the
# cache path is locked
def locked?(key)
locked = false
path = base_path_for(key)
# For archive there is no need to lock the directory so we skip those and return false for archive formatted cache
if File.directory?(path)
locked = File.exist?("#{path}/.lock")
end
locked
end
def lock(cache_path)
lock_file_path = File.join(cache_path, ".lock")
begin
FileUtils.mkdir_p(cache_path)
Inspec::Log.debug("Locking cache ..... #{cache_path}")
FileUtils.touch(lock_file_path)
rescue Errno::EACCES
raise "Permission denied while creating cache lock #{cache_path}/.lock."
end
end
def unlock(cache_path)
Inspec::Log.debug("Unlocking cache..... #{cache_path}")
begin
FileUtils.rm("#{cache_path}/.lock") if File.exist?("#{cache_path}/.lock")
rescue Errno::EACCES
raise "Permission denied while removing cache lock #{cache_path}/.lock"
end
end
end
end

View file

@ -15,5 +15,6 @@ module Inspec
"passed"
end
end
end
end

View file

@ -24,4 +24,7 @@ module Inspec
end
class InvalidProfileSignature < Error; end
class FeatureConfigMissingError < Error; end
class FeatureConfigTamperedError < Error; end
end

17
lib/inspec/feature.rb Normal file
View file

@ -0,0 +1,17 @@
require_relative "feature/config"
require_relative "feature/runner"
module Inspec
def self.with_feature(feature_name, opts = {}, &feature_implementation)
Inspec::Feature::Runner.with_feature(feature_name, opts, &feature_implementation)
end
class Feature
attr_reader :name, :description
def initialize(feature_name, feature_yaml_opts)
@name = feature_name
feature_yaml_opts ||= {}
@description = feature_yaml_opts["description"]
end
end
end

View file

@ -0,0 +1,75 @@
require "inspec/iaf_file" # Uses some of the same encryption routines
module Inspec
class Feature
class Config
VERIFICATION_KEY_NAME = "progress-2022-05-04".freeze
attr_reader :cfg_data, :valid
def initialize(conf_path = nil)
# If conf path is nil, read from source installation
conf_path ||= File.join(Inspec.src_root, "etc", "features.yaml")
# Verify path and sig file exists or else throw exception
sig_path = conf_path.sub(/\.yaml/, ".sig")
[conf_path, sig_path].each do |file|
raise Inspec::FeatureConfigMissingError.new("No such file #{file}") unless File.exist?(file)
end
# Verify sig matches contents
validation_key_path = Inspec::IafFile.find_validation_key(VERIFICATION_KEY_NAME)
verification_key = Inspec::IafFile::KEY_ALG.new File.read validation_key_path
signature = Base64.decode64 File.read sig_path
digest = Inspec::IafFile::ARTIFACT_DIGEST.new
unless verification_key.verify digest, signature, File.read(conf_path)
# If not load default empty config and raise exception
@cfg_data = load_error_data
raise Inspec::FeatureConfigTamperedError.new("Feature yaml file does not match signature - tampered?")
end
# Read YAML data from path
@cfg_data = YAML.load_file(conf_path)
@features_by_name = {}
end
def with_each_feature
cfg_data["features"].each do |feature_name, raw_info|
feat = @features_by_name[feature_name] ||= Inspec::Feature.new(feature_name.to_sym, raw_info)
yield(feat)
end
end
def [](feature_name)
raw_info = cfg_data["features"][feature_name]
return nil unless raw_info
@features_by_name[feature_name] ||= Inspec::Feature.new(feature_name.to_sym, raw_info)
end
def feature_name?(query)
cfg_data["features"].key?(query.to_s)
end
def features
@features ||= load_features
end
private
def load_features
feats = []
with_each_feature { |f| feats << f }
feats
end
# Default data for when the config is in an error state.
def load_error_data
{
"features": {},
}
end
end
end
end

View file

@ -0,0 +1,21 @@
module Inspec
class Feature
class Runner
def self.with_feature(feature_name, opts = {}, &feature_implementation)
config = opts[:config] || Inspec::Feature::Config.new
logger = opts[:logger] || Inspec::Log
# Emit log message saying we're running a feature
logger.debug("Prepping to run feature '#{feature_name}'")
# Validate that the feature is recognized
feature = config[feature_name]
unless feature
logger.warn "Unrecognized feature name '#{feature_name}'"
end
yield feature_implementation
end
end
end
end

View file

@ -115,6 +115,11 @@ module Inspec::Fetcher
%i{branch tag ref}.map { |opt_name| update_ivar_from_opt(opt_name, opts) }.any?
end
# Git fetcher is sensitive to cache contention so it needs cache locking mechanism.
def requires_locking?
true
end
private
def resolved_ref

View file

@ -160,7 +160,7 @@ module Inspec::Formatters
end
# added this additionally because stats summary is also used for determining exit code in runner rspec
skipped += 1 if control[:results].any? { |r| r[:status] == "skipped" }
skipped += 1 if control[:results] && (control[:results].any? { |r| r[:status] == "skipped" })
end
total = error + not_applicable + not_reviewed + failed + passed

View file

@ -105,6 +105,13 @@ module Inspec
file_provider = Inspec::FileProvider.for_path(archive_path)
file_provider.relative_provider
end
# Returns false by default
# This is used to regulate cache contention.
# Fetchers that are sensitive to cache contention should return true.
def requires_locking?
false
end
end
end
end

View file

@ -12,6 +12,7 @@ module Inspec::Plugin::V2::PluginType
@control_checks_count_map = {}
@controls_count = nil
@notifications = {}
@enhanced_outcome_control_wise = {}
end
private
@ -29,16 +30,43 @@ module Inspec::Plugin::V2::PluginType
# method to identify when the control ended running
# this will be useful in executing operations on control's level end
def control_ended?(control_id)
def control_ended?(notification, control_id)
set_control_checks_count_map_value
unless @control_checks_count_map[control_id].nil?
@control_checks_count_map[control_id] -= 1
@control_checks_count_map[control_id] == 0
control_ended = @control_checks_count_map[control_id] == 0
# after a control has ended it checks for certain operations, like enhanced outcomes
run_control_operations(notification, control_id) if control_ended
control_ended
else
false
end
end
def run_control_operations(notification, control_id)
check_for_enhanced_outcomes(notification, control_id)
end
def check_for_enhanced_outcomes(notification, control_id)
if enhanced_outcomes
control_outcome = add_enhanced_outcomes(control_id)
@enhanced_outcome_control_wise[control_id] = control_outcome
end
end
def format_message(indicator, control_id, title, full_description)
message_to_format = ""
message_to_format += "#{indicator} "
message_to_format += "#{control_id.to_s.strip.dup.force_encoding(Encoding::UTF_8)} "
message_to_format += "#{title.gsub(/\n*\s+/, " ").to_s.force_encoding(Encoding::UTF_8)} " if title
message_to_format += "#{full_description.gsub(/\n*\s+/, " ").to_s.force_encoding(Encoding::UTF_8)} " unless title
message_to_format
end
def control_outcome(control_id)
@enhanced_outcome_control_wise[control_id]
end
# method to identify total no. of controls
def controls_count
@controls_count ||= RSpec.configuration.formatters.grep(Inspec::Formatters::Base).first.get_controls_count

View file

@ -4,76 +4,89 @@ require "inspec/reporters/json"
require "inspec/reporters/json_automate"
require "inspec/reporters/automate"
require "inspec/reporters/yaml"
require "inspec/feature"
module Inspec::Reporters
# rubocop:disable Metrics/CyclomaticComplexity
def self.render(reporter, run_data, enhanced_outcomes = false)
name, config = reporter.dup
config[:run_data] = run_data
case name
when "cli"
reporter = Inspec::Reporters::CLI.new(config)
when "json"
reporter = Inspec::Reporters::Json.new(config)
# This reporter is only used for Chef internal. We reserve the
# right to introduce breaking changes to this reporter at any time.
when "json-automate"
reporter = Inspec::Reporters::JsonAutomate.new(config)
when "automate"
reporter = Inspec::Reporters::Automate.new(config)
when "yaml"
reporter = Inspec::Reporters::Yaml.new(config)
else
# If we made it here, it must be a plugin, and we know it exists (because we validated it in config.rb)
activator = Inspec::Plugin::V2::Registry.instance.find_activator(plugin_type: :reporter, activator_name: name.to_sym)
activator.activate!
reporter = activator.implementation_class.new(config)
end
reporter.enhanced_outcomes = enhanced_outcomes
Inspec.with_feature("inspec-reporter-#{name}") {
config[:run_data] = run_data
case name
when "cli"
reporter = Inspec::Reporters::CLI.new(config)
when "json"
reporter = Inspec::Reporters::Json.new(config)
# This reporter is only used for Chef internal. We reserve the
# right to introduce breaking changes to this reporter at any time.
when "json-automate"
reporter = Inspec::Reporters::JsonAutomate.new(config)
when "automate"
reporter = Inspec::Reporters::Automate.new(config)
when "yaml"
reporter = Inspec::Reporters::Yaml.new(config)
else
# If we made it here, it must be a plugin, and we know it exists (because we validated it in config.rb)
activator = Inspec::Plugin::V2::Registry.instance.find_activator(plugin_type: :reporter, activator_name: name.to_sym)
activator.activate!
reporter = activator.implementation_class.new(config)
end
# optional send_report method on reporter
return reporter.send_report if defined?(reporter.send_report)
if enhanced_outcomes
Inspec.with_feature("inspec-enhanced-outcomes") {
reporter.enhanced_outcomes = enhanced_outcomes
}
else
reporter.enhanced_outcomes = enhanced_outcomes
end
reporter.render
output = reporter.rendered_output
# optional send_report method on reporter
return reporter.send_report if defined?(reporter.send_report)
if config["file"]
# create destination directory if it does not exist
dirname = File.dirname(config["file"])
FileUtils.mkdir_p(dirname) unless File.directory?(dirname)
reporter.render
output = reporter.rendered_output
config_file = config["file"]
if config_file
config_file.gsub!("CHILD_PID", Process.pid.to_s)
# create destination directory if it does not exist
dirname = File.dirname(config_file)
FileUtils.mkdir_p(dirname) unless File.directory?(dirname)
File.write(config["file"], output)
elsif config["stdout"] == true
print output
$stdout.flush
end
File.write(config_file, output)
elsif config["stdout"] == true
print output
$stdout.flush
end
}
end
def self.report(reporter, run_data)
name, config = reporter.dup
config[:run_data] = run_data
case name
when "json"
reporter = Inspec::Reporters::Json.new(config)
when "json-automate"
reporter = Inspec::Reporters::JsonAutomate.new(config)
when "yaml"
reporter = Inspec::Reporters::Yaml.new(config)
else
# If we made it here, it might be a plugin
begin
activator = Inspec::Plugin::V2::Registry.instance.find_activator(plugin_type: :reporter, activator_name: name.to_sym)
activator.activate!
reporter = activator.implementation_class.new(config)
unless reporter.respond_to(:report?)
Inspec.with_feature("inspec-reporter-#{name}") {
config[:run_data] = run_data
case name
when "json"
reporter = Inspec::Reporters::Json.new(config)
when "json-automate"
reporter = Inspec::Reporters::JsonAutomate.new(config)
when "yaml"
reporter = Inspec::Reporters::Yaml.new(config)
else
# If we made it here, it might be a plugin
begin
activator = Inspec::Plugin::V2::Registry.instance.find_activator(plugin_type: :reporter, activator_name: name.to_sym)
activator.activate!
reporter = activator.implementation_class.new(config)
unless reporter.respond_to(:report?)
return run_data
end
rescue Inspec::Plugin::V2::LoadError
# Must not have been a plugin - just return the run_data
return run_data
end
rescue Inspec::Plugin::V2::LoadError
# Must not have been a plugin - just return the run_data
return run_data
end
end
reporter.report
reporter.report
}
end
end

View file

@ -317,7 +317,7 @@ module Inspec::Reporters
not_applicable = 0
all_unique_controls.each do |control|
next if control[:status].empty?
next if control[:status].blank?
if control[:status] == "failed"
failed += 1

View file

@ -27,11 +27,13 @@ module Inspec
) do
include HashLikeStruct
def initialize(raw_run_data)
self.controls = raw_run_data[:controls].map { |c| Inspec::RunData::Control.new(c) }
self.profiles = raw_run_data[:profiles].map { |p| Inspec::RunData::Profile.new(p) }
self.statistics = Inspec::RunData::Statistics.new(raw_run_data[:statistics])
self.platform = Inspec::RunData::Platform.new(raw_run_data[:platform])
self.version = raw_run_data[:version]
@raw_run_data = raw_run_data
self.controls = @raw_run_data[:controls].map { |c| Inspec::RunData::Control.new(c) }
self.profiles = @raw_run_data[:profiles].map { |p| Inspec::RunData::Profile.new(p) }
self.statistics = Inspec::RunData::Statistics.new(@raw_run_data[:statistics])
self.platform = Inspec::RunData::Platform.new(@raw_run_data[:platform])
self.version = @raw_run_data[:version]
end
end

View file

@ -11,6 +11,7 @@ require "inspec/dependencies/cache"
require "inspec/dist"
require "inspec/reporters"
require "inspec/runner_rspec"
require "chef-licensing"
# spec requirements
module Inspec
@ -60,11 +61,13 @@ module Inspec
end
if @conf[:waiver_file]
@conf[:waiver_file].each do |file|
unless File.file?(file)
raise Inspec::Exceptions::WaiversFileDoesNotExist, "Waiver file #{file} does not exist."
Inspec.with_feature("inspec-waivers") {
@conf[:waiver_file].each do |file|
unless File.file?(file)
raise Inspec::Exceptions::WaiversFileDoesNotExist, "Waiver file #{file} does not exist."
end
end
end
}
end
# About reading inputs:
@ -159,16 +162,24 @@ module Inspec
end
def run(with = nil)
ChefLicensing.check_software_entitlement! if Inspec::Dist::EXEC_NAME == "inspec"
Inspec::Log.debug "Starting run with targets: #{@target_profiles.map(&:to_s)}"
load
run_tests(with)
rescue ChefLicensing::SoftwareNotEntitled
Inspec::Log.error "License is not entitled to use InSpec."
Inspec::UI.new.exit(:license_not_entitled)
rescue ChefLicensing::Error => e
Inspec::Log.error e.message
Inspec::UI.new.exit(:usage_error)
end
def render_output(run_data)
return if @conf["reporter"].nil?
@conf["reporter"].each do |reporter|
result = Inspec::Reporters.render(reporter, run_data, @conf["enhanced_outcomes"])
enhanced_outcome_flag = @conf["enhanced_outcomes"]
result = Inspec::Reporters.render(reporter, run_data, enhanced_outcome_flag)
raise Inspec::ReporterError, "Error generating reporter '#{reporter[0]}'" if result == false
end
end

View file

@ -177,16 +177,18 @@ module Inspec
next unless streaming_reporters.include? streaming_reporter_name
# Activate the plugin so the formatter ID gets registered with RSpec, presumably
activator = reg.find_activator(plugin_type: :streaming_reporter, activator_name: streaming_reporter_name.to_sym)
activator.activate!
# We cannot pass in a nil output path. Rspec only accepts a valid string or a IO object.
if file_target&.[]("file").nil?
RSpec.configuration.add_formatter(activator.implementation_class)
else
RSpec.configuration.add_formatter(activator.implementation_class, file_target["file"])
end
@conf["reporter"].delete(streaming_reporter_name)
Inspec.with_feature("inspec-reporter-#{streaming_reporter_name}") {
activator = reg.find_activator(plugin_type: :streaming_reporter, activator_name: streaming_reporter_name.to_sym)
activator.activate!
# We cannot pass in a nil output path. Rspec only accepts a valid string or a IO object.
if file_target&.[]("file").nil?
RSpec.configuration.add_formatter(activator.implementation_class)
else
RSpec.configuration.add_formatter(activator.implementation_class, file_target["file"])
end
@conf["reporter"].delete(streaming_reporter_name)
}
end
end
@ -196,6 +198,7 @@ module Inspec
def configure_output
RSpec.configuration.output_stream = $stdout
@formatter = RSpec.configuration.add_formatter(Inspec::Formatters::Base)
@formatter.enhanced_outcomes = @conf.final_options["enhanced_outcomes"]
RSpec.configuration.add_formatter(Inspec::Formatters::ShowProgress, $stderr) if @conf[:show_progress]
set_optional_formatters

View file

@ -1,3 +1,6 @@
require "chef-licensing"
require "inspec/dist"
autoload :Pry, "pry"
module Inspec
@ -10,6 +13,7 @@ module Inspec
end
def start
ChefLicensing.check_software_entitlement! if Inspec::Dist::EXEC_NAME == "inspec"
# This will hold a single evaluation binding context as opened within
# the instance_eval context of the anonymous class that the profile
# context creates to evaluate each individual test file. We want to
@ -18,6 +22,12 @@ module Inspec
@ctx_binding = @runner.eval_with_virtual_profile("binding")
configure_pry
@ctx_binding.pry
rescue ChefLicensing::SoftwareNotEntitled
Inspec::Log.error "License is not entitled to use InSpec."
Inspec::UI.new.exit(:license_not_entitled)
rescue ChefLicensing::Error => e
Inspec::Log.error e.message
Inspec::UI.new.exit(:usage_error)
end
def configure_pry # rubocop:disable Metrics/AbcSize

View file

@ -33,8 +33,11 @@ module Inspec
EXIT_GEM_DEPENDENCY_LOAD_ERROR = 4
EXIT_BAD_SIGNATURE = 5
EXIT_LICENSE_NOT_ACCEPTED = 172
EXIT_LICENSE_NOT_ENTITLED = 173
EXIT_LICENSE_NOT_SET = 174
EXIT_FAILED_TESTS = 100
EXIT_SKIPPED_TESTS = 101
EXIT_TERMINATED_BY_CTL_C = 130
attr_reader :io

View file

@ -0,0 +1,9 @@
require_relative "../log"
require "chef-licensing"
ChefLicensing.configure do |config|
config.chef_product_name = "InSpec"
config.chef_entitlement_id = "3ff52c37-e41f-4f6c-ad4d-365192205968"
config.chef_executable_name = "inspec"
config.license_server_url = "https://licensing.chef.co/License"
config.logger = Inspec::Log
end

View file

@ -1,3 +1,3 @@
module Inspec
VERSION = "5.22.13".freeze
VERSION = "6.4.45".freeze
end

View file

@ -15,22 +15,7 @@ module Inspec
output = {}
files.each do |file_path|
file_extension = File.extname(file_path)
data = nil
if [".yaml", ".yml"].include? file_extension
data = Secrets::YAML.resolve(file_path)
unless data.nil?
data = data.inputs
validate_json_yaml(data)
end
elsif file_extension == ".csv"
data = Waivers::CSVFileReader.resolve(file_path)
headers = Waivers::CSVFileReader.headers
validate_headers(headers)
elsif file_extension == ".json"
data = Waivers::JSONFileReader.resolve(file_path)
validate_json_yaml(data) unless data.nil?
end
data = read_from_file(file_path)
output.merge!(data) if !data.nil? && data.is_a?(Hash)
if data.nil?
@ -43,10 +28,30 @@ module Inspec
@waivers_data[profile_id] = output
end
def self.read_from_file(file_path)
data = nil
file_extension = File.extname(file_path)
if [".yaml", ".yml"].include? file_extension
data = Secrets::YAML.resolve(file_path)
data = data.inputs unless data.nil?
validate_json_yaml(data)
elsif file_extension == ".csv"
data = Waivers::CSVFileReader.resolve(file_path)
headers = Waivers::CSVFileReader.headers
validate_headers(headers)
elsif file_extension == ".json"
data = Waivers::JSONFileReader.resolve(file_path)
validate_json_yaml(data) unless data.nil?
end
data
end
def self.all_fields
%w{control_id justification expiration_date run}
end
def self.validate_headers(headers, json_yaml = false)
required_fields = json_yaml ? %w{justification} : %w{control_id justification}
all_fields = %w{control_id justification expiration_date run}
Inspec::Log.warn "Missing column headers: #{(required_fields - headers)}" unless (required_fields - headers).empty?
Inspec::Log.warn "Invalid column header: Column can't be nil" if headers.include? nil
Inspec::Log.warn "Extra column headers: #{(headers - all_fields)}" unless (headers - all_fields).empty?

View file

@ -1,6 +1,7 @@
require "inspec/dist"
require_relative "api"
require "inspec/feature"
module InspecPlugins
module Compliance
@ -32,90 +33,102 @@ module InspecPlugins
option :ent, type: :string, required: false,
desc: "Enterprise for #{AUTOMATE_PRODUCT_NAME} reporting (#{AUTOMATE_PRODUCT_NAME} Only)"
def login(server)
options["server"] = server
login_response = InspecPlugins::Compliance::API.login(options)
puts login_response
Inspec.with_feature("inspec-cli-compliance-login") {
options["server"] = server
login_response = InspecPlugins::Compliance::API.login(options)
puts login_response
}
end
desc "profiles", "list all available profiles in #{AUTOMATE_PRODUCT_NAME}"
option :owner, type: :string, required: false,
desc: "owner whose profiles to list"
def profiles
config = InspecPlugins::Compliance::Configuration.new
return unless loggedin(config)
Inspec.with_feature("inspec-cli-compliance-profiles") {
begin
config = InspecPlugins::Compliance::Configuration.new
return unless loggedin(config)
# set owner to config
config["owner"] = options["owner"] || config["user"]
# set owner to config
config["owner"] = options["owner"] || config["user"]
msg, profiles = InspecPlugins::Compliance::API.profiles(config)
profiles.sort_by! { |hsh| hsh["title"] }
if !profiles.empty?
# iterate over profiles
headline("Available profiles:")
profiles.each do |profile|
owner = profile["owner_id"] || profile["owner"]
li("#{profile["title"]} v#{profile["version"]} (#{mark_text(owner + "/" + profile["name"])})")
msg, profiles = InspecPlugins::Compliance::API.profiles(config)
profiles.sort_by! { |hsh| hsh["title"] }
if !profiles.empty?
# iterate over profiles
headline("Available profiles:")
profiles.each do |profile|
owner = profile["owner_id"] || profile["owner"]
li("#{profile["title"]} v#{profile["version"]} (#{mark_text(owner + "/" + profile["name"])})")
end
else
puts msg if msg != "success"
puts "Could not find any profiles"
exit 1
end
rescue InspecPlugins::Compliance::ServerConfigurationMissing
$stderr.puts "\nServer configuration information is missing. Please login using `#{EXEC_NAME} #{subcommand_name} login`"
exit 1
end
else
puts msg if msg != "success"
puts "Could not find any profiles"
exit 1
end
rescue InspecPlugins::Compliance::ServerConfigurationMissing
$stderr.puts "\nServer configuration information is missing. Please login using `#{EXEC_NAME} #{subcommand_name} login`"
exit 1
}
end
desc "exec PROFILE", "executes a #{AUTOMATE_PRODUCT_NAME} profile"
exec_options
def exec(*tests)
compliance_config = InspecPlugins::Compliance::Configuration.new
return unless loggedin(compliance_config)
Inspec.with_feature("inspec-cli-compliance-exec") {
begin
compliance_config = InspecPlugins::Compliance::Configuration.new
return unless loggedin(compliance_config)
o = config # o is an Inspec::Config object, provided by a helper method from Inspec::BaseCLI
diagnose(o)
configure_logger(o)
o = config # o is an Inspec::Config object, provided by a helper method from Inspec::BaseCLI
diagnose(o)
configure_logger(o)
# iterate over tests and add compliance scheme
tests = tests.map { |t| "compliance://" + InspecPlugins::Compliance::API.sanitize_profile_name(t) }
# iterate over tests and add compliance scheme
tests = tests.map { |t| "compliance://" + InspecPlugins::Compliance::API.sanitize_profile_name(t) }
runner = Inspec::Runner.new(o)
tests.each { |target| runner.add_target(target) }
runner = Inspec::Runner.new(o)
tests.each { |target| runner.add_target(target) }
exit runner.run
rescue ArgumentError, RuntimeError, Train::UserError => e
$stderr.puts e.message
exit 1
exit runner.run
rescue ArgumentError, RuntimeError, Train::UserError => e
$stderr.puts e.message
exit 1
end
}
end
desc "download PROFILE", "downloads a profile from #{AUTOMATE_PRODUCT_NAME}"
option :name, type: :string,
desc: "Name of the archive filename (file type will be added)"
def download(profile_name)
o = options.dup
configure_logger(o)
Inspec.with_feature("inspec-cli-compliance-download") {
o = options.dup
configure_logger(o)
config = InspecPlugins::Compliance::Configuration.new
return unless loggedin(config)
config = InspecPlugins::Compliance::Configuration.new
return unless loggedin(config)
profile_name = InspecPlugins::Compliance::API.sanitize_profile_name(profile_name)
if InspecPlugins::Compliance::API.exist?(config, profile_name)
puts "Downloading `#{profile_name}`"
profile_name = InspecPlugins::Compliance::API.sanitize_profile_name(profile_name)
if InspecPlugins::Compliance::API.exist?(config, profile_name)
puts "Downloading `#{profile_name}`"
fetcher = InspecPlugins::Compliance::Fetcher.resolve(
{
compliance: profile_name,
}
)
fetcher = InspecPlugins::Compliance::Fetcher.resolve(
{
compliance: profile_name,
}
)
# we provide a name, the fetcher adds the extension
_owner, id = profile_name.split("/")
file_name = fetcher.fetch(o.name || id)
puts "Profile stored to #{file_name}"
else
puts "Profile #{profile_name} is not available in #{AUTOMATE_PRODUCT_NAME}."
exit 1
end
# we provide a name, the fetcher adds the extension
_owner, id = profile_name.split("/")
file_name = fetcher.fetch(o.name || id)
puts "Profile stored to #{file_name}"
else
puts "Profile #{profile_name} is not available in #{AUTOMATE_PRODUCT_NAME}."
exit 1
end
}
end
desc "upload PATH", "uploads a local profile to #{AUTOMATE_PRODUCT_NAME}"
@ -124,129 +137,137 @@ module InspecPlugins
option :owner, type: :string, required: false,
desc: "Owner that should own the profile"
def upload(path) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
config = InspecPlugins::Compliance::Configuration.new
return unless loggedin(config)
Inspec.with_feature("inspec-cli-compliance-upload") {
config = InspecPlugins::Compliance::Configuration.new
return unless loggedin(config)
# set owner to config
config["owner"] = options["owner"] || config["user"]
# set owner to config
config["owner"] = options["owner"] || config["user"]
unless File.exist?(path)
puts "Directory #{path} does not exist."
exit 1
end
unless File.exist?(path)
puts "Directory #{path} does not exist."
exit 1
end
vendor_deps(path, options) if File.directory?(path)
vendor_deps(path, options) if File.directory?(path)
o = options.dup
configure_logger(o)
o = options.dup
configure_logger(o)
# only run against the mock backend, otherwise we run against the local system
o[:backend] = Inspec::Backend.create(Inspec::Config.mock)
o[:check_mode] = true
o[:vendor_cache] = Inspec::Cache.new(o[:vendor_cache])
# only run against the mock backend, otherwise we run against the local system
o[:backend] = Inspec::Backend.create(Inspec::Config.mock)
o[:check_mode] = true
o[:vendor_cache] = Inspec::Cache.new(o[:vendor_cache])
# check the profile, we only allow to upload valid profiles
profile = Inspec::Profile.for_target(path, o)
# check the profile, we only allow to upload valid profiles
profile = Inspec::Profile.for_target(path, o)
# start verification process
error_count = 0
error = lambda { |msg|
error_count += 1
puts msg
# start verification process
error_count = 0
error = lambda { |msg|
error_count += 1
puts msg
}
result = profile.check
unless result[:summary][:valid]
error.call("Profile check failed. Please fix the profile before upload.")
else
puts("Profile is valid")
end
# determine user information
if (config["token"].nil? && config["refresh_token"].nil?) || config["user"].nil?
error.call("Please login via `#{EXEC_NAME} #{subcommand_name} login`")
end
# read profile name from inspec.yml
profile_name = profile.params[:name]
# read profile version from inspec.yml
profile_version = profile.params[:version]
# check that the profile is not uploaded already,
# confirm upload to the user (overwrite with --force)
if InspecPlugins::Compliance::API.exist?(config, "#{config["owner"]}/#{profile_name}##{profile_version}") && !options["overwrite"]
error.call("Profile exists on the server, use --overwrite")
end
# abort if we found an error
if error_count > 0
puts "Found #{error_count} error(s)"
exit 1
end
# if it is a directory, tar it to tmp directory
generated = false
if File.directory?(path)
generated = true
archive_path = Dir::Tmpname.create([profile_name, ".tar.gz"]) {}
puts "Generate temporary profile archive at #{archive_path}"
profile.archive({ output: archive_path, ignore_errors: false, overwrite: true })
else
archive_path = path
end
puts "Start upload to #{config["owner"]}/#{profile_name}"
pname = ERB::Util.url_encode(profile_name)
puts "Uploading to #{AUTOMATE_PRODUCT_NAME}"
success, msg = InspecPlugins::Compliance::API.upload(config, config["owner"], pname, archive_path)
# delete temp file if it was temporary generated
File.delete(archive_path) if generated && File.exist?(archive_path)
if success
puts "Successfully uploaded profile"
else
puts "Error during profile upload:"
puts msg
exit 1
end
}
result = profile.check
unless result[:summary][:valid]
error.call("Profile check failed. Please fix the profile before upload.")
else
puts("Profile is valid")
end
# determine user information
if (config["token"].nil? && config["refresh_token"].nil?) || config["user"].nil?
error.call("Please login via `#{EXEC_NAME} #{subcommand_name} login`")
end
# read profile name from inspec.yml
profile_name = profile.params[:name]
# read profile version from inspec.yml
profile_version = profile.params[:version]
# check that the profile is not uploaded already,
# confirm upload to the user (overwrite with --force)
if InspecPlugins::Compliance::API.exist?(config, "#{config["owner"]}/#{profile_name}##{profile_version}") && !options["overwrite"]
error.call("Profile exists on the server, use --overwrite")
end
# abort if we found an error
if error_count > 0
puts "Found #{error_count} error(s)"
exit 1
end
# if it is a directory, tar it to tmp directory
generated = false
if File.directory?(path)
generated = true
archive_path = Dir::Tmpname.create([profile_name, ".tar.gz"]) {}
puts "Generate temporary profile archive at #{archive_path}"
profile.archive({ output: archive_path, ignore_errors: false, overwrite: true })
else
archive_path = path
end
puts "Start upload to #{config["owner"]}/#{profile_name}"
pname = ERB::Util.url_encode(profile_name)
puts "Uploading to #{AUTOMATE_PRODUCT_NAME}"
success, msg = InspecPlugins::Compliance::API.upload(config, config["owner"], pname, archive_path)
# delete temp file if it was temporary generated
File.delete(archive_path) if generated && File.exist?(archive_path)
if success
puts "Successfully uploaded profile"
else
puts "Error during profile upload:"
puts msg
exit 1
end
end
desc "version", "displays the version of the #{AUTOMATE_PRODUCT_NAME} server"
def version
config = InspecPlugins::Compliance::Configuration.new
info = InspecPlugins::Compliance::API.version(config)
if !info.nil? && info["build_timestamp"]
# key info["api"] is not longer available in latest version api response
puts "Name: automate"
puts "Version: #{info["build_timestamp"]}"
else
puts "Could not determine server version."
exit 1
end
rescue InspecPlugins::Compliance::ServerConfigurationMissing
puts "\nServer configuration information is missing. Please login using `#{EXEC_NAME} #{subcommand_name} login`"
exit 1
Inspec.with_feature("inspec-cli-compliance-version") {
begin
config = InspecPlugins::Compliance::Configuration.new
info = InspecPlugins::Compliance::API.version(config)
if !info.nil? && info["build_timestamp"]
# key info["api"] is not longer available in latest version api response
puts "Name: automate"
puts "Version: #{info["build_timestamp"]}"
else
puts "Could not determine server version."
exit 1
end
rescue InspecPlugins::Compliance::ServerConfigurationMissing
puts "\nServer configuration information is missing. Please login using `#{EXEC_NAME} #{subcommand_name} login`"
exit 1
end
}
end
desc "logout", "user logout from #{AUTOMATE_PRODUCT_NAME}"
def logout
config = InspecPlugins::Compliance::Configuration.new
unless config.supported?(:oidc) || config["token"].nil? || config["server_type"] == "automate"
Inspec.with_feature("inspec-cli-compliance-logout") {
config = InspecPlugins::Compliance::Configuration.new
url = "#{config["server"]}/logout"
InspecPlugins::Compliance::HTTP.post(url, config["token"], config["insecure"], !config.supported?(:oidc))
end
success = config.destroy
unless config.supported?(:oidc) || config["token"].nil? || config["server_type"] == "automate"
config = InspecPlugins::Compliance::Configuration.new
url = "#{config["server"]}/logout"
InspecPlugins::Compliance::HTTP.post(url, config["token"], config["insecure"], !config.supported?(:oidc))
end
success = config.destroy
if success
puts "Successfully logged out"
else
puts "Could not log out"
end
if success
puts "Successfully logged out"
else
puts "Could not log out"
end
}
end
private

View file

@ -1,5 +1,6 @@
require_relative "profile"
require "inspec/dist"
require "inspec/feature"
module InspecPlugins
module Habitat
@ -14,17 +15,23 @@ module InspecPlugins
option :output_dir, type: :string, required: false,
desc: "Output directory for the Habitat artifact. Default: current directory"
def create(path = ".")
InspecPlugins::Habitat::Profile.new(path, options).create
Inspec.with_feature("inspec-cli-habitat-profile-create") {
InspecPlugins::Habitat::Profile.new(path, options).create
}
end
desc "setup PATH", "Configure the profile at PATH for Habitat, including a plan and hooks"
def setup(path = ".")
InspecPlugins::Habitat::Profile.new(path, options).setup
Inspec.with_feature("inspec-cli-habitat-profile-setup") {
InspecPlugins::Habitat::Profile.new(path, options).setup
}
end
desc "upload PATH", "Create then upload a Habitat artifact for the profile found at PATH to the Habitat Builder Depot"
def upload(path = ".")
InspecPlugins::Habitat::Profile.new(path, options).upload
Inspec.with_feature("inspec-cli-habitat-profile-upload") {
InspecPlugins::Habitat::Profile.new(path, options).upload
}
end
end

View file

@ -1,5 +1,6 @@
require "pathname" unless defined?(Pathname)
require_relative "renderer"
require "inspec/feature"
module InspecPlugins
module Init

View file

@ -27,33 +27,35 @@ module InspecPlugins
option :copyright, type: :string, default: nil, desc: "A copyright statement, to be added to LICENSE"
def plugin(plugin_name)
plugin_type = determine_plugin_type(plugin_name)
snake_case = plugin_name.tr("-", "_")
Inspec.with_feature("inspec-cli-init-plugin") {
plugin_type = determine_plugin_type(plugin_name)
snake_case = plugin_name.tr("-", "_")
# Handle deprecation of option --hook
unless options[:hook].nil?
Inspec.deprecate "cli_option_hook"
options[:activator] = options.delete(:hook)
end
# Handle deprecation of option --hook
unless options[:hook].nil?
Inspec.deprecate "cli_option_hook"
options[:activator] = options.delete(:hook)
end
template_vars = {
name: plugin_name,
plugin_name: plugin_name,
snake_case: snake_case,
}.merge(plugin_vars_from_opts)
template_vars = {
name: plugin_name,
plugin_name: plugin_name,
snake_case: snake_case,
}.merge(plugin_vars_from_opts)
template_path = File.join("plugins", plugin_type + "-plugin-template")
template_path = File.join("plugins", plugin_type + "-plugin-template")
render_opts = {
templates_path: TEMPLATES_PATH,
overwrite: options[:overwrite],
file_rename_map: make_rename_map(plugin_type, plugin_name, snake_case),
skip_files: make_skip_list(template_vars["activators"].keys),
render_opts = {
templates_path: TEMPLATES_PATH,
overwrite: options[:overwrite],
file_rename_map: make_rename_map(plugin_type, plugin_name, snake_case),
skip_files: make_skip_list(template_vars["activators"].keys),
}
renderer = InspecPlugins::Init::Renderer.new(ui, render_opts)
renderer.render_with_values(template_path, plugin_type + " plugin", template_vars)
}
renderer = InspecPlugins::Init::Renderer.new(ui, render_opts)
renderer.render_with_values(template_path, plugin_type + " plugin", template_vars)
end
private

View file

@ -25,22 +25,24 @@ module InspecPlugins
option :overwrite, type: :boolean, default: false,
desc: "Overwrites existing directory"
def profile(new_profile_name)
unless valid_profile_platforms.include?(options[:platform])
ui.error "Unable to generate profile: No template available for platform '#{options[:platform]}' (expected one of: #{valid_profile_platforms.join(", ")})"
ui.exit(:usage_error)
end
template_path = File.join("profiles", options[:platform])
Inspec.with_feature("inspec-cli-init-profile") {
unless valid_profile_platforms.include?(options[:platform])
ui.error "Unable to generate profile: No template available for platform '#{options[:platform]}' (expected one of: #{valid_profile_platforms.join(", ")})"
ui.exit(:usage_error)
end
template_path = File.join("profiles", options[:platform])
render_opts = {
templates_path: TEMPLATES_PATH,
overwrite: options[:overwrite],
}
renderer = InspecPlugins::Init::Renderer.new(ui, render_opts)
render_opts = {
templates_path: TEMPLATES_PATH,
overwrite: options[:overwrite],
}
renderer = InspecPlugins::Init::Renderer.new(ui, render_opts)
vars = {
name: new_profile_name,
vars = {
name: new_profile_name,
}
renderer.render_with_values(template_path, "profile", vars)
}
renderer.render_with_values(template_path, "profile", vars)
end
end
end

View file

@ -35,21 +35,23 @@ module InspecPlugins
# + Add --overwrite option
def resource(resource_name)
resource_vars_from_opts_resource
template_vars = {
name: options[:path], # This is used for the path prefix
resource_name: resource_name,
}
template_vars.merge!(options)
template_path = File.join("resources", template_vars["template"])
Inspec.with_feature("inspec-cli-init-resource") {
resource_vars_from_opts_resource
template_vars = {
name: options[:path], # This is used for the path prefix
resource_name: resource_name,
}
template_vars.merge!(options)
template_path = File.join("resources", template_vars["template"])
render_opts = {
templates_path: TEMPLATES_PATH,
overwrite: options[:overwrite],
file_rename_map: make_rename_map_resource(template_vars),
render_opts = {
templates_path: TEMPLATES_PATH,
overwrite: options[:overwrite],
file_rename_map: make_rename_map_resource(template_vars),
}
renderer = InspecPlugins::Init::Renderer.new(ui, render_opts)
renderer.render_with_values(template_path, "resource", template_vars)
}
renderer = InspecPlugins::Init::Renderer.new(ui, render_opts)
renderer.render_with_values(template_path, "resource", template_vars)
end
private

View file

@ -0,0 +1,16 @@
# License Plugin
## license list
Implements the `inspec license list` CLI command.
## license add
Implements the `inspec license add` CLI command.
### What This Plugin Does
This plugin consists of the following subcommands:
1. `add`: helps to add a new license
2. `list`: helps to list all the licenses for the current user

View file

@ -0,0 +1,6 @@
Gem::Specification.new do |spec|
spec.name = "inspec-license"
spec.summary = "Plugin to list user licenses."
spec.description = ""
spec.license = "Apache-2.0"
end

View file

@ -0,0 +1,14 @@
module InspecPlugins
module License
class Plugin < ::Inspec.plugin(2)
plugin_name :"inspec-license"
if Inspec::Dist::EXEC_NAME == "inspec"
cli_command :license do
require_relative "inspec-license/cli"
InspecPlugins::License::CLI
end
end
end
end
end

View file

@ -0,0 +1,26 @@
require "chef-licensing"
module InspecPlugins::License
class CLI < Inspec.plugin(2, :cli_command)
include Inspec::Dist
subcommand_desc "license SUBCOMMAND [options]", "Manage #{PRODUCT_NAME} license"
desc "list", "List licenses (not applicable to local licensing service)"
def list
ChefLicensing.list_license_keys_info
rescue ChefLicensing::Error => e
Inspec::Log.error e.message
Inspec::UI.new.exit(Inspec::UI::EXIT_LICENSE_NOT_SET)
end
desc "add", "Add a new license (not applicable to local licensing service)"
def add
ChefLicensing.add_license
rescue ChefLicensing::LicenseKeyFetcher::LicenseKeyAddNotAllowed => e
Inspec::Log.error e.message
Inspec::UI.new.exit(Inspec::UI::EXIT_LICENSE_NOT_SET)
rescue ChefLicensing::Error => e
Inspec::Log.error e.message
Inspec::UI.new.exit(Inspec::UI::EXIT_LICENSE_NOT_SET)
end
end
end

View file

@ -0,0 +1,27 @@
# Parallel Plugin
Plugin to handle parallel InSpec scan operations over multiple targets.
## parallel cli_command
Implements the `inspec parallel exec` CLI command.
## child-status Plugin
This reporter is an InSpec Streaming Reporter. It is used internally by inspec parallel to provide status updates on child processes.
### What This Plugin Does
For each control executed, after it is complete, the plugin emits a line to STDOUT like:
```
12/P/24/Control Title Here
```
When the run is complete, the single line 'EOF_MARKER' is emitted.
Where:
- 12 is the number of the control (12th seen out of all controls in all profiles)
- P indicates that it Passed (Also F = Failed, S = Skipped, E = Errored)
- 24 is the total number of controls in the run
- "Control Title Here" is the title (or if title is missing, id) of the last executed control

View file

@ -0,0 +1,6 @@
Gem::Specification.new do |spec|
spec.name = "inspec-parallel"
spec.summary = "Plugin to handle parallel InSpec scan operations over multiple targets"
spec.description = ""
spec.license = "Apache-2.0"
end

View file

@ -0,0 +1,18 @@
module InspecPlugins
module Parallelism
class Plugin < ::Inspec.plugin(2)
plugin_name :"inspec-parallel"
cli_command :parallel do
require_relative "inspec-parallel/cli"
InspecPlugins::Parallelism::CLI
end
streaming_reporter :"child-status" do
require_relative "inspec-parallel/child_status_reporter"
InspecPlugins::Parallelism::StreamingReporter
end
end
end
end

View file

@ -0,0 +1,61 @@
module InspecPlugins::Parallelism
class StreamingReporter < Inspec.plugin(2, :streaming_reporter)
# Registering these methods with RSpec::Core::Formatters class is mandatory
RSpec::Core::Formatters.register self, :example_passed, :example_failed, :example_pending, :close
def initialize(output)
@status_mapping = {}
@control_counter = 0
initialize_streaming_reporter
end
def example_passed(notification)
set_example(notification, "passed")
end
def example_failed(notification)
set_example(notification, "failed")
end
def example_pending(notification)
set_example(notification, "skipped")
end
def close(notification)
# HACK: if we've reached the end of the execution, send a special marker, to ease EOF detection on Windows
puts "EOF_MARKER"
end
private
def set_example(notification, status)
control_id = notification.example.metadata[:id]
title = notification.example.metadata[:title]
set_status_mapping(control_id, status)
output_status(control_id, title) if control_ended?(notification, control_id)
end
def output_status(control_id, title)
@control_counter += 1
stat = @status_mapping[control_id]
stat = if stat.include?("failed")
"F"
else
if stat.include?("skipped")
"S"
else
stat.include?("passed") ? "P" : "E"
end
end
display_name = title.gsub(/\n*\s+/, " ").to_s.force_encoding(Encoding::UTF_8) if title
display_name = control_id.to_s.lstrip.force_encoding(Encoding::UTF_8) unless title
puts "#{@control_counter}/#{stat}/#{controls_count}/#{display_name}"
end
def set_status_mapping(control_id, status)
@status_mapping[control_id] ||= []
@status_mapping[control_id].push(status)
end
end
end

View file

@ -0,0 +1,39 @@
require_relative "command"
require "inspec/dist"
require "inspec/base_cli"
require "inspec/feature"
module InspecPlugins::Parallelism
class CLI < Inspec.plugin(2, :cli_command)
include Inspec::Dist
subcommand_desc "parallel SUBCOMMAND [options]", "Runs #{PRODUCT_NAME} operations parallely"
desc "exec", "Executes profile parallely"
option :option_file, aliases: :o, type: :string, required: true,
desc: "File that contains list of option strings"
option :dry_run, type: :boolean,
desc: "Print commands that will run"
option :verbose, type: :boolean,
desc: "Prints all thor options on dry run"
option :jobs, aliases: :j, type: :numeric,
desc: "Number of jobs to run parallely"
option :ui, type: :string, default: "status",
desc: "Which UI to use: status, text, silent"
option :bg, type: :boolean,
desc: "Runs parallel processes in background"
option :log_path, type: :string,
desc: "Path to the runner and error logs"
exec_options
def exec(default_profile = nil)
Inspec.with_feature("inspec-cli-parallel-exec") {
parallel_cmd = InspecPlugins::Parallelism::Command.new(options, default_profile)
if options[:dry_run]
parallel_cmd.dry_run
else
parallel_cmd.run
end
}
end
end
end

View file

@ -0,0 +1,219 @@
require_relative "runner"
require_relative "validator"
require "erb" unless defined?(Erb)
module InspecPlugins
module Parallelism
class OptionFileNotReadable < RuntimeError
end
class Command
attr_accessor :cli_options_to_parallel_cmd, :default_profile, :sub_cmd, :invocations, :run_in_background
def initialize(cli_options_to_parallel_cmd, default_profile, sub_cmd = "exec")
@default_profile = default_profile
@cli_options_to_parallel_cmd = cli_options_to_parallel_cmd
@sub_cmd = sub_cmd
@logger = Inspec::Log
@invocations = read_options_file
@run_in_background = cli_options_to_parallel_cmd["bg"]
end
def run
validate_thor_options
validate_invocations!
runner = Runner.new(invocations, cli_options_to_parallel_cmd, sub_cmd)
catch_ctl_c_and_exit(runner) unless run_in_background
runner.run
end
def dry_run
validate_invocations!
dry_run_commands
end
private
def catch_ctl_c_and_exit(runner)
puts "Press CTL+C to stop\n"
trap("SIGINT") do
puts "\n"
puts "Shutting down jobs..."
if Inspec.locally_windows?
runner.kill_child_processes
sleep 1
puts "Renaming error log files..."
runner.rename_error_log_files
end
exit Inspec::UI::EXIT_TERMINATED_BY_CTL_C
end
end
def validate_thor_options
# only log path validation needed for now
validate_log_path
end
def validate_log_path
error, message = Validator.new(invocations, cli_options_to_parallel_cmd, sub_cmd).validate_log_path
if error
@logger.error message
Inspec::UI.new.exit(:usage_error)
end
end
def validate_invocations!
# Validation logic stays in Validator class...
Validator.new(invocations, cli_options_to_parallel_cmd, sub_cmd).validate
# UI logic stays in Command class.
valid = true
invocations.each do |invocation_data|
invocation_data[:validation_errors].each do |error_message|
valid = false
@logger.error "Line #{invocation_data[:line_no]}: " + error_message
end
end
unless valid
@logger.error "Please fix the options to proceed further."
Inspec::UI.new.exit(:usage_error)
end
end
def dry_run_commands
invocations.each do |invocation_data|
puts "inspec #{sub_cmd} #{invocation_data[:value]}"
end
end
## Utility functions
def read_options_file
opts = []
begin
content = content_from_file(cli_options_to_parallel_cmd[:option_file])
rescue OptionFileNotReadable => e
@logger.error "Cannot read options file: #{e.message}"
Inspec::UI.new.exit(:usage_error)
end
content.each.with_index(1) do |str, index|
data_hash = { line_no: index }
str = ERB.new(str).result_with_hash(pid: "CHILD_PID").strip
str_has_comment = str.start_with?("#")
next if str.empty? || str_has_comment
default_options = fetch_default_options(str.split(" ")).lstrip
if str.start_with?("-")
data_hash[:value] = "#{default_profile} #{str} #{default_options}"
else
data_hash[:value] = "#{str} #{default_options}"
end
opts << data_hash
end
opts
end
def content_from_file(option_file)
if File.exist?(option_file)
unless [".sh", ".csh", ".ps1"].include? File.extname(option_file)
File.readlines(option_file)
else
if Inspec.locally_windows? && (File.extname(option_file) == ".ps1")
begin
output = `powershell -File "#{option_file}"`
output.split("\n")
rescue StandardError => e
raise OptionFileNotReadable.new("Error reading powershell file #{option_file}: #{e.message}")
end
elsif [".sh", ".csh"].include? File.extname(option_file)
begin
output = `bash "#{option_file}"`
output.split("\n")
rescue StandardError => e
raise OptionFileNotReadable.new("Error reading shell file #{option_file}: #{e.message}")
end
else
raise OptionFileNotReadable.new("Powershell not supported in your system.")
end
end
else
raise OptionFileNotReadable.new("Option file not found.")
end
end
# this must return empty string or default option string which are not part of option file
def fetch_default_options(option_line)
option_line = option_line.select { |word| word.start_with?("-") }
# remove prefixes from the options to compare with default options
option_line.map! do |option_key|
option_key.gsub(options_prefix(option_key), "").gsub("-", "_")
end
default_opts = ""
# iterate through the parallel cli default options and append the option and value which are not present in option file
parallel_cmd_default_cli_options.each do |cmd|
if cmd.is_a? String
append_default_value(default_opts, cmd) unless option_line.include?(cmd)
elsif cmd.is_a? Array
if !option_line.include?(cmd[0]) && !option_line.include?(cmd[1])
append_default_value(default_opts, cmd[0])
end
end
end
default_opts
end
# returns array of default options of the subcommand
def parallel_cmd_default_cli_options
sub_cmd_opts = Inspec::InspecCLI.commands[sub_cmd].options
parallel_cmd_default_opts = cli_options_to_parallel_cmd.keys & sub_cmd_opts.keys.map(&:to_s)
options_to_append = parallel_cmd_default_opts
if cli_options_to_parallel_cmd["dry_run"] && !cli_options_to_parallel_cmd["verbose"]
# to not show thor default options of inspec commands in dry run
sub_cmd_opts_with_defaults = fetch_sub_cmd_default_options(sub_cmd_opts)
options_to_append -= sub_cmd_opts_with_defaults
end
default_opts_to_append = []
# append the options and its aliases if available.
options_to_append.each do |option_name|
opt_alias = sub_cmd_opts[option_name.to_sym].aliases
if opt_alias.empty?
default_opts_to_append << option_name
else
default_opts_to_append << [option_name, opt_alias[0].to_s]
end
end
default_opts_to_append
end
def append_default_value(default_opts, command_name)
default_value = cli_options_to_parallel_cmd[command_name.to_sym]
default_value = default_value.join(" ") if default_value.is_a? Array
default_opts << " --#{command_name.gsub("_", "-")} #{default_value}"
end
def options_prefix(option_name)
if option_name.start_with?("--")
option_name.start_with?("--no-") ? "--no-" : "--"
else
"-"
end
end
def fetch_sub_cmd_default_options(sub_cmd_opts)
default_options_to_remove = []
sub_cmd_opts_with_defaults = sub_cmd_opts.select { |_, c| !c.default.nil? }.keys.map(&:to_s)
sub_cmd_opts_with_defaults.each do |default_opt_name|
if sub_cmd_opts[default_opt_name.to_sym].default == cli_options_to_parallel_cmd[default_opt_name]
default_options_to_remove << default_opt_name
end
end
default_options_to_remove
end
end
end
end

View file

@ -0,0 +1,265 @@
require "inspec/cli"
require "concurrent"
require_relative "super_reporter/base"
module InspecPlugins
module Parallelism
class Runner
attr_accessor :invocations, :sub_cmd, :total_jobs, :run_in_background, :log_path
def initialize(invocations, cli_options, sub_cmd = "exec")
@invocations = invocations
@sub_cmd = sub_cmd
@total_jobs = cli_options["jobs"] || Concurrent.physical_processor_count
@child_tracker = {}
@child_tracker_persisted = {}
@run_in_background = cli_options["bg"]
unless run_in_background
@ui = InspecPlugins::Parallelism::SuperReporter.make(cli_options["ui"], total_jobs, invocations)
end
@log_path = cli_options["log_path"]
end
def run
initiate_background_run if run_in_background # running a process as daemon changes parent process pid
until invocations.empty? && @child_tracker.empty?
while should_start_more_jobs?
if Inspec.locally_windows?
spawn_another_process
else
fork_another_process
end
end
update_ui_poll_select
cleanup_child_processes
sleep 0.1
end
# Requires renaming operations on windows only
# Do Rename and delete operations after all child processes have exited successfully
rename_error_log_files if Inspec.locally_windows?
cleanup_empty_error_log_files
cleanup_daemon_process if run_in_background
end
def initiate_background_run
if Inspec.locally_windows?
Inspec::UI.new.exit(:usage_error)
else
Process.daemon(true, true)
end
end
def cleanup_daemon_process
current_process_id = Process.pid
Process.kill(9, current_process_id)
# DO NOT TRY TO REFACTOR IT THIS WAY
# Calling Process.kill(9,Process.pid) kills the "stopper" process itself, rather than the one it's trying to stop.
end
def cleanup_empty_error_log_files
logs_dir_path = log_path || Dir.pwd
error_files = Dir.glob("#{logs_dir_path}/logs/*.err")
error_files.each do |error_file|
if File.exist?(error_file) && !File.size?(error_file)
File.delete(error_file)
end
end
end
def kill_child_processes
@child_tracker.each do |pid, info|
Process.kill("SIGKILL", pid)
rescue Exception => e
$stderr.puts "Error while shutting down process #{pid}: #{e.message}"
end
# Waiting for child processes to die after they have been killed
wait_for_child_processes_to_die
end
def wait_for_child_processes_to_die
until @child_tracker.empty?
begin
exited_pid = Process.waitpid(-1, Process::WNOHANG)
@child_tracker.delete exited_pid if exited_pid && exited_pid > 0
sleep 1
rescue Errno::ECHILD
Inspec::Log.info "Processes shutdown complete!"
rescue Exception => e
Inspec::Log.debug "Error while waiting for child processes to shutdown: #{e.message}"
end
end
end
def rename_error_log_files
@child_tracker_persisted.each do |pid, info|
rename_error_log(info[:error_log_file], pid)
end
end
def should_start_more_jobs?
@child_tracker.length < total_jobs && !invocations.empty?
end
def spawn_another_process
invocation = invocations.shift[:value]
child_reader, parent_writer = IO.pipe
begin
logs_dir_path = log_path || Dir.pwd
log_dir = File.join(logs_dir_path, "logs")
FileUtils.mkdir_p(log_dir)
error_log_file = File.open("#{log_dir}/#{Time.now.nsec}.err", "a+")
cmd = "#{$0} #{sub_cmd} #{invocation}"
log_msg = "#{Time.now.iso8601} Start Time: #{Time.now}\n#{Time.now.iso8601} Arguments: #{invocation}\n"
child_pid = Process.spawn(cmd, out: parent_writer, err: error_log_file.path)
# Logging
create_logs(child_pid, log_msg)
@child_tracker[child_pid] = { io: child_reader }
# This is used to rename error log files after all child processes are exited
@child_tracker_persisted[child_pid] = { error_log_file: error_log_file }
@ui.child_spawned(child_pid, invocation)
# Close the file to unlock the error log files opened by processes
error_log_file.close
rescue StandardError => e
$stderr.puts "#{Time.now.iso8601} Error Message: #{e.message}"
$stderr.puts "#{Time.now.iso8601} Error Backtrace: #{e.backtrace}"
end
end
def fork_another_process
invocation = invocations.shift[:value] # Be sure to do this shift() in parent process
# thing_that_reads_from_the_child, thing_that_writes_to_the_parent = IO.pipe
child_reader, parent_writer = IO.pipe
if (child_pid = Process.fork)
# In parent with newly forked child
parent_writer.close
@child_tracker[child_pid] = { io: child_reader }
@ui.child_forked(child_pid, invocation) unless run_in_background
else
# In child
child_reader.close
# replace stdout with writer
$stdout = parent_writer
create_logs(Process.pid, nil, $stderr)
begin
create_logs(
Process.pid,
"#{Time.now.iso8601} Start Time: #{Time.now}\n#{Time.now.iso8601} Arguments: #{invocation}\n"
)
runner_invocation(invocation)
rescue StandardError => e
$stderr.puts "#{Time.now.iso8601} Error Message: #{e.message}"
$stderr.puts "#{Time.now.iso8601} Error Backtrace: #{e.backtrace}"
end
# should be unreachable but child MUST exit
exit(42)
end
end
# Still in parent
# Loop over children and check for finished processes
def cleanup_child_processes
@child_tracker.each do |pid, info|
if Process.wait(pid, Process::WNOHANG)
# Expect to (probably) find EOF marker on the pipe, and close it if so
update_ui_poll_select(pid)
create_logs(pid, "#{Time.now.iso8601} Exit code: #{$?}\n")
# child exited - status in $?
@ui.child_exited(pid) unless run_in_background
@child_tracker.delete pid
end
end
end
def update_ui_poll_select(target_pid = nil)
# Focus on one pid's pipe if specified, otherwise poll all pipes
pipes_for_reading = target_pid ? [ @child_tracker[target_pid][:io] ] : @child_tracker.values.map { |i| i[:io] }
# Next line is due to a race between the close() and the wait()... shouldn't need it, but it fixes the race.
pipes_for_reading.reject!(&:closed?)
ready_pipes = IO.select(pipes_for_reading, [], [], 0.1)
return unless ready_pipes
ready_pipes[0].each do |pipe_ready_for_reading|
# If we weren't provided a PID, hackishly look up the pid from the matching IO.
pid = target_pid || @child_tracker.keys.detect { |p| @child_tracker[p][:io] == pipe_ready_for_reading }
begin
while (update_line = pipe_ready_for_reading.readline) && !pipe_ready_for_reading.closed?
if update_line =~ /EOF_MARKER/
pipe_ready_for_reading.close
break
elsif update_line =~ /WARN/ || update_line =~ /ERROR/ || update_line =~ /INFO/
create_logs(
pid,
"#{Time.now.iso8601} Extra log: #{update_line}\n"
)
break
end
update_ui_with_line(pid, update_line) unless run_in_background
# Only pull one line if we are doing normal updates; slurp the whole file
# if we are doing a final pull on a targeted PID
break unless target_pid
end
rescue EOFError
# On unix, readline throws an EOFError when we hit the end. On Windows, nothing apparently happens.
pipe_ready_for_reading.close
next
end
end
# TODO: loop over ready_pipes[2] and handle errors?
end
def update_ui_with_line(pid, update_line)
@ui.child_status_update_line(pid, update_line)
end
private
def runner_invocation(runner_option)
splitted_result = runner_option.split(" ")
profile_to_run = splitted_result[0]
splitted_result.delete_at(0)
# thor invocation
arguments = [sub_cmd, profile_to_run, splitted_result].flatten
Inspec::InspecCLI.start(arguments, enforce_license: true)
end
def create_logs(child_pid, run_log , stderr = nil)
logs_dir_path = log_path || Dir.pwd
log_dir = File.join(logs_dir_path, "logs")
FileUtils.mkdir_p(log_dir)
if stderr
log_file = File.join(log_dir, "#{child_pid}.err") unless File.exist?("#{child_pid}.err")
stderr.reopen(log_file, "a")
else
log_file = File.join(log_dir, "#{child_pid}.log") unless File.exist?("#{child_pid}.log")
File.write(log_file, run_log, mode: "a")
end
end
def rename_error_log(error_log_file, child_pid)
logs_dir_path = log_path || Dir.pwd
log_dir = File.join(logs_dir_path, "logs")
FileUtils.mkdir_p(log_dir)
if error_log_file.closed? && File.exist?(error_log_file.path)
begin
File.rename("#{error_log_file.path}", "#{log_dir}/#{child_pid}.err")
rescue
$stderr.puts "Cannot rename error log file #{error_log_file.path} for child pid #{child_pid}"
end
end
end
end
end
end

View file

@ -0,0 +1,24 @@
module InspecPlugins::Parallelism
class SuperReporter
def self.make(type, job_count, invocations)
Object.const_get("InspecPlugins::Parallelism::SuperReporter::" + (type[0].upcase + type[1..-1])).new(job_count, invocations)
end
class Base
def initialize(job_count, invocations); end
def child_spawned(pid, invocation); end
def child_forked(pid, invocation); end
def child_exited(pid); end
def child_status_update_line(pid, update_line); end
end
require_relative "text"
require_relative "status"
require_relative "silent"
end
end

View file

@ -0,0 +1,7 @@
module InspecPlugins::Parallelism
class SuperReporter
class Silent < InspecPlugins::Parallelism::SuperReporter::Base
# This is a silent super reporter with no reporting functionality.
end
end
end

View file

@ -0,0 +1,124 @@
require "highline"
module InspecPlugins::Parallelism
class SuperReporter
class Status < InspecPlugins::Parallelism::SuperReporter::Base
attr_reader :status_by_pid, :slots
def initialize(job_count, invocations)
@status_by_pid = {}
@slots = Array.new(job_count)
paint_header(job_count, invocations)
paint
end
# --------
# SuperReporter API
# --------
def child_spawned(pid, invocation)
new_child("spawned", pid, invocation)
end
def child_forked(pid, invocation)
new_child("forked", pid, invocation)
end
def child_exited(pid)
slots[status_by_pid[pid][:slot]] = "exited"
status_by_pid[pid][:pct] = 100.0
status_by_pid[pid][:slot] = nil
status_by_pid[pid][:exit] = $?
# TODO: consider holding slot in 100 status for UI grace
paint
end
def child_status_update_line(pid, update_line)
control_serial, status, control_count, title = update_line.split("/")
percent = 100.0 * control_serial.to_i / control_count.to_i.to_f
status_by_pid[pid][:pct] = percent
status_by_pid[pid][:last_control] = title
status_by_pid[pid][:last_status] = status
paint
end
# --------
# Utilities
# --------
private
def new_child(how, pid, invocation)
# Update status by PID with new info
status_by_pid[pid] = {
pct: 0.0,
inv: invocation,
how: how,
}
# Assign first empty slot
slots.each_index do |idx|
next unless slots[idx].nil? || slots[idx] == "exited"
slots[idx] = pid
status_by_pid[pid][:slot] = idx
break
end
# TODO: consider printing log message
paint
end
def terminal_width
return @terminal_width if @terminal_width
@highline ||= HighLine.new
width = @highline.output_cols.to_i
width = 80 if width < 1
@terminal_width = width
end
def paint
# Determine the width of a slot
slot_width = terminal_width / slots.length
line = ""
# Loop over slots
slots.each_index do |idx|
if slots[idx].nil?
# line += "idle".center(slot_width)
# Need to improve UI
elsif slots[idx] == "exited"
line += "Done".center(slot_width)
else
pid = slots[idx]
with_pid = format("%s: %0.1f%%", pid, status_by_pid[pid][:pct])
if with_pid.length <= slot_width - 2
line += with_pid.center(slot_width)
else
line += format("%0.1f%%", status_by_pid[pid][:pct]).center(slot_width)
end
end
end
print "\r" + (" " * terminal_width) + "\r"
print line
end
def paint_header(jobs, invocations)
puts "InSpec Parallel".center(terminal_width)
puts "Running #{invocations.length} invocations in #{jobs} slots".center(terminal_width)
puts "-" * terminal_width
slot_width = terminal_width / slots.length
slots.each_index do |idx|
print "Slot #{idx + 1}".center(slot_width)
end
puts
puts "-" * terminal_width
end
end
end
end

View file

@ -0,0 +1,23 @@
module InspecPlugins::Parallelism
class SuperReporter
class Text < InspecPlugins::Parallelism::SuperReporter::Base
def child_spawned(pid, _inv)
puts "[#{Time.now.iso8601}] Spawned child PID #{pid}"
end
def child_forked(pid, _inv)
puts "[#{Time.now.iso8601}] Forked child PID #{pid}"
end
def child_exited(pid)
puts "[#{Time.now.iso8601}] Exited child PID #{pid} status #{$?}"
end
def child_status_update_line(pid, update_line)
control_serial, _status, control_count, _title = update_line.split("/")
percent = 100.0 * control_serial.to_i / control_count.to_i.to_f
puts "[#{Time.now.iso8601}] #{pid} " + format("%.1f%%", percent)
end
end
end
end

View file

@ -0,0 +1,170 @@
require "inspec/cli"
module InspecPlugins
module Parallelism
class Validator
# TODO: make this list dynamic so plugins can self-declare
PARALLEL_SAFE_REPORTERS = [
"automate", # Performs HTTP transactions, silent on STDOUT
"child-status", # Writes dedicated protocol to STDOUT, expected by parent
].freeze
attr_accessor :invocations, :sub_cmd, :thor_options_for_sub_cmd, :aliases_mapping, :cli_options, :config_content, :stdin_config
def initialize(invocations, cli_options, sub_cmd = "exec")
@invocations = invocations
@sub_cmd = sub_cmd
@thor_options_for_sub_cmd = Inspec::InspecCLI.commands[sub_cmd].options
@aliases_mapping = create_aliases_mapping
@cli_options = cli_options
@config_content = nil
@stdin_config = nil
end
def validate
invocations.each do |invocation_data|
invocation_data[:validation_errors] = []
convert_cli_to_thor_options(invocation_data)
check_for_spurious_options(invocation_data)
check_for_required_fields(invocation_data)
check_for_reporter_options(invocation_data)
end
end
def validate_log_path
return [] unless cli_options["log_path"]
if File.directory?(cli_options["log_path"])
[]
else
[true, "Log path #{cli_options["log_path"]} is not accessible"]
end
end
private
def create_aliases_mapping
alias_mapping = {}
thor_options_for_sub_cmd.each do |_, sub_cmd_option|
aliases = sub_cmd_option.aliases
unless aliases.empty?
alias_mapping[aliases[0]] = sub_cmd_option.name
end
end
alias_mapping
end
def check_for_spurious_options(invocation_data)
# LIMITATION: Assume the first arg is the profile name, and there is exactly one of them.
invalid_options = invocation_data[:thor_args][1..-1]
invocation_data[:validation_errors].push "No such option: #{invalid_options}" unless invalid_options.empty?
end
def check_for_required_fields(invocation_data)
required_fields = thor_options_for_sub_cmd.collect { |_, thor_option| thor_option.name if thor_option.required }.compact
option_keys = invocation_data[:thor_opts].keys
invocation_data[:thor_opts].keys.map { |key| option_keys.push(aliases_mapping[key.to_sym]) if aliases_mapping[key.to_sym] }
if !required_fields.empty? && (option_keys & required_fields).empty?
invocation_data[:validation_errors].push "No value provided for required options: #{required_fields}"
end
end
def check_for_reporter_options(invocation_data)
# if no reporter option, that's an error
unless invocation_data[:thor_opts].include?("reporter")
# Check for config reporter validation only if --reporter option is missing from options file
return if check_reporter_options_in_config(invocation_data)
invocation_data[:validation_errors] << "A --reporter option must be specified for each invocation in the options file"
return
end
have_child_status_reporter = false
# Reporter option is formatted as an array
invocation_data[:thor_opts]["reporter"].each do |reporter_spec|
reporter_name, file_output = reporter_spec.split(":")
have_child_status_reporter = true if reporter_name == "child-status"
# if there is a reporter option, each entry must either write to a file or
# else be the special child-status reporter or the automate reporter
next if PARALLEL_SAFE_REPORTERS.include?(reporter_name)
unless file_output
invocation_data[:validation_errors] << "The #{reporter_name} reporter requires being directed to a file, like #{reporter_name}:filename.out"
end
end
# if there is no child-status reporter, add one to the raw value and the parsed array
unless have_child_status_reporter
# Eww
invocation_data[:thor_opts]["reporter"] << "child-status"
invocation_data[:value].gsub!("--reporter ", "--reporter child-status ")
end
end
def check_reporter_options_in_config(invocation_data)
config_opts = invocation_data[:thor_opts]["config"] || invocation_data[:thor_opts]["json_config"]
cfg_io = check_for_piped_config_from_stdin(config_opts)
if cfg_io == STDIN
# Scenario of using config from STDIN
@config_content ||= cfg_io.read
else
if config_opts.nil?
# Scenario of using default config.json file when path not provided
default_path = File.join(Inspec.config_dir, "config.json")
config_opts = default_path
return unless File.exist?(config_opts)
elsif !File.exist?(config_opts)
invocation_data[:validation_errors] << "Could not read configuration file at #{config_opts}"
return
end
@config_content = File.open(config_opts).read
end
reporter_config = JSON.parse(config_content)["reporter"] unless config_content.nil? || config_content.empty?
unless reporter_config
invocation_data[:validation_errors] << "Config should have reporter option specified for each invocation which is not using --reporter option in options file"
end
@config_content
end
def check_for_piped_config_from_stdin(config_opts)
return nil unless config_opts
return nil unless config_opts == "-"
@stdin_config ||= STDIN
end
## Utility functions
# Parse the invocation string using Thor into Thor options
# This approach was reverse engineered from studying
# https://github.com/rails/thor/blob/ab3b5be455791f4efb79f0efb4f88cc6b59c8ccf/lib/thor/base.rb#L53
def convert_cli_to_thor_options(invocation_data)
invocation_words = invocation_data[:value].split(" ")
# LIMITATION: this approach is limited to having exactly one profile in the invocation
args = [invocation_words.shift] # That is, the profile path
# Here we're piggybacking on on a hook used by the start() method, and provides the
# specifics for the subcommand
config = { command_options: thor_options_for_sub_cmd }
# This performs the parse
thor = Inspec::InspecCLI.new(args, invocation_words, config)
# A hash (with indifferent access) of option names to option config data
invocation_data[:thor_opts] = thor.options
# A list of everything else it could not parse, including the profile
invocation_data[:thor_args] = thor.args
end
end
end
end

View file

@ -0,0 +1,28 @@
# This option file is used for testing parallel dry run
# Test 1: test_parallel_dry_run
# Command to execute: bundle exec inspec parallel exec test/fixtures/profiles/complete-profile -o lib/plugins/inspec-parallel/test/fixtures/options-file-1.txt --dry-run
# Comments (line starting with #),
# comments with improper indentation (whitespaces at beginning and end).
# and blank lines will be ignored while parsing this option file.
# Scenario 1: Use profile if specified in the cli
-t ssh://vagrant@127.0.0.1:2201 --reporter cli:myfile.out --no-create-lockfile --no-sudo
# Scenario 2: Use profile specified in the option file
test/fixtures/profiles/control-tags -t ssh://vagrant@127.0.0.1:2201 --reporter cli:myfile.out
test/fixtures/profiles/control-tags --input input1=testvalue1 input2=testvalue2 --command-timeout 10 --reporter cli:myfile.out
# Test 2: test_parallel_dry_run_with_default_opts
# Command to execute: bundle exec inspec parallel exec test/fixtures/profiles/complete-profile -o lib/plugins/inspec-parallel/test/fixtures/options-file-1.txt -t docker://8b5ec1a0344b --reporter json --dry-run
# Scenario 1: Append no default options from the cli
test/fixtures/profiles/basic_profile -t docker://8b5ec1a0344b --reporter json:myfile.json
# Scenario 2: Append default profile from the cli
-t docker://1870886821c3 --reporter cli:myfile.out
# Scenario 3: Append default profile and target from the cli
--reporter cli:myfile.out

View file

@ -0,0 +1,9 @@
# This option file is used for testing below two tests when there is a typo:
# Test 1: test_parallel_dry_run_with_typo_in_option
# Test 2: test_parallel_run_with_typo_in_option
# Comments (line starting with #), whitespaces(beginning and end of line)
# and blank lines will be ignored while parsing this option file.
# Scenario 1: Typo with the option keyword
--targetss "should_raise_error"

View file

@ -0,0 +1,16 @@
# This option file is used for testing with no target so that it doesn't fail on buildkite
# Test 1: test_parallel_run_without_forking
# Test 2: test_parallel_run_without_fork_with_default_opts
# Comments (line starting with #), whitespaces(beginning and end of line)
# and blank lines will be ignored while parsing this option file.
# Scenario 1: Use profile if specified in the cli
--reporter json:myfile.json
# Scenario 2: Use the profile specified in the option file
test/fixtures/profiles/control-tags --reporter cli:myfile.out
# Scenario 3: Using ERB templating syntax in options file
test/fixtures/profiles/control-tags --reporter <%= "cli:myfile.out" %>

View file

@ -0,0 +1,3 @@
# This file tests --reporter option validation. In this case, the file is invalid because
# the invocation contains a json reporter without a file.
-t local:// --reporter json

View file

@ -0,0 +1,5 @@
# This file tests reporter validation by testing an options
# file that does not specify a reporter at all.
-t local:// --reporter cli:myfile.out
-t local:// --reporter cli:myfile.out
-t local://

View file

@ -0,0 +1,2 @@
#!/bin/bash
cat lib/plugins/inspec-parallel/test/fixtures/options-file-3.txt

View file

@ -0,0 +1,107 @@
require_relative "../../../shared/core_plugin_test_helper"
require_relative "../../../../../test/functional/helper"
class ParallelCli < Minitest::Test
include CorePluginFunctionalHelper
let(:options_file_1) { File.join("lib", "plugins", "inspec-parallel", "test", "fixtures", "options-file-1.txt") }
let(:options_file_2) { File.join("lib", "plugins", "inspec-parallel", "test", "fixtures", "options-file-2.txt") }
let(:options_file_3) { File.join("lib", "plugins", "inspec-parallel", "test", "fixtures", "options-file-3.txt") }
let(:options_file_4) { File.join("lib", "plugins", "inspec-parallel", "test", "fixtures", "options-file-4.txt") }
let(:options_file_5) { File.join("lib", "plugins", "inspec-parallel", "test", "fixtures", "options-file-5.txt") }
let(:options_shell_file_1) { File.join("lib", "plugins", "inspec-parallel", "test", "fixtures", "options-file-shell-1.sh") }
def test_help_output
out = run_inspec_process("parallel help")
assert_includes out.stdout, "inspec parallel exec o"
assert_exit_code 0, out
end
def test_parallel_dry_run
out = run_inspec_process("parallel exec #{complete_profile} -o #{options_file_1} --dry-run --sudo")
stdout = out.stdout
assert_includes stdout, "complete-profile -t ssh://vagrant@127.0.0.1:2201 --reporter child-status cli:myfile.out --no-create-lockfile --no-sudo"
assert_includes stdout, "control-tags -t ssh://vagrant@127.0.0.1:2201 --reporter child-status cli:myfile.out --sudo true"
assert_equal stdout.split("\n").count, 6
assert_exit_code 0, out
end
def test_parallel_run_without_forking
skip_windows!
out = run_inspec_process("parallel exec #{complete_profile} -o #{options_file_3}")
assert_empty out.stderr
assert_exit_code 0, out
end
def test_parallel_dry_run_with_typo_in_option
out = run_inspec_process("parallel exec #{complete_profile} -o #{options_file_2} --dry-run")
stdout = out.stdout
assert_includes stdout, "No such option: [\"--targetss\""
assert_exit_code 1, out
end
def test_parallel_run_with_typo_in_option
out = run_inspec_process("parallel exec #{complete_profile} -o #{options_file_2}")
stdout = out.stdout
assert_includes stdout, "No such option: [\"--targetss\""
assert_exit_code 1, out
end
def test_parallel_with_default_opts
skip_windows!
out = run_inspec_process("parallel exec #{complete_profile} -o #{options_file_3} --reporter json")
assert_empty out.stderr
assert_exit_code 0, out
end
def test_parallel_run_with_shell_file_as_options_file
skip_windows!
out = run_inspec_process("parallel exec #{complete_profile} -o #{options_shell_file_1} --reporter json")
assert_empty out.stderr
assert_exit_code 0, out
end
def test_parallel_dry_run_with_shell_file_as_options_file
skip_windows!
out = run_inspec_process("parallel exec #{complete_profile} -o #{options_shell_file_1} --dry-run")
stdout = out.stdout
assert_includes stdout, "complete-profile --reporter child-status json:myfile.json --create-lockfile false"
assert_includes stdout, "control-tags --reporter child-status cli:myfile.out --create-lockfile false"
assert_empty out.stderr
assert_exit_code 0, out
end
def test_parallel_dry_run_with_default_opts
out = run_inspec_process("parallel exec #{complete_profile} -o #{options_file_1} -t docker://8b5ec1a0344b --dry-run")
stdout = out.stdout
assert_includes stdout, "basic_profile -t docker://8b5ec1a0344b --reporter child-status json:myfile.json"
assert_includes stdout, "complete-profile -t docker://1870886821c3 --reporter child-status cli:myfile.out"
assert_includes stdout, "complete-profile --reporter child-status cli:myfile.out --target docker://8b5ec1a0344b"
assert_equal stdout.split("\n").count, 6
assert_exit_code 0, out
end
def test_parallel_dry_run_with_verbose_option
out = run_inspec_process("parallel exec #{complete_profile} -o #{options_file_1} --dry-run --verbose")
stdout = out.stdout
assert_includes stdout, "complete-profile -t ssh://vagrant@127.0.0.1:2201 --reporter child-status cli:myfile.out --no-create-lockfile --no-sudo --winrm-transport negotiate --insecure false --winrm-shell-type powershell --auto-install-gems false --distinct-exit true --diff true --sort-results-by file --filter-empty-profiles false --reporter-include-source false"
assert_includes stdout, "control-tags -t ssh://vagrant@127.0.0.1:2201 --reporter child-status cli:myfile.out --winrm-transport negotiate --insecure false --winrm-shell-type powershell --auto-install-gems false --distinct-exit true --diff true --sort-results-by file --filter-empty-profiles false --reporter-include-source false"
assert_equal stdout.split("\n").count, 6
assert_exit_code 0, out
end
def test_reporter_validation_no_file_output
out = run_inspec_process("parallel exec #{complete_profile} -o #{options_file_4} --dry-run")
stdout = out.stdout
assert_includes stdout, "Line 3: The json reporter requires being directed to a file, like json:filename.out"
assert_exit_code 1, out
end
def test_reporter_validation_no_reporter
out = run_inspec_process("parallel exec #{complete_profile} -o #{options_file_5} --dry-run")
stdout = out.stdout
assert_includes stdout, "Line 5: A --reporter option must be specified for each invocation in the options file"
assert_exit_code 1, out
end
end

View file

@ -1,5 +1,6 @@
require_relative "base"
require "inspec/dist"
require "inspec/feature"
#
# Notes:
@ -85,8 +86,10 @@ module InspecPlugins
option :keydir, type: :string, default: "./",
desc: "Directory to search for keys"
def generate_keys
puts "Generating keys"
InspecPlugins::Sign::Base.keygen(options)
Inspec.with_feature("inspec-cli-sign-generate-keys") {
puts "Generating keys"
InspecPlugins::Sign::Base.keygen(options)
}
end
desc "profile PATH", "sign the profile in PATH and generate .iaf artifact."
@ -95,12 +98,16 @@ module InspecPlugins
option :profile_content_id, type: :string,
desc: "UUID of the profile. This will write the profile_content_id in the metadata file if it does not already exist in the metadata file."
def profile(profile_path)
InspecPlugins::Sign::Base.profile_sign(profile_path, options)
Inspec.with_feature("inspec-cli-sign-profile") {
InspecPlugins::Sign::Base.profile_sign(profile_path, options)
}
end
desc "verify PATH", "Verify a signed profile .iaf artifact at given path."
def verify(signed_profile_path)
InspecPlugins::Sign::Base.profile_verify(signed_profile_path)
Inspec.with_feature("inspec-cli-sign-verify") {
InspecPlugins::Sign::Base.profile_verify(signed_profile_path)
}
end
end
end

View file

@ -91,23 +91,20 @@ module InspecPlugins::StreamingReporterProgressBar
set_status_mapping(control_id, status)
collect_notifications(notification, control_id, status)
control_ended = control_ended?(control_id)
if control_ended
control_outcome = add_enhanced_outcomes(control_id) if enhanced_outcomes
show_progress(control_id, title, full_description, control_outcome)
end
show_progress(control_id, title, full_description) if control_ended?(notification, control_id)
end
def show_progress(control_id, title, full_description, control_outcome)
def show_progress(control_id, title, full_description)
@bar ||= ProgressBar.new(controls_count, :bar, :counter, :percentage)
sleep 0.1
@bar.increment!
@bar.puts format_it(control_id, title, full_description, control_outcome)
@bar.puts format_it(control_id, title, full_description)
rescue StandardError => e
raise "Exception in Progress Bar streaming reporter: #{e}"
end
def format_it(control_id, title, full_description, control_outcome)
def format_it(control_id, title, full_description)
control_outcome = control_outcome(control_id)
if control_outcome
control_status = control_outcome
else
@ -121,11 +118,7 @@ module InspecPlugins::StreamingReporterProgressBar
end
end
indicator = INDICATORS[control_status]
message_to_format = ""
message_to_format += "#{indicator} "
message_to_format += "#{control_id.to_s.strip.dup.force_encoding(Encoding::UTF_8)} "
message_to_format += "#{title.gsub(/\n*\s+/, " ").to_s.force_encoding(Encoding::UTF_8)} " if title
message_to_format += "#{full_description.gsub(/\n*\s+/, " ").to_s.force_encoding(Encoding::UTF_8)} " unless title
message_to_format = format_message(indicator, control_id, title, full_description)
format_with_color(control_status, message_to_format)
rescue Exception => e
raise "Exception in show_progress: #{e}"

View file

@ -1,9 +1,9 @@
GIT
remote: https://github.com/chef/omnibus-software.git
revision: 4b08f0bc0688f750bc55a49b8103b2d12815399e
revision: 3268356b2eaf80715887e452c89b36d8f86974a0
branch: main
specs:
omnibus-software (23.7.293)
omnibus-software (23.7.295)
omnibus (>= 9.0.0)
GIT
@ -29,18 +29,18 @@ GIT
GEM
remote: https://rubygems.org/
specs:
addressable (2.8.4)
addressable (2.8.5)
public_suffix (>= 2.0.2, < 6.0)
artifactory (3.0.15)
awesome_print (1.9.2)
aws-eventstream (1.2.0)
aws-partitions (1.784.0)
aws-sdk-core (3.177.0)
aws-partitions (1.803.0)
aws-sdk-core (3.180.3)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.5)
jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.70.0)
aws-sdk-kms (1.71.0)
aws-sdk-core (~> 3, >= 3.177.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.116.0)
@ -201,7 +201,7 @@ GEM
ffi (~> 1.0)
ffi-win32-extensions (1.0.4)
ffi
ffi-yajl (2.4.0)
ffi-yajl (2.6.0)
libyajl2 (>= 1.2)
fuzzyurl (0.9.0)
gssapi (1.3.1)
@ -302,7 +302,7 @@ GEM
net-ssh (>= 2.6.5, < 8.0.0)
net-sftp (4.0.0)
net-ssh (>= 5.0.0, < 8.0.0)
net-ssh (7.1.0)
net-ssh (7.2.0)
net-ssh-gateway (2.0.0)
net-ssh (>= 4.0.0)
netrc (0.11.0)
@ -338,7 +338,7 @@ GEM
pry (0.14.2)
coderay (~> 1.1)
method_source (~> 1.0)
public_suffix (5.0.1)
public_suffix (5.0.3)
rack (2.2.7)
rainbow (3.1.1)
rest-client (2.1.0)
@ -359,7 +359,7 @@ GEM
mime-types (>= 1.16, < 4.0)
netrc (~> 0.8)
retryable (3.0.5)
rexml (3.2.5)
rexml (3.2.6)
rspec (3.11.0)
rspec-core (~> 3.11.0)
rspec-expectations (~> 3.11.0)

View file

@ -26,7 +26,7 @@
# Disable git caching
# ------------------------------
# use_git_caching false
use_git_caching false
# Enable S3 asset caching
# ------------------------------

6
test/fixtures/features-01.sig vendored Normal file
View file

@ -0,0 +1,6 @@
xOZopkIkaGVZsAN9DXdOAB8wbVrtnDJT0pmB8IFwu2vusFSDkznKe4iS4BdE
51yUoKi/UptLmxL6A+Itz5ASXDoR/zR+7CtfsIAwbFtxrF46lxKmK7tSc+17
b/7cJgE20sSF4FMQTi9SkYPf+e/kyXHL5zqUXBLeJ5Dkufj9nnSrvDUvsaxm
alPlUZGjK31HvZTkJIwtd6PtrLL7MrydFz7tuUFmBmWflbmEpV4DPTbEdNLa
dPGW0Q1tgZAMI4gOWjGKg7BXrF2WQefFHGoFrWRp57Xf307Ty+dji+WL0TSE
yNr7Bh4jYczX7/se4okivdDn5jkfWdncrowCBaZkug==

6
test/fixtures/features-01.yaml vendored Normal file
View file

@ -0,0 +1,6 @@
---
features:
test-feature-01:
description: Fun for the whole security organization
test-feature-02:
description: A great use of your time

6
test/fixtures/features-tampered.sig vendored Normal file
View file

@ -0,0 +1,6 @@
idN5b9GDnQTm3ayYiynvJknrVinIomSBGEE4f9ZFL2IUxyeztvtUBECaSBfM
T9LOVGBT2/kW2dHQl9PHQuXLpDJz0fKGEnQhAZzsRjIBRWvphlIZ3aBXsrCP
OaDxjLdkY+lJMV4eD9XRypm0hC3cNezQMSWGtmQIdm6Ez/rDDuKL2K4wsxXt
TILhF3/4Rl3N/VWJLVOEem6RF7t+48aLrlxbkh8bo76W7RytZWrM4R0XgUPw
XwzNx+XKTwuPjn5TTyMEyH5TGATJfiu6NdxXhNcyg8KvdKOIFbUwF4zk+Vop
BQG9lQ5C8lHOX2VwPUtZG+hCuqoo0Ir9RpjOk2bp8w==

6
test/fixtures/features-tampered.yaml vendored Normal file
View file

@ -0,0 +1,6 @@
---
features:
test-feature-01:
description: Great fun for the whole security organization
test-feature-02:
description: A great use of your time

View file

@ -105,6 +105,7 @@ describe "inputs" do
# require inspec
require "inspec"
require "inspec/runner"
require "inspec/utils/licensing_config"
# inject pretty-printed runner opts
runner_args = #{options.inspect}

View file

@ -1,12 +1,14 @@
require "functional/helper"
require "inspec/runner"
require "inspec/resources/file"
require "inspec/utils/licensing_config"
describe "inspec report tests" do
include FunctionalHelper
describe "report" do
it "loads a json report" do
WebMock.allow_net_connect!
o = { "reporter" => ["json"], "report" => true }
runner = ::Inspec::Runner.new(o)
runner.add_target(example_profile)

134
test/unit/feature_test.rb Normal file
View file

@ -0,0 +1,134 @@
# This file tests Inspec::Feature functionality,
# which allows you to declare a group of functionality
# for purposes of logging, entitlement, feature flagging,
# telemetry, and other future purposes
require "helper"
require "logger"
require "stringio"
require "inspec/feature"
describe "Inspec::Feature" do
let(:fixtures_path) { "test/fixtures" }
it "should be a class" do
_(Inspec::Feature).must_be_kind_of Class
end
#======================
# The global convenience method with_feature
#======================
# It exists
describe "Inspec.with_feature" do
it "should have a with_feature class method" do
_(Inspec.respond_to?(:with_feature)).must_equal true
end
it "should take a symbol, options, and a block" do
_(Inspec.method(:with_feature).arity).must_equal(-2)
end
it "defaults to calling the block" do
called = false
Inspec.with_feature(:test_feature) do
called = true
end
_(called).must_equal true
end
let(:feature_config_file) { File.join(fixtures_path, "features-01.yaml") }
let(:cfg) { Inspec::Feature::Config.new(feature_config_file) }
it "accepts a config as an option" do
called = false
Inspec.with_feature("test-feature-01", config: cfg) do
called = true
end
# TODO: need a better test to verify that the feature was recognized
_(called).must_equal true
end
# Integration with Logger
let(:logger_io) { StringIO.new }
let(:logger) { l = Logger.new(logger_io); l.level = Logger::DEBUG; l; }
it "accepts a logger as an option" do
Inspec.with_feature("test-feature-01", config: cfg, logger: logger) do
end
_(logger_io.string).must_match(/test-feature-01/)
end
# Validation of feature names
it "validates feature names" do
Inspec.with_feature("test-feature-nonesuch", config: cfg, logger: logger) do
end
_(logger_io.string).must_match(/WARN/)
_(logger_io.string).must_match(/test-feature-nonesuch/)
end
end
# TODO: Integration with Entitlement
# TODO: Integration with feature flagging
# TODO: Integration with usage telemetry
#======================
# Internals
#======================
#------------------------
# Inspec::Feature::Config
#------------------------
describe "Inspec::Feature::Config" do
describe "when you load it from a specified file" do
let(:feature_config_file) { File.join(fixtures_path, "features-01.yaml") }
# you should be able to load it from a test file
let(:cfg) { Inspec::Feature::Config.new(feature_config_file) }
it "lists features in a block" do
feats = []
cfg.with_each_feature do |f|
feats << f
end
# you should be able to list features
# as Inspec::Features
_(feats.length).must_equal 2
_(feats[0]).must_be_kind_of Inspec::Feature
end
it "allows calling features by name with brackets" do
_(cfg["test-feature-01"]).must_be_kind_of Inspec::Feature
end
it "allows detecting if a name is a feature" do
_(cfg.feature_name?("test-feature-01")).must_equal true
_(cfg.feature_name?("test-feature-99")).must_equal false
end
it "allows accessing the array of features as a method" do
_(cfg.features).must_be_kind_of Array
_(cfg.features.length).must_equal 2
_(cfg.features[0]).must_be_kind_of Inspec::Feature
end
end
describe "when you load it from the default location" do
let(:cfg) { Inspec::Feature::Config.new }
it "lists features" do
feats = []
cfg.with_each_feature do |f|
feats << f
end
# loading from the default location should result in
# at least two features
# you should be able to list features
# as Inspec::Features
_(feats.length).must_be :>, 2
_(feats[0]).must_be_kind_of Inspec::Feature
end
end
describe "when you load it from a tampered file" do
let(:tampered_config_file) { File.join(fixtures_path, "features-tampered.yaml") }
it "throws an exception and loads no features" do
_ { Inspec::Feature::Config.new(tampered_config_file) }.must_raise(Inspec::FeatureConfigTamperedError)
end
end
end
end

View file

@ -4,6 +4,7 @@ require "helper"
require "inspec/secrets"
require "inspec/runner"
require "inspec/fetcher/mock"
require "inspec/utils/licensing_config"
describe Inspec::Runner do
let(:runner) { Inspec::Runner.new({ command_runner: :generic, reporter: [] }) }
@ -73,6 +74,7 @@ describe Inspec::Runner do
describe "testing runner.run exit codes" do
it "returns proper exit code when no profile is added" do
WebMock.allow_net_connect!
_(runner.run).must_equal 0
end
end

View file

@ -0,0 +1,18 @@
require "helper"
require "inspec/utils/licensing_config"
describe "ChefLicensing::Config" do
it "returns the default chef product name as foo" do
expect(ChefLicensing::Config.chef_product_name).must_equal("InSpec")
end
it "returns the default chef_entitlement_id" do
expect(ChefLicensing::Config.chef_entitlement_id).must_equal("3ff52c37-e41f-4f6c-ad4d-365192205968")
end
it "returns the default chef_executable_name" do
expect(ChefLicensing::Config.chef_executable_name).must_equal("inspec")
end
# TODO: Need to add the test for license_server_url.
end