├── .gitattributes ├── .github ├── release-drafter.yml └── workflows │ ├── build.yml │ ├── prep-release.yml │ └── release.yml ├── .gitignore ├── .rubocop.yml ├── .rubocop_todo.yml ├── Dockerfile ├── Gemfile ├── LICENSE.md ├── README.md ├── Rakefile ├── _config.yml ├── examples ├── CIS_Ubuntu_Linux_16.04_LTS_Benchmark_v1.0.0.pdf ├── attribute.json ├── csv2inspec │ ├── mapping.yml │ └── stig.csv ├── inspec2ckl │ └── metadata.json ├── inspec2xccdf │ ├── README.md │ ├── bad_test.json │ ├── metadata.json │ └── xccdf_compliant_attribute.json ├── sample_json │ ├── good_nginxresults.json │ ├── inspec-profile-disa_stig-el7.json │ ├── inspec-v4.28.0.json │ ├── rhel-simp.json │ ├── single_control_profile.json │ └── single_control_results.json ├── sample_yaml │ └── threshold.yaml ├── xccdf2inspec │ ├── cci.xml │ ├── data │ │ ├── README.md │ │ ├── U_CAN_Ubuntu_18-04_STIG-xccdf.xml │ │ ├── U_CCI_List.xml │ │ ├── U_JBOSS_EAP_6-3_STIG_V1R2_Manual-xccdf.xml │ │ ├── U_MS_Windows_2012_and_2012_R2_DC_STIG_V2R19_Manual-xccdf.xml │ │ ├── U_PostgreSQL_9-x_STIG_V1R1_Manual-xccdf.xml │ │ └── U_RHEL_7_STIG_V3R3_Manual-xccdf.xml │ ├── metadata.json │ └── xccdf.xml └── xlsx2inspec │ └── mapping.cis.yml ├── exe └── inspec_tools ├── inspec_tools.gemspec ├── lib ├── data │ ├── NIST_Map_02052020_CIS_Controls_Version_7.1_Implementation_Groups_1.2.xlsx │ ├── NIST_Map_09212017B_CSC-CIS_Critical_Security_Controls_VER_6.1_Excel_9.1.2016.xlsx │ ├── README.TXT │ ├── U_CCI_List.xml │ ├── attributes.yml │ ├── cci2html.xsl │ ├── mapping.yml │ ├── rubocop.yml │ ├── stig.csv │ └── threshold.yaml ├── exceptions │ ├── impact_input_error.rb │ └── severity_input_error.rb ├── happy_mapper_tools │ ├── benchmark.rb │ ├── cci_attributes.rb │ ├── stig_attributes.rb │ └── stig_checklist.rb ├── inspec_tools.rb ├── inspec_tools │ ├── cli.rb │ ├── csv.rb │ ├── generate_map.rb │ ├── help.rb │ ├── help │ │ ├── compliance.md │ │ ├── csv2inspec.md │ │ ├── inspec2ckl.md │ │ ├── inspec2csv.md │ │ ├── inspec2xccdf.md │ │ ├── pdf2inspec.md │ │ ├── summary.md │ │ └── xccdf2inspec.md │ ├── inspec.rb │ ├── pdf.rb │ ├── plugin.rb │ ├── plugin_cli.rb │ ├── summary.rb │ ├── version.rb │ ├── xccdf.rb │ └── xlsx_tool.rb ├── inspec_tools_plugin.rb ├── overrides │ ├── false_class.rb │ ├── nil_class.rb │ ├── object.rb │ ├── string.rb │ └── true_class.rb └── utilities │ ├── cci_xml.rb │ ├── cis_to_nist.rb │ ├── csv_util.rb │ ├── extract_pdf_text.rb │ ├── inspec_util.rb │ ├── mapping_validator.rb │ ├── parser.rb │ ├── text_cleaner.rb │ └── xccdf │ └── xccdf_score.rb └── test ├── data └── inspec_1.json ├── schemas ├── U_Checklist_Schema_V2-3.xsd └── xccdf_114 │ ├── cpe-1.0.xsd │ ├── cpe-language_2.0.xsd │ ├── platform-0.2.3.xsd │ ├── simpledc20021212.xsd │ ├── xccdf-1.1.4.xsd │ ├── xccdfp-1.1.xsd │ └── xml.xsd └── unit ├── inspec_tools ├── csv_test.rb ├── happymapper_test.rb ├── inspec_test.rb ├── metadata_test.rb ├── pdf_test.rb ├── summary_test.rb ├── xccdf_test.rb └── xlsx_tool_test.rb ├── inspec_tools_test.rb ├── test_helper.rb └── utils ├── inspec_util_test.rb └── xccdf └── xccdf_score_test.rb /.gitattributes: -------------------------------------------------------------------------------- 1 | *.xls filter=lfs diff=lfs merge=lfs -text 2 | *.xlsx filter=lfs diff=lfs merge=lfs -text 3 | *.pdf filter=lfs diff=lfs merge=lfs -text 4 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name-template: 'v$NEXT_PATCH_VERSION' 2 | tag-template: 'v$NEXT_PATCH_VERSION' 3 | categories: 4 | - title: 'Features' 5 | labels: 6 | - 'feature' 7 | - 'enhancement' 8 | - title: 'Bug Fixes' 9 | labels: 10 | - 'fix' 11 | - 'bugfix' 12 | - 'bug' 13 | change-template: '- $TITLE @$AUTHOR (#$NUMBER)' 14 | template: | 15 | ## Changes 16 | $CHANGES 17 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Build and test inspec_tools 3 | 4 | on: 5 | push: 6 | branches: [ master ] 7 | pull_request: 8 | branches: [ master ] 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | with: 16 | lfs: true 17 | fetch-depth: 0 18 | - name: Fetch all tags for versioning 19 | run: git fetch origin +refs/tags/*:refs/tags/* 20 | - name: Setup Ruby 2.7 21 | uses: ruby/setup-ruby@v1 22 | with: 23 | ruby-version: 2.7 24 | - name: Install bundler and git-lite-version-bump 25 | run: gem install bundler git-lite-version-bump 26 | - name: Run bundle install 27 | run: bundle install 28 | - name: Generate mapping objects 29 | run: bundle exec rake generate_mapping_objects 30 | - name: Run rubocop 31 | run: bundle exec rubocop 32 | - name: Run bundle-audit 33 | run: bundle-audit check --update 34 | - name: Build inspec_tools gem 35 | run: gem build inspec_tools.gemspec 36 | - name: Upload inspec_tools gem 37 | uses: actions/upload-artifact@v2 38 | with: 39 | name: inspec_tools_gem 40 | path: inspec_tools-*.gem 41 | 42 | test-gem: 43 | needs: build 44 | strategy: 45 | fail-fast: false 46 | matrix: 47 | platform: [ ubuntu-latest, macos-latest, windows-latest ] 48 | ruby: [ 2.7 ] 49 | runs-on: ${{ matrix.platform }} 50 | steps: 51 | - name: Setup Ruby ${{matrix.ruby}} 52 | uses: ruby/setup-ruby@v1 53 | with: 54 | ruby-version: ${{matrix.ruby}} 55 | - uses: actions/checkout@v2 56 | with: 57 | lfs: true 58 | fetch-depth: 0 59 | - name: Download inspec_tools gem 60 | uses: actions/download-artifact@v2 61 | with: 62 | name: inspec_tools_gem 63 | - name: Install inspec_tools gem 64 | run: gem install inspec_tools-*.gem 65 | - name: Run installed inspec_tools 66 | run: | 67 | inspec_tools --version 68 | inspec_tools --help 69 | inspec_tools xlsx2inspec -m examples/xlsx2inspec/mapping.cis.yml -x examples/cis.xlsx -p xlsx2inspec_test -o xlsx2inspec_test 70 | inspec_tools summary -j examples/sample_json/rhel-simp.json 71 | inspec_tools csv2inspec -c examples/csv2inspec/stig.csv -m examples/csv2inspec/mapping.yml -o csv2inspec_test 72 | inspec_tools xccdf2inspec -x examples/xccdf2inspec/xccdf.xml -a lib/data/attributes.yml -o xccdf2inspec_test 73 | inspec_tools xccdf2inspec -x examples/xccdf2inspec/data/U_JBOSS_EAP_6-3_STIG_V1R2_Manual-xccdf.xml -o xccdf2inspec_replace_test -r JBOSS_HOME 74 | inspec_tools pdf2inspec -p examples/CIS_Ubuntu_Linux_16.04_LTS_Benchmark_v1.0.0.pdf -o pdf2inspec_test 75 | inspec_tools inspec2csv -j examples/sample_json/rhel-simp.json -o inspec2csv_test.csv 76 | inspec_tools inspec2ckl -j examples/sample_json/rhel-simp.json -o inspec2ckl_test_1.ckl 77 | inspec_tools inspec2ckl -j test/data/inspec_1.json -o test/data/inspec2ckl_test_2.ckl 78 | inspec_tools inspec2xccdf -j examples/sample_json/rhel-simp.json -a lib/data/attributes.yml -o inspec2xccdf_test.xml 79 | inspec_tools inspec2xccdf -j examples/sample_json/rhel-simp.json -a examples/inspec2xccdf/xccdf_compliant_attribute.json -m examples/inspec2xccdf/metadata.json -o inspec2xccdf_11_1.xml 80 | inspec_tools inspec2xccdf -j examples/sample_json/inspec-v4.28.0.json -a examples/inspec2xccdf/xccdf_compliant_attribute.json -o inspec2xccdf_11_2.xml 81 | inspec_tools compliance -j examples/sample_json/single_control_results.json -f examples/sample_yaml/threshold.yaml 82 | env: 83 | CHEF_LICENSE: "accept" 84 | - name: Validate Output Profiles with inspec check 85 | run: | 86 | gem install inspec-bin 87 | inspec check xlsx2inspec_test 88 | inspec check csv2inspec_test 89 | inspec check xccdf2inspec_test 90 | inspec check xccdf2inspec_replace_test 91 | inspec check pdf2inspec_test 92 | env: 93 | CHEF_LICENSE: "accept" 94 | - name: Validate Output CKL with xmllint 95 | if: matrix.platform == 'ubuntu-latest' 96 | uses: ChristophWurst/xmllint-action@v1 97 | with: 98 | xml-file: inspec2ckl_test_1.ckl 99 | xml-schema-file: test/schemas/U_Checklist_Schema_V2-3.xsd 100 | - name: Validate Output CKL with xmllint 101 | if: matrix.platform == 'ubuntu-latest' 102 | uses: ChristophWurst/xmllint-action@v1 103 | with: 104 | xml-file: test/data/inspec2ckl_test_2.ckl 105 | xml-schema-file: test/schemas/U_Checklist_Schema_V2-3.xsd 106 | - name: Validate XCCDF 1.1 with xmllint (Inspec Version < 4.28) 107 | if: matrix.platform == 'ubuntu-latest' 108 | uses: ChristophWurst/xmllint-action@v1 109 | with: 110 | xml-file: inspec2xccdf_11_1.xml 111 | xml-schema-file: test/schemas/xccdf_114/xccdf-1.1.4.xsd 112 | - name: Validate XCCDF 1.1 with xmllint (Inspec Version > 4.28) 113 | if: matrix.platform == 'ubuntu-latest' 114 | uses: ChristophWurst/xmllint-action@v1 115 | with: 116 | xml-file: inspec2xccdf_11_2.xml 117 | xml-schema-file: test/schemas/xccdf_114/xccdf-1.1.4.xsd 118 | 119 | test: 120 | strategy: 121 | fail-fast: false 122 | matrix: 123 | platform: [ ubuntu-latest, macos-latest, windows-latest ] 124 | ruby: [ 2.7 ] 125 | runs-on: ${{ matrix.platform }} 126 | steps: 127 | - name: Setup Ruby ${{matrix.ruby}} 128 | uses: ruby/setup-ruby@v1 129 | with: 130 | ruby-version: ${{matrix.ruby}} 131 | - uses: actions/checkout@v2 132 | with: 133 | lfs: true 134 | fetch-depth: 0 135 | - name: Install bundler and git-lite-version-bump 136 | run: gem install bundler git-lite-version-bump 137 | - name: Run bundle install 138 | run: bundle install 139 | - name: Generate mapping objects 140 | run: bundle exec rake generate_mapping_objects 141 | - name: Run rake test 142 | run: bundle exec rake test 143 | -------------------------------------------------------------------------------- /.github/workflows/prep-release.yml: -------------------------------------------------------------------------------- 1 | name: Draft Release 2 | 3 | on: 4 | push: 5 | # branches to consider in the event; optional, defaults to all 6 | branches: 7 | - master 8 | 9 | jobs: 10 | draft_release: 11 | runs-on: ubuntu-latest 12 | steps: 13 | # Drafts your next Release notes as Pull Requests are merged into "master" 14 | - uses: toolmantim/release-drafter@v5 15 | env: 16 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 17 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release Management 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | release: 9 | name: Release to gem hosts and docker registry 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Setup ruby 13 | uses: actions/setup-ruby@v1 14 | - uses: actions/checkout@v2 15 | with: 16 | fetch-depth: 0 17 | lfs: true 18 | - run: git fetch origin +refs/tags/*:refs/tags/* 19 | - name: Setup credentials and versioning 20 | run: | 21 | gem install git-lite-version-bump keycutter roo 22 | mkdir -p $HOME/.gem 23 | touch $HOME/.gem/credentials 24 | chmod 0600 $HOME/.gem/credentials 25 | printf -- "---\n:rubygems_api_key: ${RUBYGEMS_API_KEY}\n" > $HOME/.gem/credentials 26 | printf -- ":github: Bearer ${GPR_API_KEY}\n" >> $HOME/.gem/credentials 27 | env: 28 | RUBYGEMS_API_KEY: ${{secrets.RUBYGEMS_AUTH_TOKEN}} 29 | GPR_API_KEY: ${{secrets.GITHUB_TOKEN}} 30 | - name: Build gem 31 | run: rake build_release 32 | - name: Get gem version 33 | run: echo "::set-env name=VERSION::$(ls *.gem | grep -Po '(\d+.)+\d+')" 34 | env: 35 | ACTIONS_ALLOW_UNSECURE_COMMANDS: true 36 | - name: Publish to RubyGems 37 | run: | 38 | gem push --KEY rubygems --host https://rubygems.org *.gem 39 | - name: Publish to GPR 40 | run: | 41 | gem push --KEY github --host https://rubygems.pkg.github.com/${OWNER} *.gem 42 | env: 43 | OWNER: mitre 44 | - name: Log in to docker 45 | run: echo "${{secrets.DOCKERHUB_PASSWORD}}" | docker login -u ${{secrets.DOCKERHUB_USERNAME}} --password-stdin 46 | - name: Build container 47 | run: docker build . --tag mitre/inspec_tools:$VERSION --tag mitre/inspec_tools:latest 48 | - name: Publish container to docker registry 49 | run: | 50 | docker push mitre/inspec_tools:$VERSION 51 | docker push mitre/inspec_tools:latest 52 | 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | *.swp 4 | .bundle 5 | .config 6 | .idea 7 | .yardoc 8 | .rake_tasks~ 9 | _yardoc 10 | Gemfile.lock 11 | coverage 12 | doc/ 13 | InstalledFiles 14 | lib/bundler/man 15 | pkg 16 | rdoc 17 | spec/reports 18 | test/tmp 19 | test/version_tmp 20 | pdf_text 21 | debug_text 22 | tmp 23 | # Ignore Mac DS_Store files 24 | **/.DS_Store 25 | profile/ 26 | lib/data/cis_to_nist_mapping 27 | lib/data/cis_to_nist_critical_controls 28 | vendor/* 29 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | --- 2 | inherit_from: .rubocop_todo.yml 3 | 4 | require: rubocop-minitest 5 | 6 | AllCops: 7 | TargetRubyVersion: 2.7 8 | Exclude: 9 | - Gemfile 10 | - Rakefile 11 | - 'vendor/**/*' 12 | Gemspec/DateAssignment: # (new in 1.10) 13 | Enabled: true 14 | Layout/SpaceBeforeBrackets: # (new in 1.7) 15 | Enabled: true 16 | Lint/AmbiguousAssignment: # (new in 1.7) 17 | Enabled: true 18 | Lint/DeprecatedConstants: # (new in 1.8) 19 | Enabled: true 20 | Lint/DuplicateBranch: # (new in 1.3) 21 | Enabled: true 22 | Lint/DuplicateRegexpCharacterClassElement: # (new in 1.1) 23 | Enabled: true 24 | Lint/EmptyBlock: # (new in 1.1) 25 | Enabled: true 26 | Lint/EmptyClass: # (new in 1.3) 27 | Enabled: true 28 | Lint/LambdaWithoutLiteralBlock: # (new in 1.8) 29 | Enabled: true 30 | Lint/NoReturnInBeginEndBlocks: # (new in 1.2) 31 | Enabled: true 32 | Lint/NumberedParameterAssignment: # (new in 1.9) 33 | Enabled: true 34 | Lint/OrAssignmentToConstant: # (new in 1.9) 35 | Enabled: true 36 | Lint/RedundantDirGlobSort: # (new in 1.8) 37 | Enabled: true 38 | Lint/SymbolConversion: # (new in 1.9) 39 | Enabled: true 40 | Lint/ToEnumArguments: # (new in 1.1) 41 | Enabled: true 42 | Lint/TripleQuotes: # (new in 1.9) 43 | Enabled: true 44 | Lint/UnexpectedBlockArity: # (new in 1.5) 45 | Enabled: true 46 | Lint/UnmodifiedReduceAccumulator: # (new in 1.1) 47 | Enabled: true 48 | Style/ArgumentsForwarding: # (new in 1.1) 49 | Enabled: true 50 | Style/CollectionCompact: # (new in 1.2) 51 | Enabled: true 52 | Style/DocumentDynamicEvalDefinition: # (new in 1.1) 53 | Enabled: true 54 | Style/EndlessMethod: # (new in 1.8) 55 | Enabled: true 56 | Style/HashConversion: # (new in 1.10) 57 | Enabled: true 58 | Style/HashExcept: # (new in 1.7) 59 | Enabled: true 60 | Style/IfWithBooleanLiteralBranches: # (new in 1.9) 61 | Enabled: true 62 | Style/NegatedIfElseCondition: # (new in 1.2) 63 | Enabled: true 64 | Style/NilLambda: # (new in 1.3) 65 | Enabled: true 66 | Style/RedundantArgument: # (new in 1.4) 67 | Enabled: true 68 | Style/StringChars: # (new in 1.12) 69 | Enabled: true 70 | Style/SwapValues: # (new in 1.1) 71 | Enabled: true 72 | Minitest/AssertInDelta: # (new in 0.10) 73 | Enabled: true 74 | Minitest/AssertionInLifecycleHook: # (new in 0.10) 75 | Enabled: true 76 | Minitest/AssertKindOf: # (new in 0.10) 77 | Enabled: true 78 | Minitest/AssertOutput: # (new in 0.10) 79 | Enabled: false 80 | Minitest/AssertPathExists: # (new in 0.10) 81 | Enabled: true 82 | Minitest/AssertSilent: # (new in 0.10) 83 | Enabled: true 84 | Minitest/AssertWithExpectedArgument: # (new in 0.11) 85 | Enabled: true 86 | Minitest/LiteralAsActualArgument: # (new in 0.10) 87 | Enabled: true 88 | Minitest/RefuteInDelta: # (new in 0.10) 89 | Enabled: true 90 | Minitest/RefuteKindOf: # (new in 0.10) 91 | Enabled: true 92 | Minitest/RefutePathExists: # (new in 0.10) 93 | Enabled: true 94 | Minitest/TestMethodName: # (new in 0.10) 95 | Enabled: true 96 | Minitest/UnspecifiedException: # (new in 0.10) 97 | Enabled: true 98 | Minitest/MultipleAssertions: 99 | Enabled: false 100 | Style/Documentation: 101 | Enabled: false 102 | Layout/MultilineBlockLayout: 103 | Enabled: true 104 | Layout/MultilineAssignmentLayout: 105 | Enabled: true 106 | Layout/ParameterAlignment: 107 | Enabled: true 108 | Style/Encoding: 109 | Enabled: false 110 | Style/HashSyntax: 111 | Enabled: true 112 | Layout/LineLength: 113 | Enabled: true 114 | AutoCorrect: true 115 | Max: 8192 116 | Layout/EmptyLinesAroundBlockBody: 117 | Enabled: true 118 | Metrics/BlockLength: 119 | Enabled: true 120 | Max: 8192 121 | Style/NumericLiterals: 122 | MinDigits: 10 123 | Metrics/ModuleLength: 124 | Enabled: true 125 | Style/StringLiterals: 126 | Enabled: true 127 | Style/PercentLiteralDelimiters: 128 | PreferredDelimiters: 129 | '%': '{}' 130 | '%i': () 131 | '%q': '{}' 132 | '%Q': () 133 | '%r': '{}' 134 | '%s': () 135 | '%w': '{}' 136 | '%W': () 137 | '%x': () 138 | Layout/HashAlignment: 139 | Enabled: true 140 | Naming/PredicateName: 141 | Enabled: false 142 | Style/ClassAndModuleChildren: 143 | Enabled: true 144 | Style/ConditionalAssignment: 145 | Enabled: false 146 | Style/AndOr: 147 | Enabled: false 148 | Style/Not: 149 | Enabled: false 150 | Naming/FileName: 151 | Enabled: false 152 | Style/TrailingCommaInArrayLiteral: 153 | EnforcedStyleForMultiline: comma 154 | Style/TrailingCommaInArguments: 155 | EnforcedStyleForMultiline: comma 156 | Style/NegatedIf: 157 | Enabled: false 158 | Style/UnlessElse: 159 | Enabled: false 160 | Style/BlockDelimiters: 161 | Enabled: true 162 | Layout/SpaceAroundOperators: 163 | Enabled: false 164 | Style/IfUnlessModifier: 165 | Enabled: false 166 | Style/FrozenStringLiteralComment: 167 | Enabled: false 168 | Style/SignalException: 169 | Enabled: false 170 | Style/HashEachMethods: 171 | Enabled: false 172 | Style/HashTransformKeys: 173 | Enabled: false 174 | Style/HashTransformValues: 175 | Enabled: false 176 | Layout/AccessModifierIndentation: 177 | Enabled: true 178 | EnforcedStyle: indent 179 | IndentationWidth: 2 180 | Layout/ArgumentAlignment: 181 | Enabled: true 182 | EnforcedStyle: with_first_argument 183 | IndentationWidth: 2 184 | Layout/ArrayAlignment: 185 | Enabled: true 186 | Layout/AssignmentIndentation: 187 | Enabled: true 188 | Layout/IndentationConsistency: 189 | Enabled: true 190 | EnforcedStyle: normal 191 | Layout/IndentationWidth: 192 | Enabled: true 193 | Width: 2 194 | Layout/InitialIndentation: 195 | Enabled: true 196 | Layout/LeadingCommentSpace: 197 | Enabled: true 198 | -------------------------------------------------------------------------------- /.rubocop_todo.yml: -------------------------------------------------------------------------------- 1 | # This configuration was generated by 2 | # `rubocop --auto-gen-config` 3 | # on 2021-05-11 19:56:27 UTC using RuboCop version 1.14.0. 4 | # The point is for the user to remove these configuration records 5 | # one by one as the offenses are removed from the code base. 6 | # Note that changes in the inspected code, or installation of new 7 | # versions of RuboCop, may require this file to be generated again. 8 | 9 | # Offense count: 3 10 | Lint/FloatComparison: 11 | Exclude: 12 | - 'lib/inspec_tools/summary.rb' 13 | - 'lib/utilities/inspec_util.rb' 14 | - 'lib/utilities/xccdf/xccdf_score.rb' 15 | 16 | # Offense count: 42 17 | # Configuration parameters: IgnoredMethods, CountRepeatedAttributes. 18 | Metrics/AbcSize: 19 | Max: 182 20 | 21 | # Offense count: 9 22 | # Configuration parameters: CountComments, CountAsOne. 23 | Metrics/ClassLength: 24 | Max: 550 25 | 26 | # Offense count: 17 27 | # Configuration parameters: IgnoredMethods. 28 | Metrics/CyclomaticComplexity: 29 | Max: 30 30 | 31 | # Offense count: 66 32 | # Configuration parameters: CountComments, CountAsOne, ExcludedMethods, IgnoredMethods. 33 | Metrics/MethodLength: 34 | Max: 44 35 | 36 | # Offense count: 14 37 | # Configuration parameters: IgnoredMethods. 38 | Metrics/PerceivedComplexity: 39 | Max: 30 40 | 41 | # Offense count: 2 42 | # Configuration parameters: AllowedNames. 43 | # AllowedNames: module_parent 44 | Naming/ClassAndModuleCamelCase: 45 | Exclude: 46 | - 'lib/happy_mapper_tools/cci_attributes.rb' 47 | 48 | # Offense count: 1 49 | Security/MarshalLoad: 50 | Exclude: 51 | - 'lib/utilities/cis_to_nist.rb' 52 | 53 | # Offense count: 1 54 | # Configuration parameters: MinBodyLength. 55 | Style/GuardClause: 56 | Exclude: 57 | - 'lib/inspec_tools/plugin_cli.rb' 58 | 59 | # Offense count: 5 60 | # Configuration parameters: AllowedMethods. 61 | # AllowedMethods: respond_to_missing? 62 | Style/OptionalBooleanParameter: 63 | Exclude: 64 | - 'lib/inspec_tools/csv.rb' 65 | - 'lib/inspec_tools/inspec.rb' 66 | - 'lib/inspec_tools/pdf.rb' 67 | - 'lib/inspec_tools/xlsx_tool.rb' 68 | - 'lib/utilities/inspec_util.rb' 69 | 70 | # Offense count: 1 71 | # Cop supports --auto-correct. 72 | Style/StringConcatenation: 73 | Exclude: 74 | - 'lib/happy_mapper_tools/stig_attributes.rb' 75 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ruby:2-alpine 2 | COPY *.gem /tmp 3 | ENV CHEF_LICENSE=accept-silent 4 | RUN apk add --no-cache build-base 5 | RUN gem install /tmp/*.gem 6 | RUN apk del build-base 7 | ENTRYPOINT ["inspec_tools"] 8 | VOLUME ["/share"] 9 | WORKDIR /share 10 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | # Specify your gem dependencies in inspec_tools.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Licensed under the Apache 2.0 license. 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 4 | 5 | * Redistributions of source code must retain the above copyright/ digital rights legend, this list of conditions and the following Notice. 6 | 7 | * Redistributions in binary form must reproduce the above copyright copyright/ digital rights legend, this list of conditions and the following Notice in the documentation and/or other materials provided with the distribution. 8 | 9 | * Neither the name of The MITRE Corporation nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 10 | 11 | MITRE’s licensed products include third-party materials that are subject to open source or free software licenses (“Open Source Materials”). The Open Source Materials are as follows: 12 | 13 | * CIS Benchmarks. Please visit www.cissecurity.org for full terms of use. 14 | 15 | The Open Source Materials are licensed under the terms of the applicable third-party licenses that accompany the Open Source Materials. MITTRE’s license does not limit a licensee’s rights under the terms of the Open Source Materials license. MITRE’s license also does not grant licensee rights to the Open Source Materials that supersede the terms and conditions of the Open Source Materials license. 16 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake/testtask' 2 | require File.expand_path('../lib/inspec_tools/version', __FILE__) 3 | 4 | Rake::TestTask.new(:test) do |t| 5 | t.libs << 'test' 6 | t.libs << 'lib' 7 | t.test_files = FileList['test/**/*_test.rb'] 8 | end 9 | 10 | namespace :test do 11 | Rake::TestTask.new(:windows) do |t| 12 | t.libs << 'test' 13 | t.libs << "lib" 14 | t.test_files = Dir.glob([ 15 | 'test/unit/inspec_tools/csv_test.rb', 16 | 'test/unit/inspec_tools/inspec_test.rb', 17 | 'test/unit/inspec_tools/xccdf_test.rb', 18 | 'test/unit/utils/inspec_util_test.rb', 19 | 'test/unit/inspec_tools_test.rb', 20 | 'test/unit/inspec_tools/metadata_test.rb' 21 | ]) 22 | end 23 | 24 | Rake::TestTask.new(:exclude_slow) do |t| 25 | t.description = 'Excluding all tests that take more than 3 seconds to complete' 26 | t.libs << 'test' 27 | t.libs << "lib" 28 | t.verbose = true 29 | t.test_files = FileList['test/**/*_test.rb'].reject{|file| file.include? 'pdf_test.rb'}.reverse 30 | end 31 | end 32 | 33 | desc 'Build for release' 34 | task :build_release do 35 | 36 | Rake::Task["generate_mapping_objects"].reenable 37 | Rake::Task["generate_mapping_objects"].invoke 38 | 39 | system('gem build inspec_tools.gemspec') 40 | end 41 | 42 | desc 'Generate mapping objects' 43 | task :generate_mapping_objects do 44 | require 'roo' 45 | 46 | nist_mapping_cis_controls = ENV['NIST_MAPPING_CIS_CONTROLS'] || 'NIST_Map_02052020_CIS_Controls_Version_7.1_Implementation_Groups_1.2.xlsx'.freeze 47 | nist_mapping_cis_critical_controls = ENV['NIST_MAPPING_CIS_CRITICAL_CONTROLS'] || 'NIST_Map_09212017B_CSC-CIS_Critical_Security_Controls_VER_6.1_Excel_9.1.2016.xlsx'.freeze 48 | 49 | data_root_path = File.join(File.expand_path(__dir__), 'lib', 'data') 50 | cis_controls_path = File.join(data_root_path, nist_mapping_cis_controls) 51 | cis_critical_controls_path = File.join(data_root_path, nist_mapping_cis_critical_controls) 52 | 53 | raise "#{cis_controls_path} does not exist" unless File.exist?(cis_controls_path) 54 | 55 | raise "#{cis_critical_controls_path} does not exist" unless File.exist?(cis_critical_controls_path) 56 | 57 | marshal_cis_controls(cis_controls_path, data_root_path) 58 | marshal_cis_critical_controls(cis_critical_controls_path, data_root_path) 59 | end 60 | 61 | def marshal_cis_controls(cis_controls_path, data_root_path) 62 | cis_to_nist = {} 63 | Roo::Spreadsheet.open(cis_controls_path).sheet(3).each do |row| 64 | if row[3].is_a?(Numeric) 65 | cis_to_nist[row[3].to_s] = row[0] 66 | else 67 | cis_to_nist[row[2].to_s] = row[0] unless (row[2] == '') || row[2].to_i.nil? 68 | end 69 | end 70 | output_file = File.new(File.join(data_root_path, 'cis_to_nist_mapping'), 'w') 71 | Marshal.dump(cis_to_nist, output_file) 72 | output_file.close 73 | end 74 | 75 | def marshal_cis_critical_controls(cis_critical_controls_path, data_root_path) 76 | controls_spreadsheet = Roo::Spreadsheet.open(cis_critical_controls_path) 77 | controls_spreadsheet.default_sheet = 'VER 6.1 Controls' 78 | headings = {} 79 | controls_spreadsheet.row(3).each_with_index { |header, idx| headings[header] = idx } 80 | 81 | nist_ver = 4 82 | cis_ver = controls_spreadsheet.row(2)[4].split(' ')[-1] 83 | control_count = 1 84 | mapping = [] 85 | ((controls_spreadsheet.first_row + 3)..controls_spreadsheet.last_row).each do |row_value| 86 | current_row = {} 87 | if controls_spreadsheet.row(row_value)[headings['NIST SP 800-53 Control #']].to_s != '' 88 | current_row[:nist] = controls_spreadsheet.row(row_value)[headings['NIST SP 800-53 Control #']].to_s 89 | else 90 | current_row[:nist] = 'Not Mapped' 91 | end 92 | current_row[:nist_ver] = nist_ver 93 | if controls_spreadsheet.row(row_value)[headings['Control']].to_s == '' 94 | current_row[:cis] = control_count.to_s 95 | control_count += 1 96 | else 97 | current_row[:cis] = controls_spreadsheet.row(row_value)[headings['Control']].to_s 98 | end 99 | current_row[:cis_ver] = cis_ver 100 | mapping << current_row 101 | end 102 | output_file = File.new(File.join(data_root_path, 'cis_to_nist_critical_controls'), 'w') 103 | Marshal.dump(mapping, output_file) 104 | output_file.close 105 | end 106 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /examples/CIS_Ubuntu_Linux_16.04_LTS_Benchmark_v1.0.0.pdf: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:18d1734a5d5dee94b50a855edc7e8299ca22a096b13f3a102057318d7d5a4170 3 | size 2043778 4 | -------------------------------------------------------------------------------- /examples/attribute.json: -------------------------------------------------------------------------------- 1 | { 2 | "benchmark.title": "example_title", 3 | "benchmark.id": "example_id", 4 | "benchmark.description": "some description", 5 | "benchmark.version": "0.1.0", 6 | "benchmark.status": "accepted", 7 | "benchmark.status.date": "03-04-05", 8 | "benchmark.notice": "some notice", 9 | "benchmark.plaintext": "plaintext", 10 | "benchmark.plaintext.id": "plaintext_id", 11 | "reference.href": "https://someurl.com", 12 | "reference.dc.source": "some source" 13 | } -------------------------------------------------------------------------------- /examples/csv2inspec/mapping.yml: -------------------------------------------------------------------------------- 1 | # Setting csv_header to true will skip the csv file header 2 | skip_csv_header: true 3 | width : 80 4 | 5 | 6 | control.id: 0 7 | control.title: 15 8 | control.desc: 16 9 | control.tags: 10 | severity: 1 11 | rid: 8 12 | stig_id: 3 13 | cci: 2 14 | check: 12 15 | fix: 10 16 | 17 | -------------------------------------------------------------------------------- /examples/inspec2ckl/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "stigid" : "RHEL_7_STIG", 3 | "hostname" : "myawesome", 4 | "ip" : "10.10.10.10", 5 | "fqdn" : "myawesome.host.com", 6 | "mac": "aa:aa:99:99:99:99", 7 | "identity": { 8 | "identity": "userabc", 9 | "privileged": true 10 | }, 11 | "benchmark": { 12 | "title": "SI_DATA title, STIG_DATA STIGRef", 13 | "plaintext": "SI_DATA releaseinfo, STIG_DATA STIGRef", 14 | "version": "SI_DATA version, STIG_DATA STIGRef" 15 | }, 16 | "organization": "MITRE Corporation", 17 | "role": "my role", 18 | "type": "my type", 19 | "tech_area": "my area", 20 | "target_area": "my area", 21 | "web_or_database": "my database", 22 | "web_db_site": "my web_db site", 23 | "web_db_instance": "my web_db instance" 24 | } 25 | -------------------------------------------------------------------------------- /examples/inspec2xccdf/README.md: -------------------------------------------------------------------------------- 1 | # Inspec2XCCDF Usage information 2 | 3 | The InSpec to XCCDF Parser scans and extracts the results defined in the Inspec JSON results and converts them into a 4 | XCCDF XML file to enable portability in publishing the execution results in consuming tools. 5 | 6 | The parser requires two files: 7 | 8 | 1. The Inspec JSON results file 9 | 1. The XCCDF attributes file. See `xccdf2inspec` option `--attributes` for how to generate a base attribute file from the source specification. 10 | 11 | If all of the following requirements are followed, a XML will be produced conforming to the [XCCDF 1.1 specification](https://csrc.nist.gov/publications/detail/nistir/7275/rev-3/final). 12 | Note: All files in the /test/schemas/xccdf_114 directory are directly sourced from . 13 | 14 | ## XCCDF attributes YAML file 15 | 16 | Inspec is unable to produce certain data that is required for conversion into conforming XCCDF. The attributes marked 'Required' 17 | below `MUST` be included in a XCCDF attributes YAML file and provided as part of the Inspec2XCCDF conversion process. 18 | 19 | ```yaml 20 | benchmark.id # Required: Benchmark id 21 | benchmark.status # Required: Benchmark status. Must be one of 'accepted', 'deprecated', 'draft', 'incomplete', 'interim' 22 | benchmark.version # Required: Benchmark version 23 | ``` 24 | 25 | The following attributes `SHOULD` be included in order to more closely generate an XCCDF that matches the original. 26 | 27 | ```yaml 28 | benchmark.status.date # Optional: Benchmark status date 29 | benchmark.title # Optional: Benchmark title 30 | reference.href # Optional: Benchmark reference href 31 | reference.dc.publisher # Optional: Benchmark and Rule reference publisher 32 | reference.dc.source # Optional: Benchmark and Rule reference source 33 | reference.dc.title # Optional: Rule reference title 34 | reference.dc.subject # Optional: Rule reference subject 35 | reference.dc.type # Optional: Rule reference type 36 | reference.dc.identifier # Optional: Rule reference identifier 37 | ``` 38 | 39 | ## Metadata json file 40 | 41 | ### Inclusion of test results within the XCCDF output 42 | 43 | Test results from an Inspec execution will be included in the output only if fqdn is provided at minimum for the fulfilment 44 | of valid XCCDF. 45 | 46 | Example execution: 47 | 48 | ``` 49 | inspec_tools inspec2xccdf -j examples/sample_json/rhel-simp.json -a lib/data/attributes.yml -m examples/inspec2xccdf/metadata.json -o output.xccdf 50 | ``` 51 | 52 | JSON format: 53 | 54 | ```text 55 | "hostname" : "myawesome", 56 | "ip" : "10.10.10.10", # Optional: A IPV4, IPV6, or MAC address. Applied to TestResult target-address and target-facts element. 57 | "fqdn" : "myawesome.host.com", # Required: The host that is the target of the execution. Applied to TestResult target element. 58 | "mac" : "aa:aa:99:99:99:99", # Optional: A MAC address to include. Applied to TestResult target-facts element. 59 | "identity" : { 60 | "identity" : "userabc", # Optional: Account used to perform scan operation. Applied to TestResult identity element. 61 | "privileged" : true, # Optional: Indicator of whether the identity has priviliged access. Applied to TestResult identity element. 62 | }, 63 | "organization" : "MITRE Corporation" # Optional: Name of organization applying this benchmark. Applied to TestResult organization element. 64 | ``` 65 | 66 | ## Inspec JSON result file 67 | 68 | Inspec will not prevent execution of controls with missing required tags defined since it is a general purpose framework. 69 | However, doing so will result in non-conforming XCCDF 1.1 output. In order to generate conforming XCCDF, the tags marked 70 | 'Required' below MUST be included in each of the Inspec controls. 71 | 72 | | Tag | Required | XCCDF Element | 73 | | --- | --- | --- | 74 | | gid | yes | Group attribute id | 75 | | gdescription | no | Group description | 76 | | gtitle | no | Group title | 77 | | rid | yes | Rule attribute id | 78 | | severity | yes | Rule attribute severity. Must be one of 'unknown', 'info', 'low', 'medium', 'high' | 79 | | rweight | no | Rule weight. If missing, this may make the scoring results out of line as compared to an originating XCCDF Benchmark specification. | 80 | | title | no | Rule title | 81 | | cci | no | Rule ident | 82 | | fix | no | Rule fixTextType content | 83 | | fixref | no | Rule fixTextType fixref | 84 | | checkref | no | TestResult rule-result check system attribute | 85 | 86 | -------------------------------------------------------------------------------- /examples/inspec2xccdf/bad_test.json: -------------------------------------------------------------------------------- 1 | { 2 | "benchmark.id": "example_id", 3 | "benchmark.status": "accepted", 4 | "benchmark.version": "0.1.0", 5 | "test_result.target.target": "some.host" 6 | } -------------------------------------------------------------------------------- /examples/inspec2xccdf/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "hostname" : "myawesome", 3 | "ip" : "10.10.10.10", 4 | "fqdn" : "myawesome.host.com", 5 | "mac" : "aa:aa:99:99:99:99", 6 | "identity" : { 7 | "identity" : "userabc", 8 | "privileged" : true 9 | }, 10 | "organization" : "MITRE Corporation" 11 | } -------------------------------------------------------------------------------- /examples/inspec2xccdf/xccdf_compliant_attribute.json: -------------------------------------------------------------------------------- 1 | { 2 | "benchmark.title": "example_title", 3 | "benchmark.id": "example_id", 4 | "benchmark.description": "some description", 5 | "benchmark.version": "0.1.0", 6 | "benchmark.status": "accepted", 7 | "benchmark.status.date": "2003-04-05", 8 | "benchmark.notice": "some notice", 9 | "benchmark.notice.id": "terms-of-use", 10 | "benchmark.plaintext": "plaintext", 11 | "benchmark.plaintext.id": "plaintext_id", 12 | "reference.href": "https://someurl.com", 13 | "reference.dc.source": "some source", 14 | "reference.dc.publisher": "some publisher", 15 | "reference.dc.title": "some title", 16 | "reference.dc.subject": "some subject", 17 | "reference.dc.type": "some type", 18 | "reference.dc.identifier": "some id", 19 | "content_ref.name": "M", 20 | "content_ref.href": "SOME_STIG.xml" 21 | } -------------------------------------------------------------------------------- /examples/sample_json/inspec-v4.28.0.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example_id", 3 | "title": "bTitle", 4 | "maintainer": "The Authors", 5 | "copyright": "The Authors", 6 | "copyright_email": "you@example.com", 7 | "license": "Apache-2.0", 8 | "summary": "bDescription", 9 | "version": "0.1.0", 10 | "supports": [], 11 | "controls": [ 12 | { 13 | "title": "Ensure configuration is set in situations", 14 | "desc": "Identify the threat actor and threat vector.\n \n Describe the mitigation.\n \n Note the external dependencies of the configuration.", 15 | "descriptions": { 16 | "default": "Identify the threat actor and threat vector.\n \n Describe the mitigation.\n \n Note the external dependencies of the configuration.", 17 | "rationale": "", 18 | "check": "Describe preconditions for conducting the check.\n \nList each step of the check.\n\nIdentify mitigating factors.\n\nDefine success or failure conditions.", 19 | "fix": "Describe preconditions for changing the configuration.\n\nList each step of applying the configuration.\n\nIdentify risks to confidentialty, integrity, or availability associated with applying the configuration." 20 | }, 21 | "impact": 0.5, 22 | "refs": [], 23 | "tags": { 24 | "severity": "low", 25 | "gtitle": "SRG-APP-000220-ZZZ-567890", 26 | "gid": "gid_unused", 27 | "rid": "r1_rule", 28 | "stig_id": "stig_id_unused", 29 | "fix_id": "fix_id_unused", 30 | "cci": [ 31 | "CCI-001499", 32 | "CCI-000197" 33 | ], 34 | "legacy": [ 35 | "V-72845", 36 | "SV-87497" 37 | ], 38 | "nist": [ 39 | "CM-5 (6)", 40 | "IA-5 (1) (c)" 41 | ] 42 | }, 43 | "code": "control 'X-123456' do\n title 'Ensure configuration is set in situations'\n desc \"Identify the threat actor and threat vector.\n \n Describe the mitigation.\n \n Note the external dependencies of the configuration.\n \"\n desc 'rationale', ''\n desc 'check', \"Describe preconditions for conducting the check.\n \nList each step of the check.\n\nIdentify mitigating factors.\n\nDefine success or failure conditions.\n\"\n desc 'fix', \"\n Describe preconditions for changing the configuration.\n\n List each step of applying the configuration.\n\n Identify risks to confidentialty, integrity, or availability associated with applying the configuration.\n \"\n impact 0.5\n tag severity: 'low'\n tag gtitle: 'SRG-APP-000220-ZZZ-567890'\n tag gid: 'gid_unused'\n tag rid: 'r1_rule'\n tag stig_id: 'stig_id_unused'\n tag fix_id: 'fix_id_unused'\n tag cci: ['CCI-001499', 'CCI-000197']\n tag legacy: ['V-72845', 'SV-87497']\n tag nist: ['CM-5 (6)', 'IA-5 (1) (c)']\nend\n", 44 | "source_location": { 45 | "ref": "./controls/g1Identifier.rb", 46 | "line": 3 47 | }, 48 | "id": "X-123456" 49 | }, 50 | { 51 | "title": "Ensure a log metric filter and alarm exist for AWS Config\nconfiguration changes", 52 | "desc": "Real-time monitoring of API calls can be achieved by directing\nCloudTrail Logs to CloudWatch Logs and establishing corresponding metric\nfilters and alarms. It is recommended that a metric filter and alarm be\nestablished for detecting changes to CloudTrail's configurations.", 53 | "descriptions": { 54 | "default": "Real-time monitoring of API calls can be achieved by directing\nCloudTrail Logs to CloudWatch Logs and establishing corresponding metric\nfilters and alarms. It is recommended that a metric filter and alarm be\nestablished for detecting changes to CloudTrail's configurations.", 55 | "rationale": "", 56 | "check": "N/A", 57 | "fix": "ft2FixText" 58 | }, 59 | "impact": 0.5, 60 | "refs": [], 61 | "tags": { 62 | "severity": "medium", 63 | "gtitle": "g2Title", 64 | "gid": "g2Identifier", 65 | "rid": "r2_rule", 66 | "stig_id": "r2Version", 67 | "fix_id": "f2Identifier", 68 | "cci": [ 69 | "CCI-001495", 70 | "CCI-000196" 71 | ], 72 | "legacy": [ 73 | "identVLegacy3", 74 | "identVLegacy4" 75 | ], 76 | "nist": [ 77 | "AU-9", 78 | "IA-5 (1) (c)" 79 | ] 80 | }, 81 | "code": "control 'g2Identifier' do\n title \"Ensure a log metric filter and alarm exist for AWS Config\nconfiguration changes\"\n desc \"Real-time monitoring of API calls can be achieved by directing\nCloudTrail Logs to CloudWatch Logs and establishing corresponding metric\nfilters and alarms. It is recommended that a metric filter and alarm be\nestablished for detecting changes to CloudTrail's configurations.\"\n desc 'rationale', ''\n desc 'check', 'N/A'\n desc 'fix', 'ft2FixText'\n impact 0.5\n tag severity: 'medium'\n tag gtitle: 'g2Title'\n tag gid: 'g2Identifier'\n tag rid: 'r2_rule'\n tag stig_id: 'r2Version'\n tag fix_id: 'f2Identifier'\n tag cci: ['CCI-001495', 'CCI-000196']\n tag legacy: ['identVLegacy3', 'identVLegacy4']\n tag nist: ['AU-9', 'IA-5 (1) (c)']\nend\n", 82 | "source_location": { 83 | "ref": "./controls/g2Identifier.rb", 84 | "line": 3 85 | }, 86 | "id": "g2Identifier" 87 | }, 88 | { 89 | "title": "Ensure a log metric filter and alarm exist for AWS Config\nconfiguration changes", 90 | "desc": "Real-time monitoring of API calls can be achieved by directing\nCloudTrail Logs to CloudWatch Logs and establishing corresponding metric\nfilters and alarms. It is recommended that a metric filter and alarm be\nestablished for detecting changes to CloudTrail's configurations.", 91 | "descriptions": { 92 | "default": "Real-time monitoring of API calls can be achieved by directing\nCloudTrail Logs to CloudWatch Logs and establishing corresponding metric\nfilters and alarms. It is recommended that a metric filter and alarm be\nestablished for detecting changes to CloudTrail's configurations.", 93 | "rationale": "", 94 | "check": "N/A", 95 | "fix": "ft3FixText" 96 | }, 97 | "impact": 0.5, 98 | "refs": [], 99 | "tags": { 100 | "severity": "medium", 101 | "gtitle": "g3Title", 102 | "gid": "g3Identifier", 103 | "rid": "r3_rule", 104 | "stig_id": "r3Version", 105 | "fix_id": "f3Identifier", 106 | "cci": [ 107 | "CCI-001495", 108 | "CCI-000196" 109 | ], 110 | "legacy": [ 111 | "identVLegacy5", 112 | "identVLegacy6" 113 | ], 114 | "nist": [ 115 | "AU-9", 116 | "IA-5 (1) (c)" 117 | ] 118 | }, 119 | "code": "control 'g3Identifier' do\n title \"Ensure a log metric filter and alarm exist for AWS Config\nconfiguration changes\"\n desc \"Real-time monitoring of API calls can be achieved by directing\nCloudTrail Logs to CloudWatch Logs and establishing corresponding metric\nfilters and alarms. It is recommended that a metric filter and alarm be\nestablished for detecting changes to CloudTrail's configurations.\"\n desc 'rationale', ''\n desc 'check', 'N/A'\n desc 'fix', 'ft3FixText'\n impact 0.5\n tag severity: 'medium'\n tag gtitle: 'g3Title'\n tag gid: 'g3Identifier'\n tag rid: 'r3_rule'\n tag stig_id: 'r3Version'\n tag fix_id: 'f3Identifier'\n tag cci: ['CCI-001495', 'CCI-000196']\n tag legacy: ['identVLegacy5', 'identVLegacy6']\n tag nist: ['AU-9', 'IA-5 (1) (c)']\nend\n", 120 | "source_location": { 121 | "ref": "./controls/g3Identifier.rb", 122 | "line": 3 123 | }, 124 | "id": "g3Identifier" 125 | } 126 | ], 127 | "groups": [ 128 | { 129 | "title": null, 130 | "controls": [ 131 | "X-123456" 132 | ], 133 | "id": "controls/g1Identifier.rb" 134 | }, 135 | { 136 | "title": null, 137 | "controls": [ 138 | "g2Identifier" 139 | ], 140 | "id": "controls/g2Identifier.rb" 141 | }, 142 | { 143 | "title": null, 144 | "controls": [ 145 | "g3Identifier" 146 | ], 147 | "id": "controls/g3Identifier.rb" 148 | } 149 | ], 150 | "inputs": [], 151 | "sha256": "4e73883fc2f0d7c85e953346717c149539978c2780bc52c99276e3d6d6fe0567", 152 | "status_message": "", 153 | "status": "loaded", 154 | "generator": { 155 | "name": "inspec", 156 | "version": "4.28.0" 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /examples/sample_yaml/threshold.yaml: -------------------------------------------------------------------------------- 1 | failed: 2 | critical: 3 | max: 0 4 | high: 5 | max: 1 6 | compliance: 7 | min: 81 8 | 9 | 10 | # Alternate Definitions 11 | # ----------------------- 12 | # {compliance: {min: 80}, failed: {critical: {max: 0}, high: {max: 0}}} 13 | # ----------------------- 14 | # {compliance.min: 81, failed.critical.max: 10, failed.high.max: 0} 15 | # ----------------------- 16 | # compliance.min: 81 17 | # failed.critical.max: 10 18 | # failed.high.max: 1 19 | -------------------------------------------------------------------------------- /examples/xccdf2inspec/data/README.md: -------------------------------------------------------------------------------- 1 | # README - Test Data 2 | 3 | The XCCDF to InSpec Parser scans and extracts the Controls defined in the 4 | DISA XCCDF STIG XML documents and converts them into InSpec controls to 5 | help make writing InSpec profiles based on the controls defined in DISA 6 | STIG documents. 7 | 8 | The parser requires two files: 9 | 1. The XCCDF XML file - http://iase.disa.mil/stigs/Pages/a-z.aspx 10 | 2. the CCI XML file: 11 | a. Info: http://iase.disa.mil/stigs/cci/Pages/index.aspx 12 | b. File: http://iasecontent.disa.mil/stigs/zip/u_cci_list.zip 13 | 14 | All test data has been produced by DISA. For any information or questions, please contact IASE. 15 | -------------------------------------------------------------------------------- /examples/xccdf2inspec/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "maintainer" : "author@example.com", 3 | "copyright" : "Example Corporation", 4 | "copyright_email" : "author@example.com", 5 | "license" : "https://www.mit.edu/~amini/LICENSE.md" 6 | } -------------------------------------------------------------------------------- /examples/xlsx2inspec/mapping.cis.yml: -------------------------------------------------------------------------------- 1 | 2 | # Setting csv_header to true will skip the csv file header 3 | skip_csv_header: true 4 | width : 80 5 | 6 | 7 | control.id: 1 8 | control.title: 2 9 | control.desc: 5 10 | control.tags: 11 | cis_controls: 11 12 | check: 8 13 | fix: 7 14 | ref: 13 15 | rationale: 6 16 | # applicability comes from the page of the sheet its in/the title of the sheet 17 | # applicability: 18 | -------------------------------------------------------------------------------- /exe/inspec_tools: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env -S ruby -EUTF-8 2 | 3 | # Trap ^C 4 | Signal.trap('INT') do 5 | puts "\nCtrl-C detected. Exiting..." 6 | sleep 1 7 | exit 8 | end 9 | 10 | $LOAD_PATH.unshift(File.expand_path('../lib', __dir__)) 11 | require 'inspec_tools' 12 | require 'inspec_tools/cli' 13 | 14 | InspecTools::CLI.start(ARGV) 15 | -------------------------------------------------------------------------------- /inspec_tools.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | lib = File.expand_path('lib', __dir__) 4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 5 | 6 | begin 7 | require 'inspec_tools/version' 8 | rescue LoadError 9 | nil 10 | end 11 | 12 | Gem::Specification.new do |spec| 13 | spec.name = 'inspec_tools' 14 | spec.version = 15 | begin 16 | InspecTools::VERSION 17 | rescue StandardError 18 | '0.0.0.1.ENOGVB' 19 | end 20 | spec.authors = ['Robert Thew', 'Matthew Dromazos', 'Rony Xavier', 'Aaron Lippold'] 21 | spec.email = ['saf@mitre.org'] 22 | spec.summary = 'Converter utils for Inspec' 23 | spec.description = 'Converter utils for Inspec that can be included as a gem or used from the command line' 24 | spec.homepage = 'https://inspec-tools.mitre.org/' 25 | spec.license = 'Apache-2.0' 26 | spec.files = Dir.glob('{lib,exe}/**/*').reject! { |file| file.end_with?('.xlsx') } + %w{LICENSE.md Rakefile README.md} 27 | spec.bindir = 'exe' 28 | spec.executables << 'inspec_tools' 29 | spec.require_paths = ['lib'] 30 | 31 | spec.required_ruby_version = '>= 2.7' 32 | 33 | spec.add_runtime_dependency 'colorize', '~> 0' 34 | spec.add_runtime_dependency 'git-lite-version-bump', '>= 0.17.3' 35 | spec.add_runtime_dependency 'inspec', '>= 4.18.100', '< 5.0' 36 | spec.add_runtime_dependency 'inspec_objects', '~> 0.1' 37 | spec.add_runtime_dependency 'nokogiri', '~> 1.8' 38 | spec.add_runtime_dependency 'nokogiri-happymapper', '~> 0' 39 | spec.add_runtime_dependency 'OptionParser', '~> 0' 40 | spec.add_runtime_dependency 'pdf-reader', '~> 2.1' 41 | spec.add_runtime_dependency 'roo', '~> 2.8' 42 | spec.add_runtime_dependency 'rubocop', '~> 1.11' 43 | spec.add_runtime_dependency 'word_wrap', '~> 1.0' 44 | spec.add_development_dependency 'bundler' 45 | spec.add_development_dependency 'bundler-audit' 46 | spec.add_development_dependency 'fakefs' 47 | spec.add_development_dependency 'minitest' 48 | spec.add_development_dependency 'minitest-reporters', '~> 1.4' 49 | spec.add_development_dependency 'o_stream_catcher' 50 | spec.add_development_dependency 'pry' 51 | spec.add_development_dependency 'rake' 52 | spec.add_development_dependency 'rubocop-minitest', '~> 0.12.1' 53 | spec.add_development_dependency 'simplecov' 54 | end 55 | -------------------------------------------------------------------------------- /lib/data/NIST_Map_02052020_CIS_Controls_Version_7.1_Implementation_Groups_1.2.xlsx: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:bcbd9ab4314254846153ec7c53b4022f02cdfacda1a76030291fc06bfc4028a0 3 | size 840324 4 | -------------------------------------------------------------------------------- /lib/data/NIST_Map_09212017B_CSC-CIS_Critical_Security_Controls_VER_6.1_Excel_9.1.2016.xlsx: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:f542d87577aa174edabd919a720769d62bb3054ffb084d06e18aa8719368f855 3 | size 54212 4 | -------------------------------------------------------------------------------- /lib/data/README.TXT: -------------------------------------------------------------------------------- 1 | Data and Sources: 2 | 3 | ### NOTICE 4 | 5 | The Open Source Materials are licensed under the terms of the applicable third-party 6 | licenses that accompany the Open Source Materials. MITTRE’s license does not limit 7 | a licensee’s rights under the terms of the Open Source Materials license. MITRE’s 8 | license also does not grant licensee rights to the Open Source Materials that 9 | supersede the terms and conditions of the Open Source Materials license. 10 | 11 | ### Referenced Data 12 | 13 | * U_CCI_List.xml is published by the DISA FSO and IASE. The Control Correlation 14 | Identifier (CCI) provides a standard identifier and description for each of the 15 | singular, actionable statements that comprise an IA control or IA best practice. 16 | 17 | * cci2html.xsl 18 | - Stylesheet for viewing CCI List as HTML 19 | (open U_CCI_List.xml in a web browser to view) 20 | 21 | see: https://public.cyber.mil/stigs/cci/ 22 | 23 | * NIST_Map_09212017B_CSC-CIS_Critical_Security_Controls_VER_6.1_Excel_9.1.2016.xlsx 24 | 25 | * CIS Benchmarks. Please visit www.cissecurity.org for full terms of use. 26 | -------------------------------------------------------------------------------- /lib/data/attributes.yml: -------------------------------------------------------------------------------- 1 | --- 2 | benchmark.title: PostgreSQL 9.x Security Technical Implementation Guide 3 | benchmark.id: PostgreSQL_9-x_STIG 4 | benchmark.description: 'This Security Technical Implementation Guide is published 5 | as a tool to improve the security of Department of Defense (DoD) information systems. 6 | The requirements are derived from the National Institute of Standards and Technology 7 | (NIST) 800-53 and related documents. Comments or proposed revisions to this document 8 | should be sent via email to the following address: disa.stig_spt@mail.mil.' 9 | benchmark.version: '1' 10 | benchmark.status: accepted 11 | benchmark.status.date: '2017-01-20' 12 | benchmark.notice.id: terms-of-use 13 | benchmark.plaintext: 'Release: 1 Benchmark Date: 20 Jan 2017' 14 | benchmark.plaintext.id: release-info 15 | reference.href: http://iase.disa.mil 16 | reference.dc.publisher: DISA 17 | reference.dc.source: STIG.DOD.MIL 18 | reference.dc.title: DPMS Target PostgreSQL 9.x 19 | reference.dc.subject: PostgreSQL 9.x 20 | reference.dc.type: DPMS Target 21 | reference.dc.identifier: '3087' 22 | content_ref.name: M 23 | content_ref.href: DPMS_XCCDF_Benchmark_PostgreSQL_9-x_STIG.xml 24 | -------------------------------------------------------------------------------- /lib/data/cci2html.xsl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ' 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 19 | CCI List 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | CCI List
29 | Version
30 | Date 31 |
32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 |
60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 |
83 | 84 | 85 | , 86 | 87 |
108 |
109 |
110 | 111 | 112 | 113 | 114 | 115 | References: 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 |
-------------------------------------------------------------------------------- /lib/data/mapping.yml: -------------------------------------------------------------------------------- 1 | 2 | # Setting csv_header to true will skip the csv file header 3 | skip_csv_header: true 4 | width : 80 5 | 6 | 7 | control.id: 0 8 | control.title: 15 9 | control.desc: 16 10 | control.tags: 11 | severity: 1 12 | rid: 8 13 | stig_id: 3 14 | cci: 2 15 | check: 12 16 | fix: 10 17 | 18 | -------------------------------------------------------------------------------- /lib/data/rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | DisabledByDefault: true 3 | Style/StringLiterals: 4 | Enabled: true 5 | -------------------------------------------------------------------------------- /lib/data/threshold.yaml: -------------------------------------------------------------------------------- 1 | compliance: 2 | min: -1 3 | max: -1 4 | passed: 5 | total: 6 | min: -1 7 | max: -1 8 | critical: 9 | min: -1 10 | max: -1 11 | high: 12 | min: -1 13 | max: -1 14 | medium: 15 | min: -1 16 | max: -1 17 | low: 18 | min: -1 19 | max: -1 20 | failed: 21 | total: 22 | min: -1 23 | max: -1 24 | critical: 25 | min: -1 26 | max: -1 27 | high: 28 | min: -1 29 | max: -1 30 | medium: 31 | min: -1 32 | max: -1 33 | low: 34 | min: -1 35 | max: -1 36 | skipped: 37 | total: 38 | min: -1 39 | max: -1 40 | critical: 41 | min: -1 42 | max: -1 43 | high: 44 | min: -1 45 | max: -1 46 | medium: 47 | min: -1 48 | max: -1 49 | low: 50 | min: -1 51 | max: -1 52 | no_impact: 53 | total: 54 | min: -1 55 | max: -1 56 | critical: 57 | min: -1 58 | max: -1 59 | high: 60 | min: -1 61 | max: -1 62 | medium: 63 | min: -1 64 | max: -1 65 | low: 66 | min: -1 67 | max: -1 68 | error: 69 | total: 70 | min: -1 71 | max: -1 72 | critical: 73 | min: -1 74 | max: -1 75 | high: 76 | min: -1 77 | max: -1 78 | medium: 79 | min: -1 80 | max: -1 81 | low: 82 | min: -1 83 | max: -1 -------------------------------------------------------------------------------- /lib/exceptions/impact_input_error.rb: -------------------------------------------------------------------------------- 1 | module Utils 2 | class InspecUtil 3 | class ImpactInputError < ::StandardError 4 | end 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /lib/exceptions/severity_input_error.rb: -------------------------------------------------------------------------------- 1 | module Utils 2 | class InspecUtil 3 | class SeverityInputError < ::StandardError 4 | end 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /lib/happy_mapper_tools/benchmark.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'happymapper' 4 | require 'nokogiri' 5 | 6 | # see: https://github.com/dam5s/happymapper 7 | # Class Status maps from the 'status' from Benchmark XML file using HappyMapper 8 | module HappyMapperTools 9 | module Benchmark 10 | class Status 11 | include HappyMapper 12 | tag 'status' 13 | attribute :date, String, tag: 'date' 14 | content :status, String, tag: 'status' 15 | end 16 | 17 | # Class Notice maps from the 'notice' from Benchmark XML file using HappyMapper 18 | class Notice 19 | include HappyMapper 20 | tag 'notice' 21 | attribute :id, String, tag: 'id' 22 | attribute :xml_lang, String, namespace: 'xml', tag: 'lang' 23 | content :notice, String, tag: 'notice' 24 | end 25 | 26 | # Class ReferenceBenchmark maps from the 'reference' from Benchmark XML file using HappyMapper 27 | class ReferenceBenchmark 28 | include HappyMapper 29 | tag 'reference' 30 | attribute :href, String, tag: 'href' 31 | element :dc_publisher, String, namespace: 'dc', tag: 'publisher' 32 | element :dc_source, String, namespace: 'dc', tag: 'source' 33 | end 34 | 35 | # Class ReferenceGroup maps from the 'reference' from Benchmark XML file using HappyMapper 36 | class ReferenceGroup 37 | include HappyMapper 38 | tag 'reference' 39 | element :dc_title, String, namespace: 'dc', tag: 'title' 40 | element :dc_publisher, String, namespace: 'dc', tag: 'publisher' 41 | element :dc_type, String, namespace: 'dc', tag: 'type' 42 | element :dc_subject, String, namespace: 'dc', tag: 'subject' 43 | element :dc_identifier, String, namespace: 'dc', tag: 'identifier' 44 | end 45 | 46 | # Class Plaintext maps from the 'plain-text' from Benchmark XML file using HappyMapper 47 | class Plaintext 48 | include HappyMapper 49 | tag 'plain-text' 50 | attribute :id, String, tag: 'id' 51 | content :plaintext, String 52 | end 53 | 54 | # Class Select maps from the 'Select' from Benchmark XML file using HappyMapper 55 | class Select 56 | include HappyMapper 57 | tag 'Select' 58 | attribute :idref, String, tag: 'idref' 59 | attribute :selected, String, tag: 'selected' 60 | end 61 | 62 | # Class Ident maps from the 'ident' from Benchmark XML file using HappyMapper 63 | class Ident 64 | include HappyMapper 65 | tag 'ident' 66 | attribute :system, String, tag: 'system' 67 | content :ident, String 68 | def initialize(ident_str) 69 | @ident = ident_str 70 | case ident_str 71 | when /^(CCI-[0-9]{6})$/ 72 | # Match CCI IDs; e.g. CCI-123456 73 | @system = 'http://cyber.mil/cci' 74 | when /^(S?V-[0-9]{5})$/ 75 | # Match SV- IDs; e.g. SV-12345 76 | # Match V- IDs; e.g. V-12345 77 | @system = 'http://cyber.mil/legacy' 78 | else 79 | # for all other ident_str, use the old identifier 80 | @system = 'https://public.cyber.mil/stigs/cci/' 81 | end 82 | end 83 | end 84 | 85 | # Class Fixtext maps from the 'fixtext' from Benchmark XML file using HappyMapper 86 | class Fixtext 87 | include HappyMapper 88 | tag 'fixtext' 89 | attribute :fixref, String, tag: 'fixref' 90 | content :fixtext, String 91 | end 92 | 93 | # Class Fix maps from the 'fixtext' from Benchmark XML file using HappyMapper 94 | class Fix 95 | include HappyMapper 96 | tag 'fixtext' 97 | attribute :id, String, tag: 'id' 98 | end 99 | 100 | # Class ContentRef maps from the 'check-content-ref' from Benchmark XML file using HappyMapper 101 | class ContentRef 102 | include HappyMapper 103 | tag 'check-content-ref' 104 | attribute :name, String, tag: 'name' 105 | attribute :href, String, tag: 'href' 106 | end 107 | 108 | # Class Check maps from the 'Check' from Benchmark XML file using HappyMapper 109 | class Check 110 | include HappyMapper 111 | tag 'check' 112 | attribute :system, String, tag: 'system' 113 | element :content_ref, ContentRef, tag: 'check-content-ref' 114 | element :content, String, tag: 'check-content' 115 | end 116 | 117 | class MessageType 118 | include HappyMapper 119 | attribute :severity, String, tag: 'severity' 120 | content :message, String 121 | end 122 | 123 | class RuleResultType 124 | include HappyMapper 125 | attribute :idref, String, tag: 'idref' 126 | attribute :severity, String, tag: 'severity' 127 | attribute :time, String, tag: 'time' 128 | attribute :weight, String, tag: 'weight' 129 | element :result, String, tag: 'result' 130 | # element override - Not implemented. Does not apply to Inspec execution 131 | has_many :ident, Ident, tag: 'ident' 132 | # NOTE: element metadata not implemented at this time 133 | has_many :message, MessageType, tag: 'message' 134 | has_many :instance, String, tag: 'instance' 135 | element :fix, Fix, tag: 'fix' 136 | element :check, Check, tag: 'check' 137 | end 138 | 139 | class ScoreType 140 | include HappyMapper 141 | 142 | def initialize(system, maximum, score) 143 | @system = system 144 | @maximum = maximum 145 | @score = score 146 | end 147 | 148 | attribute :system, String, tag: 'system' 149 | attribute :maximum, String, tag: 'maximum' # optional attribute 150 | content :score, String 151 | end 152 | 153 | class CPE2idrefType 154 | include HappyMapper 155 | attribute :idref, String, tag: 'idref' 156 | end 157 | 158 | class IdentityType 159 | include HappyMapper 160 | attribute :authenticated, Boolean, tag: 'authenticated' 161 | attribute :privileged, Boolean, tag: 'privileged' 162 | content :identity, String 163 | end 164 | 165 | class Fact 166 | include HappyMapper 167 | attribute :name, String, tag: 'name' 168 | attribute :type, String, tag: 'type' 169 | content :fact, String 170 | end 171 | 172 | class TargetFact 173 | include HappyMapper 174 | has_many :fact, Fact, tag: 'fact' 175 | end 176 | 177 | class TestResult 178 | include HappyMapper 179 | # NOTE: element benchmark not implemented at this time since this is same file 180 | # Note: element title not implemented due to no mapping from Chef Inspec 181 | element :remark, String, tag: 'remark' 182 | has_many :organization, String, tag: 'organization' 183 | element :identity, IdentityType, tag: 'identity' 184 | element :target, String, tag: 'target' 185 | has_many :target_address, String, tag: 'target-address' 186 | element :target_facts, TargetFact, tag: 'target-facts' 187 | element :platform, CPE2idrefType, tag: 'platform' 188 | # NOTE: element profile not implemented since Benchmark profile is also not implemented 189 | has_many :rule_result, RuleResultType, tag: 'rule-result' 190 | has_many :score, ScoreType, tag: 'score' # One minimum 191 | # Note: element signature not implemented due to no mapping from Chef Inspec 192 | attribute :id, String, tag: 'id' 193 | attribute :starttime, String, tag: 'start-time' 194 | attribute :endtime, String, tag: 'end-time' 195 | # NOTE: attribute test-system not implemented at this time due to unknown CPE value for Chef Inspec 196 | attribute :version, String, tag: 'version' 197 | end 198 | 199 | # Class Profile maps from the 'Profile' from Benchmark XML file using HappyMapper 200 | class Profile 201 | include HappyMapper 202 | tag 'Profile' 203 | attribute :id, String, tag: 'id' 204 | element :title, String, tag: 'title' 205 | element :description, String, tag: 'description' 206 | has_many :select, Select, tag: 'select' 207 | end 208 | 209 | # Class Rule maps from the 'Rule' from Benchmark XML file using HappyMapper 210 | class Rule 211 | include HappyMapper 212 | tag 'Rule' 213 | attribute :id, String, tag: 'id' 214 | attribute :severity, String, tag: 'severity' 215 | attribute :weight, String, tag: 'weight' 216 | element :version, String, tag: 'version' 217 | element :title, String, tag: 'title' 218 | element :description, String, tag: 'description' 219 | element :reference, ReferenceGroup, tag: 'reference' 220 | has_many :ident, Ident, tag: 'ident' 221 | element :fixtext, Fixtext, tag: 'fixtext' 222 | element :fix, Fix, tag: 'fix' 223 | element :check, Check, tag: 'check' 224 | end 225 | 226 | # Class Group maps from the 'Group' from Benchmark XML file using HappyMapper 227 | class Group 228 | include HappyMapper 229 | tag 'Group' 230 | attribute :id, String, tag: 'id' 231 | element :title, String, tag: 'title' 232 | element :description, String, tag: 'description' 233 | element :rule, Rule, tag: 'Rule' 234 | end 235 | 236 | # Class Benchmark maps from the 'Benchmark' from Benchmark XML file using HappyMapper 237 | class Benchmark 238 | include HappyMapper 239 | tag 'Benchmark' 240 | register_namespace 'dsig', 'http://www.w3.org/2000/09/xmldsig#' 241 | register_namespace 'xsi', 'http://www.w3.org/2001/XMLSchema-instance' 242 | register_namespace 'cpe', 'http://cpe.mitre.org/language/2.0' 243 | register_namespace 'xhtml', 'http://www.w3.org/1999/xhtml' 244 | register_namespace 'dc', 'http://purl.org/dc/elements/1.1/' 245 | attribute :id, String, tag: 'id' 246 | attribute :xmlns, String, tag: 'xmlns' 247 | element :status, Status, tag: 'status' 248 | element :title, String, tag: 'title' 249 | element :description, String, tag: 'description' 250 | element :notice, Notice, tag: 'notice' 251 | element :reference, ReferenceBenchmark, tag: 'reference' 252 | element :plaintext, Plaintext, tag: 'plain-text' 253 | element :version, String, tag: 'version' 254 | has_many :profile, Profile, tag: 'Profile' 255 | has_many :group, Group, tag: 'Group' 256 | element :testresult, TestResult, tag: 'TestResult' 257 | end 258 | end 259 | end 260 | -------------------------------------------------------------------------------- /lib/happy_mapper_tools/cci_attributes.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'happymapper' 4 | require 'nokogiri' 5 | 6 | module HappyMapperTools 7 | module CCIAttributes 8 | class Reference 9 | include HappyMapper 10 | tag 'reference' 11 | 12 | attribute :creator, String, tag: 'creator' 13 | attribute :title, String, tag: 'title' 14 | attribute :version, String, tag: 'version' 15 | attribute :location, String, tag: 'location' 16 | attribute :index, String, tag: 'index' 17 | end 18 | 19 | class CCI_Item 20 | include HappyMapper 21 | tag 'cci_item' 22 | 23 | attribute :id, String, tag: 'id' 24 | element :status, String, tag: 'status' 25 | element :publishdate, String, tag: 'publishdate' 26 | element :contributor, String, tag: 'contributor' 27 | element :definition, String, tag: 'definition' 28 | element :type, String, tag: 'type' 29 | has_many :references, Reference, xpath: 'xmlns:references' 30 | end 31 | 32 | class Metadata 33 | include HappyMapper 34 | tag 'metadata' 35 | 36 | element :version, String, tag: 'version' 37 | element :publishdate, String, tag: 'publishdate' 38 | end 39 | 40 | class CCI_List 41 | include HappyMapper 42 | tag 'cci_list' 43 | 44 | attribute :xsi, String, tag: 'xsi', namespace: 'xmlns' 45 | attribute :schemaLocation, String, tag: 'schemaLocation', namespace: 'xmlns' 46 | has_one :metadata, Metadata, tag: 'metadata' 47 | has_many :cci_items, CCI_Item, xpath: 'xmlns:cci_items' 48 | 49 | def fetch_nists(ccis) 50 | ccis = [ccis] unless ccis.is_a?(Array) 51 | 52 | # some of the XCCDF files were having CCE- tags show up which 53 | # we don't support, not sure if this is a typo on their part or 54 | # we need to see about supporting CCE tags but ... for now 55 | filtered_ccis = ccis.select { |f| /CCI-/.match(f) } 56 | filtered_ccis.map do |cci| 57 | cci_items.find { |item| item.id == cci }.references.max_by(&:version).index 58 | end 59 | end 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /lib/happy_mapper_tools/stig_attributes.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module HappyMapperTools 4 | module StigAttributes 5 | require 'happymapper' 6 | require 'nokogiri' 7 | require 'colorize' 8 | 9 | class ContentRef 10 | include HappyMapper 11 | tag 'check-content-ref' 12 | attribute :name, String, tag: 'name' 13 | attribute :href, String, tag: 'href' 14 | end 15 | 16 | class Check 17 | include HappyMapper 18 | tag 'check' 19 | 20 | element :content_ref, ContentRef, tag: 'check-content-ref' 21 | element :content, String, tag: 'check-content' 22 | end 23 | 24 | class Fix 25 | include HappyMapper 26 | tag 'fix' 27 | 28 | attribute :id, String, tag: 'id' 29 | end 30 | 31 | class DescriptionDetails 32 | include HappyMapper 33 | tag 'Details' 34 | 35 | element :vuln_discussion, String, tag: 'VulnDiscussion' 36 | element :false_positives, String, tag: 'FalsePositives' 37 | element :false_negatives, String, tag: 'FalseNegatives' 38 | element :documentable, Boolean, tag: 'Documentable' 39 | element :mitigations, String, tag: 'Mitigations' 40 | element :severity_override_guidance, String, tag: 'SeverityOverrideGuidance' 41 | element :security_override_guidance, String, tag: 'SecurityOverrideGuidance' 42 | element :potential_impacts, String, tag: 'PotentialImpacts' 43 | element :third_party_tools, String, tag: 'ThirdPartyTools' 44 | element :mitigation_controls, String, tag: 'MitigationControl' 45 | element :responsibility, String, tag: 'Responsibility' 46 | element :ia_controls, String, tag: 'IAControls' 47 | end 48 | 49 | class Description 50 | include HappyMapper 51 | tag 'description' 52 | 53 | content :details, DescriptionDetails 54 | 55 | detail_tags = %i(vuln_discussion false_positives false_negatives documentable 56 | mitigations severity_override_guidance potential_impacts 57 | third_party_tools mitigation_controls responsibility ia_controls 58 | security_override_guidance) 59 | 60 | detail_tags.each do |name| 61 | define_method name do 62 | details.send(name) 63 | end 64 | end 65 | end 66 | 67 | class ReferenceInfo 68 | include HappyMapper 69 | tag 'reference' 70 | 71 | attribute :href, String, tag: 'href' 72 | element :dc_publisher, String, tag: 'publisher', namespace: 'dc' 73 | element :dc_source, String, tag: 'source', namespace: 'dc' 74 | element :dc_title, String, tag: 'title', namespace: 'dc' 75 | element :dc_type, String, tag: 'type', namespace: 'dc' 76 | element :dc_subject, String, tag: 'subject', namespace: 'dc' 77 | element :dc_identifier, String, tag: 'identifier', namespace: 'dc' 78 | end 79 | 80 | class Ident 81 | include HappyMapper 82 | attr_accessor :legacy, :cci 83 | 84 | tag 'ident' 85 | attribute :system, String, tag: 'system' 86 | content :ident, String 87 | end 88 | 89 | class Rule 90 | include HappyMapper 91 | tag 'Rule' 92 | 93 | attribute :id, String, tag: 'id' 94 | attribute :severity, String, tag: 'severity' 95 | element :version, String, tag: 'version' 96 | element :title, String, tag: 'title' 97 | has_one :description, Description, tag: 'description' 98 | element :reference, ReferenceInfo, tag: 'reference' 99 | has_many :idents, Ident, tag: 'ident' 100 | element :fixtext, String, tag: 'fixtext' 101 | has_one :fix, Fix, tag: 'fix' 102 | has_one :check, Check, tag: 'check' 103 | end 104 | 105 | class Group 106 | include HappyMapper 107 | tag 'Group' 108 | 109 | attribute :id, String, tag: 'id' 110 | element :title, String, tag: 'title' 111 | element :description, String, tag: 'description' 112 | has_one :rule, Rule, tag: 'Rule' 113 | end 114 | 115 | class ReleaseDate 116 | include HappyMapper 117 | tag 'status' 118 | 119 | attribute :release_date, String, tag: 'date' 120 | end 121 | 122 | class Notice 123 | include HappyMapper 124 | tag 'notice' 125 | attribute :id, String, tag: 'id' 126 | attribute :xml_lang, String, namespace: 'xml', tag: 'lang' 127 | content :notice, String, tag: 'notice' 128 | end 129 | 130 | class Plaintext 131 | include HappyMapper 132 | tag 'plain-text' 133 | attribute :id, String, tag: 'id' 134 | content :plaintext, String 135 | end 136 | 137 | class Benchmark 138 | include HappyMapper 139 | tag 'Benchmark' 140 | 141 | has_one :release_date, ReleaseDate, tag: 'status' 142 | attribute :id, String, tag: 'id' 143 | element :status, String, tag: 'status' 144 | element :title, String, tag: 'title' 145 | element :description, String, tag: 'description' 146 | element :version, String, tag: 'version' 147 | element :notice, Notice, tag: 'notice' 148 | has_one :reference, ReferenceInfo, tag: 'reference' 149 | element :plaintext, Plaintext, tag: 'plain-text' 150 | has_many :group, Group, tag: 'Group' 151 | end 152 | 153 | class DescriptionDetailsType 154 | class << self 155 | def type 156 | DescriptionDetails 157 | end 158 | 159 | def apply(value) 160 | value = value.gsub('&', 'and') 161 | value = value.gsub('"<"', 'less than (converted less than)') 162 | DescriptionDetails.parse("
#{value}
") 163 | rescue Nokogiri::XML::SyntaxError => e 164 | if report_disallowed_tags(value) # if there was a bad tag 165 | exit(1) 166 | else 167 | report_error(value, e) 168 | end 169 | end 170 | 171 | def apply?(value, _convert_to_type) 172 | value.is_a?(String) 173 | end 174 | 175 | private 176 | 177 | def report_error(value, error) 178 | puts error.to_s.colorize(:red) 179 | column = error.column - '
'.length - 2 180 | puts "Error around #{value[column-10..column+10].colorize(:light_yellow)}" 181 | exit(1) 182 | end 183 | 184 | def report_disallowed_tags(value) 185 | allowed_tags = %w{VulnDiscussion FalsePositives FalseNegatives Documentable 186 | Mitigations SeverityOverrideGuidance PotentialImpacts 187 | PotentialImpacts ThirdPartyTools MitigationControl 188 | Responsibility IAControl IAControls SecurityOverrideGuidance} 189 | 190 | tags_found = value.scan(%r{(?<=<)([^\/]*?)((?= \/>)|(?=>))}).to_a 191 | 192 | tags_found = tags_found.uniq.flatten.reject!(&:empty?) 193 | offending_tags = tags_found - allowed_tags 194 | 195 | unless offending_tags.count.zero? 196 | puts "\n\nThe non-standard tag(s): #{offending_tags.to_s.colorize(:red)}" \ 197 | ' were found in: ' + "\n\n#{value}" 198 | puts "\n\nPlease:\n " 199 | option_one = '(1) ' + '(best)'.colorize(:green) + ' Use the ' + 200 | '`-r --replace-tags array` '.colorize(:light_yellow) + 201 | '(case sensitive) option to replace the offending tags ' \ 202 | 'during processing of the XCCDF ' \ 203 | 'file to use the ' + 204 | "`$#{offending_tags[0]}` ".colorize(:light_green) + 205 | 'syntax in your InSpec profile.' 206 | option_two = '(2) Update your XCCDF file to *not use* non-standard XCCDF ' \ 207 | 'elements within ' + 208 | '`<`,`>`, `<` '.colorize(:red) + 209 | 'or '.colorize(:default) + 210 | '`>` '.colorize(:red) + 211 | 'as "placeholders", and use something that doesn\'t confuse ' \ 212 | 'the XML parser, such as : ' + 213 | "`$#{offending_tags[0]}`".colorize(:light_green) 214 | puts option_one 215 | puts "\n" 216 | puts option_two 217 | return true 218 | end 219 | false 220 | end 221 | end 222 | HappyMapper::SupportedTypes.register DescriptionDetailsType 223 | end 224 | end 225 | end 226 | -------------------------------------------------------------------------------- /lib/happy_mapper_tools/stig_checklist.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'happymapper' 4 | require 'nokogiri' 5 | 6 | module HappyMapperTools 7 | module StigChecklist 8 | # see: https://github.com/dam5s/happymapper 9 | # Class Asset maps from the 'Asset' from Checklist XML file using HappyMapper 10 | class Asset 11 | include HappyMapper 12 | tag 'ASSET' 13 | element :role, String, tag: 'ROLE' 14 | element :type, String, tag: 'ASSET_TYPE' 15 | element :host_name, String, tag: 'HOST_NAME' 16 | element :host_ip, String, tag: 'HOST_IP' 17 | element :host_mac, String, tag: 'HOST_MAC' 18 | element :host_guid, String, tag: 'HOST_GUID' 19 | element :host_fqdn, String, tag: 'HOST_FQDN' 20 | element :tech_area, String, tag: 'TECH_AREA' 21 | element :target_key, String, tag: 'TARGET_KEY' 22 | element :web_or_database, String, tag: 'WEB_OR_DATABASE' 23 | element :web_db_site, String, tag: 'WEB_DB_SITE' 24 | element :web_db_instance, String, tag: 'WEB_DB_INSTANCE' 25 | end 26 | 27 | # Class Asset maps from the 'SI_DATA' from Checklist XML file using HappyMapper 28 | class SiData 29 | include HappyMapper 30 | 31 | def initialize(name, data) 32 | @name = name 33 | @data = data 34 | end 35 | 36 | tag 'SI_DATA' 37 | element :name, String, tag: 'SID_NAME' 38 | element :data, String, tag: 'SID_DATA' 39 | end 40 | 41 | # Class Asset maps from the 'STIG_INFO' from Checklist XML file using HappyMapper 42 | class StigInfo 43 | include HappyMapper 44 | 45 | def initialize(si_data) 46 | self.si_data = si_data 47 | end 48 | 49 | tag 'STIG_INFO' 50 | has_many :si_data, SiData, tag: 'SI_DATA' 51 | end 52 | 53 | # Class Asset maps from the 'STIG_DATA' from Checklist XML file using HappyMapper 54 | class StigData 55 | include HappyMapper 56 | 57 | def initialize(attrib = nil, data = nil) 58 | self.attrib = attrib 59 | self.data = data 60 | end 61 | 62 | tag 'STIG_DATA' 63 | has_one :attrib, String, tag: 'VULN_ATTRIBUTE' 64 | has_one :data, String, tag: 'ATTRIBUTE_DATA' 65 | end 66 | 67 | # Class Asset maps from the 'VULN' from Checklist XML file using HappyMapper 68 | class Vuln 69 | include HappyMapper 70 | tag 'VULN' 71 | has_many :stig_data, StigData, tag: 'STIG_DATA' 72 | has_one :status, String, tag: 'STATUS' 73 | has_one :finding_details, String, tag: 'FINDING_DETAILS' 74 | has_one :comments, String, tag: 'COMMENTS' 75 | has_one :severity_override, String, tag: 'SEVERITY_OVERRIDE' 76 | has_one :severity_justification, String, tag: 'SEVERITY_JUSTIFICATION' 77 | end 78 | 79 | # Class Asset maps from the 'iSTIG' from Checklist XML file using HappyMapper 80 | class IStig 81 | include HappyMapper 82 | 83 | def initialize(stig_info, vulns) 84 | self.stig_info = stig_info 85 | self.vuln = vulns 86 | end 87 | tag 'iSTIG' 88 | has_one :stig_info, StigInfo, tag: 'STIG_INFO' 89 | has_many :vuln, Vuln, tag: 'VULN' 90 | end 91 | 92 | # Class Asset maps from the 'STIGS' from Checklist XML file using HappyMapper 93 | class Stigs 94 | include HappyMapper 95 | 96 | def initialize(istig) 97 | self.istig = istig 98 | end 99 | tag 'STIGS' 100 | has_one :istig, IStig, tag: 'iSTIG' 101 | end 102 | 103 | class Checklist 104 | include HappyMapper 105 | tag 'CHECKLIST' 106 | has_one :asset, Asset, tag: 'ASSET' 107 | has_one :stig, Stigs, tag: 'STIGS' 108 | 109 | def where(attrib, data) 110 | stig.istig.vuln.each do |vuln| 111 | if vuln.stig_data.any? { |element| element.attrib == attrib && element.data == data } 112 | # TODO: Handle multiple objects that match the condition 113 | return vuln 114 | end 115 | end 116 | end 117 | end 118 | end 119 | end 120 | -------------------------------------------------------------------------------- /lib/inspec_tools.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift(File.expand_path(__dir__)) 2 | require 'inspec_tools/version' 3 | require 'rubygems' 4 | 5 | module InspecTools 6 | autoload :Help, 'inspec_tools/help' 7 | autoload :Command, 'inspec_tools/command' 8 | autoload :CLI, 'inspec_tools/cli' 9 | autoload :XCCDF, 'inspec_tools/xccdf' 10 | autoload :PDF, 'inspec_tools/pdf' 11 | autoload :CSV, 'inspec_tools/csv' 12 | autoload :CKL, 'inspec_tools/ckl' 13 | autoload :Inspec, 'inspec_tools/inspec' 14 | autoload :Summary, 'inspec_tools/summary' 15 | autoload :Threshold, 'inspec_tools/threshold' 16 | autoload :XLSXTool, 'inspec_tools/xlsx_tool' 17 | end 18 | -------------------------------------------------------------------------------- /lib/inspec_tools/cli.rb: -------------------------------------------------------------------------------- 1 | require 'yaml' 2 | require 'json' 3 | 4 | require 'inspec-objects' 5 | require 'inspec' 6 | require_relative './plugin_cli' 7 | 8 | # This tells the ruby cli app to use the same argument parsing as the plugin 9 | module InspecTools 10 | CLI = InspecPlugins::InspecToolsPlugin::CliCommand 11 | end 12 | 13 | #=====================================================================# 14 | # Pre-Flight Code 15 | #=====================================================================# 16 | help_commands = ['-h', '--help', 'help'] 17 | log_commands = ['-l', '--log-directory'] 18 | version_commands = ['-v', '--version', 'version'] 19 | 20 | #---------------------------------------------------------------------# 21 | # Adjustments for non-required version commands 22 | #---------------------------------------------------------------------# 23 | unless (version_commands & ARGV).empty? 24 | puts InspecTools::VERSION 25 | exit 0 26 | end 27 | 28 | #---------------------------------------------------------------------# 29 | # Adjustments for non-required log-directory 30 | #---------------------------------------------------------------------# 31 | ARGV.push("--log-directory=#{Dir.pwd}/logs") if (log_commands & ARGV).empty? && (help_commands & ARGV).empty? 32 | -------------------------------------------------------------------------------- /lib/inspec_tools/csv.rb: -------------------------------------------------------------------------------- 1 | require 'csv' 2 | require 'yaml' 3 | require 'digest' 4 | 5 | require_relative '../utilities/inspec_util' 6 | require_relative '../utilities/cci_xml' 7 | require_relative '../utilities/mapping_validator' 8 | 9 | module InspecTools 10 | # Methods for converting from CSV to various formats 11 | class CSVTool 12 | def initialize(csv, mapping, name, verbose = false) 13 | @name = name 14 | @csv = csv 15 | @mapping = Utils::MappingValidator.validate(mapping) 16 | @verbose = verbose 17 | @csv.shift if @mapping['skip_csv_header'] 18 | end 19 | 20 | def to_inspec(control_name_prefix: nil) 21 | @controls = [] 22 | @profile = {} 23 | @cci_xml = Utils::CciXml.get_cci_list('U_CCI_List.xml') 24 | insert_metadata 25 | parse_controls(control_name_prefix) 26 | @profile['controls'] = @controls 27 | @profile['sha256'] = Digest::SHA256.hexdigest(@profile.to_s) 28 | @profile 29 | end 30 | 31 | private 32 | 33 | def insert_metadata 34 | @profile['name'] = @name 35 | @profile['title'] = 'InSpec Profile' 36 | @profile['maintainer'] = 'The Authors' 37 | @profile['copyright'] = 'The Authors' 38 | @profile['copyright_email'] = 'you@example.com' 39 | @profile['license'] = 'Apache-2.0' 40 | @profile['summary'] = 'An InSpec Compliance Profile' 41 | @profile['version'] = '0.1.0' 42 | @profile['supports'] = [] 43 | @profile['attributes'] = [] 44 | @profile['generator'] = { 45 | name: 'inspec_tools', 46 | version: VERSION 47 | } 48 | end 49 | 50 | def get_nist_reference(cci_number) 51 | item_node = @cci_xml.xpath("//cci_list/cci_items/cci_item[@id='#{cci_number}']")[0] unless @cci_xml.nil? 52 | return nil if item_node.nil? 53 | 54 | [] << item_node.xpath('./references/reference[not(@version <= preceding-sibling::reference/@version) and not(@version <=following-sibling::reference/@version)]/@index').text 55 | end 56 | 57 | def get_cci_number(cell) 58 | # Return nil if a mapping to the CCI was not provided or if there is not content in the CSV cell. 59 | return nil if cell.nil? || @mapping['control.tags']['cci'].nil? 60 | 61 | # If the content has been exported from STIG Viewer, the cell will have extra information 62 | cell.split("\n").first 63 | end 64 | 65 | def parse_controls(prefix) 66 | @csv.each do |row| 67 | control = {} 68 | control['id'] = generate_control_id(prefix, row[@mapping['control.id']]) unless @mapping['control.id'].nil? || row[@mapping['control.id']].nil? 69 | control['title'] = row[@mapping['control.title']] unless @mapping['control.title'].nil? || row[@mapping['control.title']].nil? 70 | control['desc'] = row[@mapping['control.desc']] unless @mapping['control.desc'].nil? || row[@mapping['control.desc']].nil? 71 | control['tags'] = {} 72 | cci_number = get_cci_number(row[@mapping['control.tags']['cci']]) 73 | nist = get_nist_reference(cci_number) unless cci_number.nil? 74 | control['tags']['nist'] = nist unless nist.nil? || nist.include?(nil) 75 | @mapping['control.tags'].each do |tag| 76 | if tag.first == 'cci' 77 | if cci_number.is_a? Array 78 | control['tags'][tag.first] = cci_number 79 | else 80 | control['tags'][tag.first] = [cci_number] 81 | end 82 | next 83 | end 84 | control['tags'][tag.first] = row[tag.last] unless row[tag.last].nil? 85 | end 86 | unless @mapping['control.tags']['severity'].nil? || row[@mapping['control.tags']['severity']].nil? 87 | control['impact'] = Utils::InspecUtil.get_impact(row[@mapping['control.tags']['severity']]) 88 | control['tags']['severity'] = Utils::InspecUtil.get_impact_string(control['impact']) 89 | end 90 | @controls << control 91 | end 92 | end 93 | 94 | def generate_control_id(prefix, id) 95 | return id if prefix.nil? 96 | 97 | "#{prefix}-#{id}" 98 | end 99 | end 100 | end 101 | -------------------------------------------------------------------------------- /lib/inspec_tools/generate_map.rb: -------------------------------------------------------------------------------- 1 | module InspecTools 2 | class GenerateMap 3 | attr_accessor :text 4 | 5 | def initialize(text = nil) 6 | @text = text.nil? ? default_text : text 7 | end 8 | 9 | def generate_example(file) 10 | File.write(file, @text) 11 | end 12 | 13 | private 14 | 15 | def default_text 16 | <<~YML 17 | # Setting csv_header to true will skip the csv file header 18 | skip_csv_header: true 19 | width : 80 20 | 21 | 22 | control.id: 0 23 | control.title: 15 24 | control.desc: 16 25 | control.tags: 26 | severity: 1 27 | rid: 8 28 | stig_id: 3 29 | cci: 2 30 | check: 12 31 | fix: 10 32 | YML 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/inspec_tools/help.rb: -------------------------------------------------------------------------------- 1 | module InspecTools 2 | module Help 3 | class << self 4 | def text(namespaced_command) 5 | path = namespaced_command.to_s.tr(':', '/') 6 | path = File.expand_path("../help/#{path}.md", __FILE__) 7 | IO.read(path) if File.exist?(path) 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/inspec_tools/help/compliance.md: -------------------------------------------------------------------------------- 1 | compliance parses an inspec results json to check if the compliance level meets a specified threshold 2 | 3 | Examples: 4 | 5 | inspec_tools compliance -j examples/sample_json/rhel-simp.json -i '{compliance.min: 80, failed.critical.max: 0, failed.high.max: 0}' 6 | 7 | inspec_tools compliance -j examples/sample_json/rhel-simp.json -f examples/sample_yaml/threshold.yaml -------------------------------------------------------------------------------- /lib/inspec_tools/help/csv2inspec.md: -------------------------------------------------------------------------------- 1 | csv2inspec translates CSV to Inspec controls using a mapping file 2 | 3 | Examples: 4 | 5 | inspec_tools csv2inspec -m examples/csv2inspec/mapping.yml -c examples/csv2inspec/stig.csv 6 | -------------------------------------------------------------------------------- /lib/inspec_tools/help/inspec2ckl.md: -------------------------------------------------------------------------------- 1 | inspec2ckl translates an inspec json file to a Checklist file 2 | 3 | Examples: 4 | 5 | inspec_tools inspec2ckl -j examples/sample_json/good_nginxresults.json -o output.ckl 6 | -------------------------------------------------------------------------------- /lib/inspec_tools/help/inspec2csv.md: -------------------------------------------------------------------------------- 1 | inspec2csv translates Inspec controls to CSV 2 | 3 | Examples: 4 | 5 | inspec_tools inspec2csv -j examples/sample_json/good_nginxresults.json -o output.csv 6 | -------------------------------------------------------------------------------- /lib/inspec_tools/help/inspec2xccdf.md: -------------------------------------------------------------------------------- 1 | inspec2xccdf translates an inspec profile and attributes files to an xccdf file 2 | 3 | Examples: 4 | 5 | inspec_tools inspec2xccdf -j examples/sample_json/good_nginxresults.json -a lib/data/attributes.yml -o output.xccdf 6 | -------------------------------------------------------------------------------- /lib/inspec_tools/help/pdf2inspec.md: -------------------------------------------------------------------------------- 1 | pdf2inspec translates a PDF Security Control Speficication to Inspec Security Profile 2 | 3 | Examples: 4 | 5 | inspec_tools pdf2inspec -p examples/CIS_Ubuntu_Linux_16.04_LTS_Benchmark_v1.0.0.pdf 6 | inspec_tools pdf2inspec -p examples/CIS_Ubuntu_Linux_16.04_LTS_Benchmark_v1.0.0.pdf -o CIS_Ubuntu_Linux_16.04_LTS_Benchmark_v1.0.0 7 | -------------------------------------------------------------------------------- /lib/inspec_tools/help/summary.md: -------------------------------------------------------------------------------- 1 | summary parses an inspec results json to create a summary json 2 | 3 | Examples: 4 | 5 | inspec_tools summary -j examples/sample_json/rhel-simp.json -f -o summary.json 6 | -------------------------------------------------------------------------------- /lib/inspec_tools/help/xccdf2inspec.md: -------------------------------------------------------------------------------- 1 | xccdf2inspec translates an xccdf file to an inspec profile 2 | 3 | Examples: 4 | 5 | inspec_tools xccdf2inspec -x examples/xccdf2inspec/xccdf.xml -o myprofile 6 | -------------------------------------------------------------------------------- /lib/inspec_tools/pdf.rb: -------------------------------------------------------------------------------- 1 | require 'digest' 2 | 3 | require_relative '../utilities/inspec_util' 4 | require_relative '../utilities/extract_pdf_text' 5 | require_relative '../utilities/parser' 6 | require_relative '../utilities/text_cleaner' 7 | require_relative '../utilities/cis_to_nist' 8 | 9 | module InspecTools 10 | class PDF 11 | def initialize(pdf, profile_name, debug = false) 12 | raise ArgumentError if pdf.nil? 13 | 14 | @pdf = pdf 15 | @name = profile_name 16 | @debug = debug 17 | end 18 | 19 | def to_inspec 20 | @controls = [] 21 | @csv_handle = nil 22 | @cci_xml = nil 23 | @nist_mapping = Utils::CisToNist.get_mapping('cis_to_nist_critical_controls') 24 | @pdf_text = '' 25 | @clean_text = '' 26 | @transformed_data = '' 27 | @profile = {} 28 | read_pdf 29 | @title ||= extract_title 30 | clean_pdf_text 31 | transform_data 32 | insert_json_metadata 33 | @profile['controls'] = parse_controls 34 | @profile['sha256'] = Digest::SHA256.hexdigest @profile.to_s 35 | @profile 36 | end 37 | 38 | def to_csv 39 | # TODO: to_csv 40 | end 41 | 42 | def to_xccdf 43 | # TODO: to_xccdf 44 | end 45 | 46 | def to_ckl 47 | # TODO: to_ckl 48 | end 49 | 50 | private 51 | 52 | # converts passed in data into InSpec format 53 | def parse_controls 54 | controls = [] 55 | @transformed_data.each do |contr| 56 | nist = find_nist(contr[:cis]) unless contr[:cis] == 'No CIS Control' 57 | control = {} 58 | control['id'] = "M-#{contr[:title].split[0]}" 59 | control['title'] = contr[:title] 60 | control['desc'] = contr[:descr] 61 | control['impact'] = Utils::InspecUtil.get_impact('medium') 62 | control['tags'] = {} 63 | control['tags']['severity'] = Utils::InspecUtil.get_impact_string(control['impact']) 64 | control['tags']['ref'] = contr[:ref] unless contr[:ref].nil? 65 | control['tags']['applicability'] = contr[:applicability] unless contr[:applicability].nil? 66 | control['tags']['cis_id'] = contr[:title].split[0] unless contr[:title].nil? 67 | control['tags']['cis_control'] = [contr[:cis], @nist_mapping[0][:cis_ver]] unless contr[:cis].nil? # tag cis_control: [5, 6.1] ##6.1 is the version 68 | control['tags']['cis_level'] = contr[:level] unless contr[:level].nil? 69 | control['tags']['nist'] = nist unless nist.nil? # tag nist: [AC-3, 4] ##4 is the version 70 | control['tags']['check'] = contr[:check] unless contr[:check].nil? 71 | control['tags']['fix'] = contr[:fix] unless contr[:fix].nil? 72 | control['tags']['Default Value'] = contr[:default] unless contr[:default].nil? 73 | controls << control 74 | end 75 | controls 76 | end 77 | 78 | def insert_json_metadata 79 | @profile['name'] = @name 80 | @profile['title'] = @title 81 | @profile['maintainer'] = 'The Authors' 82 | @profile['copyright'] = 'The Authors' 83 | @profile['copyright_email'] = 'you@example.com' 84 | @profile['license'] = 'Apache-2.0' 85 | @profile['summary'] = 'An InSpec Compliance Profile' 86 | @profile['version'] = '0.1.0' 87 | @profile['supports'] = [] 88 | @profile['attributes'] = [] 89 | @profile['generator'] = { 90 | name: 'inspec_tools', 91 | version: VERSION 92 | } 93 | end 94 | 95 | def extract_title 96 | @pdf_text.match(/([^\n]*)\n/).captures[0] 97 | end 98 | 99 | def read_pdf 100 | @pdf_text = Util::ExtractPdfText.new(@pdf).extracted_text 101 | write_pdf_text if @debug 102 | end 103 | 104 | def clean_pdf_text 105 | @clean_text = Util::TextCleaner.new.clean_data(@pdf_text) 106 | write_clean_text if @debug 107 | end 108 | 109 | def transform_data 110 | @transformed_data = Util::PrepareData.new(@clean_text).transformed_data 111 | end 112 | 113 | def write_pdf_text 114 | File.write('pdf_text', @pdf_text) 115 | end 116 | 117 | def write_clean_text 118 | File.write('debug_text', @clean_text) 119 | end 120 | end 121 | end 122 | -------------------------------------------------------------------------------- /lib/inspec_tools/plugin.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module InspecToolsPlugin 4 | class Plugin < Inspec.plugin(2) 5 | # Metadata 6 | # Must match entry in plugins.json 7 | plugin_name :'inspec-tools_plugin' 8 | 9 | # Activation hooks (CliCommand as an example) 10 | cli_command :tools do 11 | require_relative 'plugin_cli' 12 | InspecPlugins::InspecToolsPlugin::CliCommand 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/inspec_tools/plugin_cli.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | require 'roo' 3 | require_relative '../utilities/inspec_util' 4 | require_relative '../utilities/csv_util' 5 | 6 | module InspecTools 7 | autoload :Help, 'inspec_tools/help' 8 | autoload :Command, 'inspec_tools/command' 9 | autoload :XCCDF, 'inspec_tools/xccdf' 10 | autoload :PDF, 'inspec_tools/pdf' 11 | autoload :CSVTool, 'inspec_tools/csv' 12 | autoload :CKL, 'inspec_tools/ckl' 13 | autoload :Inspec, 'inspec_tools/inspec' 14 | autoload :Summary, 'inspec_tools/summary' 15 | autoload :XLSXTool, 'inspec_tools/xlsx_tool' 16 | autoload :GenerateMap, 'inspec_tools/generate_map' 17 | end 18 | 19 | module InspecPlugins 20 | module InspecToolsPlugin 21 | class CliCommand < Inspec.plugin(2, :cli_command) 22 | POSSIBLE_LOG_LEVELS = %w{debug info warn error fatal}.freeze 23 | 24 | class_option :log_directory, type: :string, aliases: :l, desc: 'Provide log location' 25 | class_option :log_level, type: :string, desc: "Set the logging level: #{POSSIBLE_LOG_LEVELS}" 26 | 27 | subcommand_desc 'tools [COMMAND]', 'Runs inspec_tools commands through Inspec' 28 | 29 | desc 'xccdf2inspec', 'xccdf2inspec translates an xccdf file to an inspec profile' 30 | long_desc InspecTools::Help.text(:xccdf2inspec) 31 | option :xccdf, required: true, aliases: '-x' 32 | option :attributes, required: false, aliases: '-a' 33 | option :output, required: false, aliases: '-o', default: 'profile' 34 | option :format, required: false, aliases: '-f', enum: %w{ruby hash}, default: 'ruby' 35 | option :separate_files, required: false, type: :boolean, default: true, aliases: '-s' 36 | option :replace_tags, type: :array, required: false, aliases: '-r' 37 | option :metadata, required: false, aliases: '-m' 38 | option :control_id, required: false, enum: %w{ruleID vulnID}, aliases: '-c', default: 'vulnID' 39 | def xccdf2inspec 40 | xccdf = InspecTools::XCCDF.new(File.read(options[:xccdf]), options[:control_id] == 'vulnID', options[:replace_tags]) 41 | profile = xccdf.to_inspec 42 | 43 | if !options[:metadata].nil? 44 | xccdf.inject_metadata(File.read(options[:metadata])) 45 | end 46 | 47 | Utils::InspecUtil.unpack_inspec_json(options[:output], profile, options[:separate_files], options[:format]) 48 | if !options[:attributes].nil? 49 | attributes = xccdf.to_attributes 50 | File.write(options[:attributes], YAML.dump(attributes)) 51 | end 52 | end 53 | 54 | desc 'inspec2xccdf', 'inspec2xccdf translates an inspec profile and attributes files to an xccdf file' 55 | long_desc InspecTools::Help.text(:inspec2xccdf) 56 | option :inspec_json, required: true, aliases: '-j', 57 | desc: 'path to InSpec JSON file created' 58 | option :attributes, required: true, aliases: '-a', 59 | desc: 'path to yml file that provides the required attributes for the XCCDF document. These attributes are parts of XCCDF document which do not fit into the InSpec schema.' 60 | option :output, required: true, aliases: '-o', 61 | desc: 'name or path to create the XCCDF and title to give the XCCDF' 62 | option :metadata, required: false, type: :string, aliases: '-m', 63 | desc: 'path to JSON file with additional host metadata for the XCCDF file' 64 | def inspec2xccdf 65 | io = File.open(options[:inspec_json], 'rb') 66 | io.set_encoding_by_bom 67 | metadata = options[:metadata] ? JSON.parse(File.read(options[:metadata])) : {} 68 | inspec_tool = InspecTools::Inspec.new(io.read, metadata) 69 | attr_hsh = YAML.load_file(options[:attributes]) 70 | xccdf = inspec_tool.to_xccdf(attr_hsh) 71 | File.write(options[:output], xccdf) 72 | end 73 | 74 | desc 'csv2inspec', 'csv2inspec translates CSV to Inspec controls using a mapping file' 75 | long_desc InspecTools::Help.text(:csv2inspec) 76 | option :csv, required: true, aliases: '-c' 77 | option :mapping, required: true, aliases: '-m' 78 | option :verbose, required: false, type: :boolean, aliases: '-V' 79 | option :output, required: false, aliases: '-o', default: 'profile' 80 | option :format, required: false, aliases: '-f', enum: %w{ruby hash}, default: 'ruby' 81 | option :separate_files, required: false, type: :boolean, default: true, aliases: '-s' 82 | option :control_name_prefix, required: false, type: :string, aliases: '-p' 83 | def csv2inspec 84 | csv = CSV.read(options[:csv], encoding: 'ISO8859-1') 85 | mapping = YAML.load_file(options[:mapping]) 86 | profile = InspecTools::CSVTool.new(csv, mapping, options[:csv].split('/')[-1].split('.')[0], options[:verbose]).to_inspec(control_name_prefix: options[:control_name_prefix]) 87 | Utils::InspecUtil.unpack_inspec_json(options[:output], profile, options[:separate_files], options[:format]) 88 | end 89 | 90 | desc 'xlsx2inspec', 'xlsx2inspec translates CIS XLSX to Inspec controls using a mapping file' 91 | long_desc InspecTools::Help.text(:xlsx2inspec) 92 | option :xlsx, required: true, aliases: '-x' 93 | option :mapping, required: true, aliases: '-m' 94 | option :control_name_prefix, required: true, aliases: '-p' 95 | option :verbose, required: false, type: :boolean, aliases: '-V' 96 | option :output, required: false, aliases: '-o', default: 'profile' 97 | option :format, required: false, aliases: '-f', enum: %w{ruby hash}, default: 'ruby' 98 | option :separate_files, required: false, type: :boolean, default: true, aliases: '-s' 99 | def xlsx2inspec 100 | xlsx = Roo::Spreadsheet.open(options[:xlsx]) 101 | mapping = YAML.load_file(options[:mapping]) 102 | profile = InspecTools::XLSXTool.new(xlsx, mapping, options[:xlsx].split('/')[-1].split('.')[0], options[:verbose]).to_inspec(options[:control_name_prefix]) 103 | Utils::InspecUtil.unpack_inspec_json(options[:output], profile, options[:separate_files], options[:format]) 104 | end 105 | 106 | desc 'inspec2csv', 'inspec2csv translates Inspec controls to CSV' 107 | long_desc InspecTools::Help.text(:inspec2csv) 108 | option :inspec_json, required: true, aliases: '-j' 109 | option :output, required: true, aliases: '-o' 110 | option :verbose, required: false, type: :boolean, aliases: '-V' 111 | def inspec2csv 112 | csv = InspecTools::Inspec.new(File.read(options[:inspec_json])).to_csv 113 | Utils::CSVUtil.unpack_csv(csv, options[:output]) 114 | end 115 | 116 | desc 'inspec2ckl', 'inspec2ckl translates an Inspec results JSON file to a Checklist file' 117 | long_desc InspecTools::Help.text(:inspec2ckl) 118 | option :inspec_json, required: true, aliases: '-j', 119 | desc: 'path to InSpec results JSON file' 120 | option :output, required: true, aliases: '-o', 121 | desc: 'path to output checklist file' 122 | option :metadata, required: false, aliases: '-m', 123 | desc: 'path to JSON file with additional metadata for the Checklist file' 124 | option :verbose, type: :boolean, aliases: '-V' 125 | def inspec2ckl 126 | inspec_tools = 127 | if options[:metadata].nil? 128 | InspecTools::Inspec.new(File.read(options[:inspec_json])) 129 | else 130 | InspecTools::Inspec.new(File.read(options[:inspec_json]), JSON.parse(File.read(options[:metadata]))) 131 | end 132 | File.write(options[:output], inspec_tools.to_ckl) 133 | end 134 | 135 | desc 'pdf2inspec', 'pdf2inspec translates a PDF Security Control Speficication to Inspec Security Profile' 136 | long_desc InspecTools::Help.text(:pdf2inspec) 137 | option :pdf, required: true, aliases: '-p' 138 | option :output, required: false, aliases: '-o', default: 'profile' 139 | option :debug, required: false, aliases: '-d', type: :boolean, default: false 140 | option :format, required: false, aliases: '-f', enum: %w{ruby hash}, default: 'ruby' 141 | option :separate_files, required: false, type: :boolean, default: true, aliases: '-s' 142 | def pdf2inspec 143 | pdf = File.open(options[:pdf]) 144 | profile = InspecTools::PDF.new(pdf, options[:output], options[:debug]).to_inspec 145 | Utils::InspecUtil.unpack_inspec_json(options[:output], profile, options[:separate_files], options[:format]) 146 | end 147 | 148 | desc 'generate_map', 'Generates mapping template from CSV to Inspec Controls' 149 | def generate_map 150 | generator = InspecTools::GenerateMap.new 151 | generator.generate_example('mapping.yml') 152 | end 153 | 154 | desc 'generate_ckl_metadata', 'Generate metadata file that can be passed to inspec2ckl' 155 | def generate_ckl_metadata 156 | metadata = {} 157 | metadata['benchmark'] = {} 158 | 159 | print('STIG ID: ') 160 | metadata['stigid'] = $stdin.gets.chomp 161 | print('Role: ') 162 | metadata['role'] = $stdin.gets.chomp 163 | print('Type: ') 164 | metadata['type'] = $stdin.gets.chomp 165 | print('Hostname: ') 166 | metadata['hostname'] = $stdin.gets.chomp 167 | print('IP Address: ') 168 | metadata['ip'] = $stdin.gets.chomp 169 | print('MAC Address: ') 170 | metadata['mac'] = $stdin.gets.chomp 171 | print('FQDN: ') 172 | metadata['fqdn'] = $stdin.gets.chomp 173 | print('Tech Area: ') 174 | metadata['tech_area'] = $stdin.gets.chomp 175 | print('Target Key: ') 176 | metadata['target_key'] = $stdin.gets.chomp 177 | print('Web or Database: ') 178 | metadata['web_or_database'] = $stdin.gets.chomp 179 | print('Web DB Site: ') 180 | metadata['web_db_site'] = $stdin.gets.chomp 181 | print('Web DB Instance: ') 182 | metadata['web_db_instance'] = $stdin.gets.chomp 183 | print('Benchmark title: ') 184 | metadata['benchmark']['title'] = $stdin.gets.chomp 185 | print('Benchmark version: ') 186 | metadata['benchmark']['version'] = $stdin.gets.chomp 187 | print('Benchmark revision/release information: ') 188 | metadata['benchmark']['plaintext'] = $stdin.gets.chomp 189 | 190 | recursive_compact = 191 | proc do |_k, value| 192 | value.delete_if(&recursive_compact) if value.is_a?(Hash) 193 | 194 | value.empty? 195 | end 196 | 197 | metadata.delete_if(&recursive_compact) 198 | 199 | File.open('metadata.json', 'w') do |f| 200 | f.write(metadata.to_json) 201 | end 202 | end 203 | 204 | desc 'generate_inspec_metadata', 'Generate mapping file that can be passed to xccdf2inspec' 205 | def generate_inspec_metadata 206 | metadata = {} 207 | 208 | print('Maintainer: ') 209 | metadata['maintainer'] = $stdin.gets.chomp 210 | print('Copyright: ') 211 | metadata['copyright'] = $stdin.gets.chomp 212 | print('Copyright Email: ') 213 | metadata['copyright_email'] = $stdin.gets.chomp 214 | print('License: ') 215 | metadata['license'] = $stdin.gets.chomp 216 | print('Version: ') 217 | metadata['version'] = $stdin.gets.chomp 218 | 219 | metadata.delete_if { |_key, value| value.empty? } 220 | File.open('metadata.json', 'w') do |f| 221 | f.write(metadata.to_json) 222 | end 223 | end 224 | 225 | desc 'summary', 'summary parses an inspec results json to create a summary json' 226 | long_desc InspecTools::Help.text(:summary) 227 | option :inspec_json, required: true, aliases: '-j' 228 | option :json_full, type: :boolean, required: false, aliases: '-f' 229 | option :json_counts, type: :boolean, required: false, aliases: '-k' 230 | option :threshold_file, required: false, aliases: '-t' 231 | option :threshold_inline, required: false, aliases: '-i' 232 | 233 | def summary 234 | summary = InspecTools::Summary.new(options: options) 235 | summary.output_summary 236 | end 237 | 238 | desc 'compliance', 'compliance parses an inspec results json to check if the compliance level meets a specified threshold' 239 | long_desc InspecTools::Help.text(:compliance) 240 | option :inspec_json, required: true, aliases: '-j' 241 | option :threshold_file, required: false, aliases: '-f' 242 | option :threshold_inline, required: false, aliases: '-i' 243 | 244 | def compliance 245 | compliance = InspecTools::Summary.new(options: options) 246 | compliance.results_meet_threshold? ? exit(0) : exit(1) 247 | end 248 | end 249 | end 250 | end 251 | 252 | #=====================================================================# 253 | # Pre-Flight Code 254 | #=====================================================================# 255 | help_commands = ['-h', '--help', 'help'] 256 | log_commands = ['-l', '--log-directory'] 257 | version_commands = ['-v', '--version', 'version'] 258 | 259 | #---------------------------------------------------------------------# 260 | # Adjustments for non-required version commands 261 | #---------------------------------------------------------------------# 262 | unless (version_commands & ARGV).empty? 263 | puts InspecTools::VERSION 264 | exit 0 265 | end 266 | 267 | #---------------------------------------------------------------------# 268 | # Adjustments for non-required log-directory 269 | #---------------------------------------------------------------------# 270 | ARGV.push("--log-directory=#{Dir.pwd}/logs") if (log_commands & ARGV).empty? && (help_commands & ARGV).empty? 271 | 272 | # Push help to front of command so thor recognizes subcommands are called with help 273 | if help_commands.any? { |cmd| ARGV.include? cmd } 274 | help_commands.each do |cmd| 275 | if (match = ARGV.delete(cmd)) 276 | ARGV.unshift match 277 | end 278 | end 279 | end 280 | -------------------------------------------------------------------------------- /lib/inspec_tools/summary.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | require 'yaml' 3 | require_relative '../utilities/inspec_util' 4 | 5 | # Impact Definitions 6 | CRITICAL = 0.9 7 | HIGH = 0.7 8 | MEDIUM = 0.5 9 | LOW = 0.3 10 | 11 | BUCKETS = %i(failed passed no_impact skipped error).freeze 12 | TALLYS = %i(total critical high medium low).freeze 13 | 14 | THRESHOLD_TEMPLATE = File.expand_path('../data/threshold.yaml', File.dirname(__FILE__)) 15 | 16 | module InspecTools 17 | class Summary 18 | attr_reader :json, :json_full, :json_counts, :threshold_file, :threshold_inline, :summary, :threshold 19 | 20 | def initialize(**options) 21 | options = options[:options] 22 | @json = JSON.parse(File.read(options[:inspec_json])) 23 | @json_full = false || options[:json_full] 24 | @json_counts = false || options[:json_counts] 25 | @threshold = parse_threshold(options[:threshold_inline], options[:threshold_file]) 26 | @threshold_provided = options[:threshold_inline] || options[:threshold_file] 27 | @summary = compute_summary 28 | end 29 | 30 | def output_summary 31 | unless @json_full || @json_counts 32 | puts "\nThreshold compliance: #{@threshold['compliance.min']}%" 33 | puts "\nOverall compliance: #{@summary[:compliance]}%\n\n" 34 | @summary[:status].keys.each do |category| 35 | puts category 36 | @summary[:status][category].keys.each do |impact| 37 | puts "\t#{impact} : #{@summary[:status][category][impact]}" 38 | end 39 | end 40 | end 41 | 42 | puts @summary.to_json if @json_full 43 | puts @summary[:status].to_json if @json_counts 44 | end 45 | 46 | def results_meet_threshold? 47 | raise 'Please provide threshold as a yaml file or inline yaml' unless @threshold_provided 48 | 49 | compliance = true 50 | failure = [] 51 | failure << check_max_compliance(@threshold['compliance.max'], @summary[:compliance], '', 'compliance') 52 | failure << check_min_compliance(@threshold['compliance.min'], @summary[:compliance], '', 'compliance') 53 | 54 | BUCKETS.each do |bucket| 55 | TALLYS.each do |tally| 56 | failure << check_min_compliance(@threshold["#{bucket}.#{tally}.min"], @summary[:status][bucket][tally], bucket, tally) 57 | failure << check_max_compliance(@threshold["#{bucket}.#{tally}.max"], @summary[:status][bucket][tally], bucket, tally) 58 | end 59 | end 60 | 61 | failure.reject!(&:nil?) 62 | compliance = false if failure.length.positive? 63 | output(compliance, failure) 64 | compliance 65 | end 66 | 67 | private 68 | 69 | def check_min_compliance(min, data, bucket, tally) 70 | expected_to_string(bucket, tally, 'min', min, data) if min != -1 and data < min 71 | end 72 | 73 | def check_max_compliance(max, data, bucket, tally) 74 | expected_to_string(bucket, tally, 'max', max, data) if max != -1 and data > max 75 | end 76 | 77 | def output(passed_threshold, what_failed) 78 | if passed_threshold 79 | puts "Overall compliance threshold of #{@threshold['compliance.min']}\% met. Current compliance at #{@summary[:compliance]}\%" 80 | else 81 | puts 'Compliance threshold was not met: ' 82 | puts what_failed.join("\n") 83 | end 84 | end 85 | 86 | def expected_to_string(bucket, tally, maxmin, value, got) 87 | return "Expected #{bucket}.#{tally}.#{maxmin}:#{value} got:#{got}" unless bucket.empty? || bucket.nil? 88 | 89 | "Expected #{tally}.#{maxmin}:#{value}\% got:#{got}\%" 90 | end 91 | 92 | def parse_threshold(threshold_inline, threshold_file) 93 | threshold = Utils::InspecUtil.to_dotted_hash(YAML.load_file(THRESHOLD_TEMPLATE)) 94 | threshold.merge!(Utils::InspecUtil.to_dotted_hash(YAML.load_file(threshold_file))) if threshold_file 95 | threshold.merge!(Utils::InspecUtil.to_dotted_hash(YAML.safe_load(threshold_inline))) if threshold_inline 96 | threshold 97 | end 98 | 99 | def compute_summary 100 | data = Utils::InspecUtil.parse_data_for_ckl(@json) 101 | 102 | data.keys.each do |control_id| 103 | current_control = data[control_id] 104 | current_control[:compliance_status] = Utils::InspecUtil.control_status(current_control, true) 105 | current_control[:finding_details] = Utils::InspecUtil.control_finding_details(current_control, current_control[:compliance_status]) 106 | end 107 | 108 | summary = {} 109 | summary[:buckets] = {} 110 | summary[:buckets][:failed] = select_by_status(data, 'Open') 111 | summary[:buckets][:passed] = select_by_status(data, 'NotAFinding') 112 | summary[:buckets][:no_impact] = select_by_status(data, 'Not_Applicable') 113 | summary[:buckets][:skipped] = select_by_status(data, 'Not_Reviewed') 114 | summary[:buckets][:error] = select_by_status(data, 'Profile_Error') 115 | 116 | summary[:status] = {} 117 | %i(failed passed no_impact skipped error).each do |key| 118 | summary[:status][key] = tally_by_impact(summary[:buckets][key]) 119 | end 120 | summary[:compliance] = compute_compliance(summary) 121 | summary 122 | end 123 | 124 | def select_by_impact(controls, impact) 125 | controls.select { |_key, value| value[:impact].to_f.eql?(impact) } 126 | end 127 | 128 | def select_by_status(controls, status) 129 | controls.select { |_key, value| value[:compliance_status].eql?(status) } 130 | end 131 | 132 | def tally_by_impact(controls) 133 | tally = {} 134 | tally[:total] = controls.count 135 | tally[:critical] = select_by_impact(controls, CRITICAL).count 136 | tally[:high] = select_by_impact(controls, HIGH).count 137 | tally[:medium] = select_by_impact(controls, MEDIUM).count 138 | tally[:low] = select_by_impact(controls, LOW).count 139 | tally 140 | end 141 | 142 | def compute_compliance(summary) 143 | (summary[:status][:passed][:total]*100.0/ 144 | (summary[:status][:passed][:total]+ 145 | summary[:status][:failed][:total]+ 146 | summary[:status][:skipped][:total]+ 147 | summary[:status][:error][:total])).floor 148 | end 149 | end 150 | end 151 | -------------------------------------------------------------------------------- /lib/inspec_tools/version.rb: -------------------------------------------------------------------------------- 1 | require 'git-version-bump' 2 | 3 | module InspecTools 4 | # Enable lite-tags (2nd parameter to git-version-bump version command) 5 | # Lite tags are tags that are used by GitHub releases that do not contain 6 | # annotations 7 | VERSION = GVB.version(false, true) 8 | end 9 | -------------------------------------------------------------------------------- /lib/inspec_tools/xccdf.rb: -------------------------------------------------------------------------------- 1 | require_relative '../happy_mapper_tools/stig_attributes' 2 | require_relative '../happy_mapper_tools/cci_attributes' 3 | require_relative '../utilities/inspec_util' 4 | 5 | require 'digest' 6 | require 'json' 7 | 8 | module InspecTools 9 | class XCCDF 10 | def initialize(xccdf, use_vuln_id, replace_tags = nil) 11 | @xccdf = xccdf 12 | @xccdf = replace_tags_in_xccdf(replace_tags, @xccdf) unless replace_tags.nil? 13 | cci_list_path = File.join(File.dirname(__FILE__), '../data/U_CCI_List.xml') 14 | @cci_items = HappyMapperTools::CCIAttributes::CCI_List.parse(File.read(cci_list_path)) 15 | register_after_parse_callbacks 16 | @benchmark = HappyMapperTools::StigAttributes::Benchmark.parse(@xccdf) 17 | @use_vuln_id = use_vuln_id 18 | end 19 | 20 | def to_ckl 21 | # TODO: to_ckl 22 | end 23 | 24 | def to_csv 25 | # TODO: to_csv 26 | end 27 | 28 | def to_inspec 29 | @profile = {} 30 | @controls = [] 31 | insert_json_metadata 32 | insert_controls 33 | @profile['sha256'] = Digest::SHA256.hexdigest @profile.to_s 34 | @profile 35 | end 36 | 37 | #### 38 | # extracts non-InSpec attributes 39 | ### 40 | # TODO there may be more attributes we want to extract, see data/attributes.yml for example 41 | def to_attributes 42 | @attribute = {} 43 | 44 | @attribute['benchmark.title'] = @benchmark.title 45 | @attribute['benchmark.id'] = @benchmark.id 46 | @attribute['benchmark.description'] = @benchmark.description 47 | @attribute['benchmark.version'] = @benchmark.version 48 | 49 | @attribute['benchmark.status'] = @benchmark.status 50 | @attribute['benchmark.status.date'] = @benchmark.release_date.release_date 51 | 52 | @attribute['benchmark.notice.id'] = @benchmark.notice.id 53 | 54 | @attribute['benchmark.plaintext'] = @benchmark.plaintext.plaintext 55 | @attribute['benchmark.plaintext.id'] = @benchmark.plaintext.id 56 | 57 | @attribute['reference.href'] = @benchmark.reference.href 58 | @attribute['reference.dc.publisher'] = @benchmark.reference.dc_publisher 59 | @attribute['reference.dc.source'] = @benchmark.reference.dc_source 60 | @attribute['reference.dc.title'] = @benchmark.group[0].rule.reference.dc_title if !@benchmark.group[0].nil? 61 | @attribute['reference.dc.subject'] = @benchmark.group[0].rule.reference.dc_subject if !@benchmark.group[0].nil? 62 | @attribute['reference.dc.type'] = @benchmark.group[0].rule.reference.dc_type if !@benchmark.group[0].nil? 63 | @attribute['reference.dc.identifier'] = @benchmark.group[0].rule.reference.dc_identifier if !@benchmark.group[0].nil? 64 | 65 | @attribute['content_ref.name'] = @benchmark.group[0].rule.check.content_ref.name if !@benchmark.group[0].nil? 66 | @attribute['content_ref.href'] = @benchmark.group[0].rule.check.content_ref.href if !@benchmark.group[0].nil? 67 | 68 | @attribute 69 | end 70 | 71 | def publisher 72 | @benchmark.reference.dc_publisher 73 | end 74 | 75 | def published 76 | @benchmark.release_date.release_date 77 | end 78 | 79 | def inject_metadata(metadata = '{}') 80 | json_metadata = JSON.parse(metadata) 81 | json_metadata.each do |key, value| 82 | @profile[key] = value 83 | end 84 | end 85 | 86 | private 87 | 88 | def register_after_parse_callbacks 89 | # Determine if the parsed Ident is refrencing a legacy ID number. 90 | HappyMapperTools::StigAttributes::Ident.after_parse do |object| 91 | object.cci = object.system.eql?('http://cyber.mil/cci') 92 | object.legacy = !object.cci 93 | end 94 | end 95 | 96 | def replace_tags_in_xccdf(replace_tags, xccdf_xml) 97 | replace_tags.each do |tag| 98 | xccdf_xml = xccdf_xml.gsub(/(<|<)#{tag}(>|>)/, "$#{tag}") 99 | end 100 | xccdf_xml 101 | end 102 | 103 | def insert_json_metadata 104 | @profile['name'] = @benchmark.id 105 | @profile['title'] = @benchmark.title 106 | @profile['maintainer'] = 'The Authors' if @profile['maintainer'].nil? 107 | @profile['copyright'] = 'The Authors' if @profile['copyright'].nil? 108 | @profile['copyright_email'] = 'you@example.com' if @profile['copyright_email'].nil? 109 | @profile['license'] = 'Apache-2.0' if @profile['license'].nil? 110 | @profile['summary'] = "\"#{@benchmark.description.gsub('\\', '\\\\\\').gsub('"', '\"')}\"" 111 | @profile['version'] = '0.1.0' if @profile['version'].nil? 112 | @profile['supports'] = [] 113 | @profile['attributes'] = [] 114 | @profile['generator'] = { 115 | name: 'inspec_tools', 116 | version: VERSION 117 | } 118 | @profile['plaintext'] = @benchmark.plaintext.plaintext 119 | @profile['status'] = "#{@benchmark.status} on #{@benchmark.release_date.release_date}" 120 | @profile['reference_href'] = @benchmark.reference.href 121 | @profile['reference_publisher'] = @benchmark.reference.dc_publisher 122 | @profile['reference_source'] = @benchmark.reference.dc_source 123 | end 124 | 125 | def insert_controls 126 | @benchmark.group.each do |group| 127 | control = {} 128 | control['id'] = @use_vuln_id ? group.id : group.rule.id.split('r').first 129 | control['title'] = group.rule.title 130 | control['desc'] = group.rule.description.vuln_discussion.split('Satisfies: ')[0] 131 | control['impact'] = Utils::InspecUtil.get_impact(group.rule.severity) 132 | control['tags'] = {} 133 | control['tags']['severity'] = Utils::InspecUtil.get_impact_string(control['impact']) 134 | control['tags']['gtitle'] = group.title 135 | control['tags']['satisfies'] = group.rule.description.vuln_discussion.split('Satisfies: ')[1].split(',').map(&:strip) if group.rule.description.vuln_discussion.split('Satisfies: ').length > 1 136 | control['tags']['gid'] = group.id 137 | control['tags']['rid'] = group.rule.id 138 | control['tags']['stig_id'] = group.rule.version 139 | control['tags']['fix_id'] = group.rule.fix.id 140 | control['tags']['cci'] = group.rule.idents.select(&:cci).map(&:ident) 141 | legacy_tags = group.rule.idents.select(&:legacy) 142 | control['tags']['legacy'] = legacy_tags.map(&:ident) unless legacy_tags.empty? 143 | control['tags']['nist'] = @cci_items.fetch_nists(control['tags']['cci']) 144 | control['tags']['false_negatives'] = group.rule.description.false_negatives if group.rule.description.false_negatives != '' 145 | control['tags']['false_positives'] = group.rule.description.false_positives if group.rule.description.false_positives != '' 146 | control['tags']['documentable'] = group.rule.description.documentable if group.rule.description.documentable != '' 147 | control['tags']['mitigations'] = group.rule.description.false_negatives if group.rule.description.mitigations != '' 148 | control['tags']['severity_override_guidance'] = group.rule.description.severity_override_guidance if group.rule.description.severity_override_guidance != '' 149 | control['tags']['security_override_guidance'] = group.rule.description.security_override_guidance if group.rule.description.security_override_guidance != '' 150 | control['tags']['potential_impacts'] = group.rule.description.potential_impacts if group.rule.description.potential_impacts != '' 151 | control['tags']['third_party_tools'] = group.rule.description.third_party_tools if group.rule.description.third_party_tools != '' 152 | control['tags']['mitigation_controls'] = group.rule.description.mitigation_controls if group.rule.description.mitigation_controls != '' 153 | control['tags']['responsibility'] = group.rule.description.responsibility if group.rule.description.responsibility != '' 154 | control['tags']['ia_controls'] = group.rule.description.ia_controls if group.rule.description.ia_controls != '' 155 | control['tags']['check'] = group.rule.check.content 156 | control['tags']['fix'] = group.rule.fixtext 157 | @controls << control 158 | end 159 | @profile['controls'] = @controls 160 | end 161 | end 162 | end 163 | -------------------------------------------------------------------------------- /lib/inspec_tools/xlsx_tool.rb: -------------------------------------------------------------------------------- 1 | require 'nokogiri' 2 | require 'inspec-objects' 3 | require 'word_wrap' 4 | require 'yaml' 5 | require 'digest' 6 | 7 | require_relative '../utilities/inspec_util' 8 | require_relative '../utilities/cis_to_nist' 9 | require_relative '../utilities/mapping_validator' 10 | 11 | module InspecTools 12 | # Methods for converting from XLS to various formats 13 | class XLSXTool 14 | LATEST_NIST_REV = 'Rev_4'.freeze 15 | 16 | def initialize(xlsx, mapping, name, verbose = false) 17 | @name = name 18 | @xlsx = xlsx 19 | @mapping = Utils::MappingValidator.validate(mapping) 20 | @verbose = verbose 21 | @cis_to_nist = Utils::CisToNist.get_mapping('cis_to_nist_mapping') 22 | end 23 | 24 | def to_ckl 25 | # TODO 26 | end 27 | 28 | def to_xccdf 29 | # TODO 30 | end 31 | 32 | def to_inspec(control_prefix) 33 | @controls = [] 34 | @cci_xml = nil 35 | @profile = {} 36 | insert_json_metadata 37 | parse_cis_controls(control_prefix) 38 | @profile['controls'] = @controls 39 | @profile['sha256'] = Digest::SHA256.hexdigest @profile.to_s 40 | @profile 41 | end 42 | 43 | private 44 | 45 | def insert_json_metadata 46 | @profile['name'] = @name 47 | @profile['title'] = 'InSpec Profile' 48 | @profile['maintainer'] = 'The Authors' 49 | @profile['copyright'] = 'The Authors' 50 | @profile['copyright_email'] = 'you@example.com' 51 | @profile['license'] = 'Apache-2.0' 52 | @profile['summary'] = 'An InSpec Compliance Profile' 53 | @profile['version'] = '0.1.0' 54 | @profile['supports'] = [] 55 | @profile['attributes'] = [] 56 | @profile['generator'] = { 57 | name: 'inspec_tools', 58 | version: ::InspecTools::VERSION 59 | } 60 | end 61 | 62 | def parse_cis_controls(control_prefix) 63 | [1, 2].each do |level| 64 | @xlsx.sheet(level).each_row_streaming do |row| 65 | if row[@mapping['control.id']].nil? || !/^\d+(\.?\d)*$/.match(row[@mapping['control.id']].formatted_value) 66 | next 67 | end 68 | 69 | tag_pos = @mapping['control.tags'] 70 | control = {} 71 | control['tags'] = {} 72 | control['id'] = "#{control_prefix}-#{row[@mapping['control.id']].formatted_value}" unless cell_empty?(@mapping['control.id']) || cell_empty?(row[@mapping['control.id']]) 73 | control['title'] = row[@mapping['control.title']].formatted_value unless cell_empty?(@mapping['control.title']) || cell_empty?(row[@mapping['control.title']]) 74 | control['desc'] = '' 75 | control['desc'] = row[@mapping['control.desc']].formatted_value unless cell_empty?(row[@mapping['control.desc']]) 76 | control['tags']['rationale'] = row[tag_pos['rationale']].formatted_value unless cell_empty?(row[tag_pos['rationale']]) 77 | 78 | control['tags']['severity'] = level == 1 ? 'medium' : 'high' 79 | control['impact'] = Utils::InspecUtil.get_impact(control['tags']['severity']) 80 | control['tags']['ref'] = row[@mapping['control.ref']].formatted_value unless cell_empty?(@mapping['control.ref']) || cell_empty?(row[@mapping['control.ref']]) 81 | control['tags']['cis_level'] = level unless level.nil? 82 | 83 | unless cell_empty?(row[tag_pos['cis_controls']]) 84 | # cis_control must be extracted from CIS control column via regex 85 | cis_tags_array = row[tag_pos['cis_controls']].formatted_value.scan(/CONTROL:v(\d) (\d+)\.?(\d*)/).flatten 86 | cis_tags = %i(revision section sub_section).zip(cis_tags_array).to_h 87 | control = apply_cis_and_nist_controls(control, cis_tags) 88 | end 89 | 90 | control['tags']['cis_rid'] = row[@mapping['control.id']].formatted_value unless cell_empty?(@mapping['control.id']) || cell_empty?(row[@mapping['control.id']]) 91 | control['tags']['check'] = row[tag_pos['check']].formatted_value unless cell_empty?(tag_pos['check']) || cell_empty?(row[tag_pos['check']]) 92 | control['tags']['fix'] = row[tag_pos['fix']].formatted_value unless cell_empty?(tag_pos['fix']) || cell_empty?(row[tag_pos['fix']]) 93 | 94 | @controls << control 95 | end 96 | end 97 | end 98 | 99 | def cell_empty?(cell) 100 | return cell.empty? if cell.respond_to?(:empty?) 101 | 102 | cell.nil? 103 | end 104 | 105 | def apply_cis_and_nist_controls(control, cis_tags) 106 | control['tags']['cis_controls'] = [] 107 | control['tags']['nist'] = [] 108 | 109 | if cis_tags[:sub_section].nil? || cis_tags[:sub_section].blank? 110 | control['tags']['cis_controls'] << cis_tags[:section] 111 | control['tags']['nist'] << get_nist_control_for_cis(cis_tags[:section]) 112 | else 113 | control['tags']['cis_controls'] << "#{cis_tags[:section]}.#{cis_tags[:sub_section]}" 114 | control['tags']['nist'] << get_nist_control_for_cis(cis_tags[:section], cis_tags[:sub_section]) 115 | end 116 | 117 | control['tags']['nist'] << LATEST_NIST_REV unless control['tags']['nist'].nil? 118 | control['tags']['cis_controls'] << "Rev_#{cis_tags[:revision]}" unless cis_tags[:revision].nil? 119 | 120 | control 121 | end 122 | 123 | def get_nist_control_for_cis(section, sub_section = nil) 124 | return @cis_to_nist[section] if sub_section.nil? 125 | 126 | @cis_to_nist["#{section}.#{sub_section}"] 127 | end 128 | end 129 | end 130 | -------------------------------------------------------------------------------- /lib/inspec_tools_plugin.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | libdir = File.dirname(__FILE__) 4 | $LOAD_PATH.unshift(libdir) unless $LOAD_PATH.include?(libdir) 5 | 6 | require 'inspec_tools/version' 7 | require 'inspec_tools/plugin' 8 | -------------------------------------------------------------------------------- /lib/overrides/false_class.rb: -------------------------------------------------------------------------------- 1 | class FalseClass 2 | def blank? 3 | true 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /lib/overrides/nil_class.rb: -------------------------------------------------------------------------------- 1 | class NilClass 2 | def blank? 3 | true 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /lib/overrides/object.rb: -------------------------------------------------------------------------------- 1 | class Object 2 | def blank? 3 | respond_to?(:empty?) ? empty? : !self 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /lib/overrides/string.rb: -------------------------------------------------------------------------------- 1 | class String 2 | def blank? 3 | strip.empty? 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /lib/overrides/true_class.rb: -------------------------------------------------------------------------------- 1 | class TrueClass 2 | def blank? 3 | false 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /lib/utilities/cci_xml.rb: -------------------------------------------------------------------------------- 1 | require 'nokogiri' 2 | 3 | module Utils 4 | class CciXml 5 | def self.get_cci_list(cci_list_file) 6 | path = File.expand_path(File.join(File.expand_path(__dir__), '..', 'data', cci_list_file)) 7 | raise "CCI list does not exist at #{path}" unless File.exist?(path) 8 | 9 | cci_list = Nokogiri::XML(File.open(path)) 10 | cci_list.remove_namespaces! 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/utilities/cis_to_nist.rb: -------------------------------------------------------------------------------- 1 | module Utils 2 | class CisToNist 3 | def self.get_mapping(mapping_file) 4 | path = File.expand_path(File.join(File.expand_path(__dir__), '..', 'data', mapping_file)) 5 | raise "CIS to NIST control mapping does not exist at #{path}. Has it been generated?" unless File.exist?(path) 6 | 7 | mapping = File.open(path) 8 | Marshal.load(mapping) 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/utilities/csv_util.rb: -------------------------------------------------------------------------------- 1 | require 'csv' 2 | 3 | module Utils 4 | class CSVUtil 5 | def self.unpack_csv(csv_string, file) 6 | csv = CSV.parse(csv_string) 7 | CSV.open(file, 'wb') do |csv_file| 8 | csv.each do |line| 9 | csv_file << line 10 | end 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/utilities/extract_pdf_text.rb: -------------------------------------------------------------------------------- 1 | require 'pdf-reader' 2 | 3 | module Util 4 | class ExtractPdfText 5 | def initialize(pdf) 6 | @pdf = pdf 7 | @extracted_text = '' 8 | read_text 9 | end 10 | 11 | attr_reader :extracted_text 12 | 13 | def read_text 14 | reader = PDF::Reader.new(@pdf.path) 15 | reader.pages.each do |page| 16 | @extracted_text += page.text 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/utilities/mapping_validator.rb: -------------------------------------------------------------------------------- 1 | module Utils 2 | class MappingValidator 3 | def self.validate(mapping) 4 | return mapping unless mapping.include?('control.tags') && mapping['control.tags'].include?('nist') 5 | 6 | raise "Mapping file should not contain an entry for 'control.tags.nist'. 7 | NIST tags will be autogenerated via 'control.tags.cci'." 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/utilities/text_cleaner.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module Util 4 | class TextCleaner 5 | # Takes in text file, cleans data and writes to new text file. 6 | def clean_data(data) 7 | clean_controls(data) 8 | end 9 | 10 | # Cleans control information from passed in file 11 | def clean_controls(extracted_data) 12 | controls_data = isolate_controls_data(extracted_data) 13 | clean_section_header = remove_section_header(controls_data) 14 | clean_whitespace = remove_newline_in_controls(clean_section_header) 15 | clean_special = remove_special(clean_whitespace) 16 | clean_no_space = remove_extra_space(clean_special) 17 | clean_pagenum = remove_pagenum(clean_no_space) 18 | separate_controls(clean_pagenum) 19 | end 20 | 21 | # Removes everything before and after the controls 22 | def isolate_controls_data(extracted_data) 23 | extracted_data = extracted_data.gsub(/\| P a g e+/, "| P a g e\n") 24 | extracted_data = extracted_data.split("\n").map(&:strip).reject { |e| e.to_s.empty? }.join("\n") 25 | extracted_data = extracted_data.gsub('???', '') 26 | /^1\.1\s*[^)]*?(?=\)$)(.*\n)*?(?=\s*Appendix:)/.match(extracted_data).to_s 27 | end 28 | 29 | # Removes all pagenumbers between the controls 30 | def remove_pagenum(extracted_data) 31 | clean_pagenum = extracted_data.gsub(/(\d{1,3}\|Page|\d{1,3} \| P a g e)/, '').to_s 32 | clean_pagenum.gsub(/(\d{1,3} \| Page)/, '').to_s 33 | end 34 | 35 | # Removes section headers for each control 36 | def remove_section_header(extracted_data) 37 | extracted_data.gsub(/(? 2 | 26 | 27 | 32 | 33 | 34 | This is an XML Schema for defining names in the 35 | Common Platform Enumeration, a naming system for 36 | IT platforms. Each CPE name has the following URI format: 37 | 38 | cpe:/ [ hardware-part ] [ / [ os-part ] [ / application-part ] ] 39 | 40 | Each part consists of zero or more name elements separated 41 | by semicolons. Generally, in a description listing like 42 | this, at most one name element will comprise each part. 43 | Each name element consists of one or more name components 44 | separated by colons. The first name component of each 45 | name element is a supplier or vendor name, expressed as 46 | the organization-specific label from the supplier's DNS 47 | name (e.g. from "microsoft.com" use "microsoft"). 48 | 49 | This specification was written by Neal Ziring, with assistance 50 | from Andrew Buttner, and ideas from Todd Wittbold and other 51 | attendees at the 2nd Annual NIST Security Automation Workshop. 52 | For more information, consult the CPE specification document. 53 | 1.0 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | This element acts as a top-level container for CPE 65 | name items. 66 | 67 | 68 | 69 | 70 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | This element denotes a single name in the Common 86 | Platform Enumeration. It has the following parts: 87 | - name, a URI, which must be a unique key, and 88 | should follow the URI structure outlined in 89 | the CPE specification. 90 | - title, arbitrary friendly name 91 | - notes, optional descriptive material 92 | - references, optional external info references 93 | - check, optional reference to an OVAL test that 94 | can confirm or reject an IT system as an 95 | instance of the named platform. 96 | 97 | 98 | 99 | 100 | 102 | 103 | 104 | 105 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 115 | 116 | 117 | 118 | 120 | 121 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | Type for an reference in the description of a CPE 138 | item. This would normally be used to point to extra 139 | descriptive material, or the supplier's web site, or 140 | the platform documentation. It consists of a piece 141 | of text (intended to be human-readable) and a URI 142 | (intended to be a URL, and point to a real resource). 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | Data type for the check element, a checking system 156 | specification URI, string content, and an optional 157 | external file reference. The checking system 158 | specification should be the URI for a particular 159 | version of OVAL or a related system testing language, 160 | and the content will be an identifier of a test written 161 | in that language. The external file reference could 162 | be used to point to the file in which the content test 163 | identifier is defined. 164 | 165 | 166 | 167 | 168 | 170 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | -------------------------------------------------------------------------------- /test/schemas/xccdf_114/cpe-language_2.0.xsd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | This XML Schema defines the CPE Language. An individual CPE Name addresses a single part of an actual system. To identify more complex platform types, there needs to be a way to combine different CPE Names using logical operators. For example, there may be a need to identify a platform with a particular operating system AND a certain application. The CPE Language exists to satisfy this need, enabling the CPE Name for the operating system to be combined with the CPE Name for the application. For more information, consult the CPE Specification document. 6 | 7 | CPE Language 8 | Neal Ziring, Andrew Buttner 9 | 2.0 (release candidate) 10 | 9/4/2007 09:00:00 AM 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | This element is the root element of a CPE Language XML documents and therefore acts as a container for child platform definitions. 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | The platform element represents the description or qualifications of a particular IT platform type. The platform is defined by the logical-test child element. The id attribute holds a locally unique name for the platform. There is no defined format for this id, it just has to be unique to the containing language document. 36 | The optional title element may appear as a child to a platform element. It provides a human-readable title for it. To support uses intended for multiple languages, this element supports the ‘xml:lang’ attribute. At most one title element can appear for each language. 37 | The optional remark element may appear as a child of a platform element. It provides some additional description. Zero or more remark elements may appear. To support uses intended for multiple languages, this element supports the ‘xml:lang’ attribute. There can be multiple remarks for a single language. 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | The logical-test element appears as a child of a platform element, and may also be nested to create more complex logical tests. The content consists of one or more elements: fact-ref, and logical-test children are permitted. The operator to be applied, and optional negation of the test, are given as attributes. 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | The fact-ref element appears as a child of a logical-test element. It is simply a reference to a CPE Name that always evaluates to a Boolean result. 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | The OperatorEnumeration simple type defines acceptable operators. Each operator defines how to evaluate multiple arguments. 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | This type allows the xml:lang attribute to associate a specific language with an element's string content. 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | Define the format for acceptable CPE Names. An urn format is used with the id starting with the word oval followed by a unique string, followed by the three letter code 'def', and ending with an integer. 94 | 95 | 96 | 97 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /test/schemas/xccdf_114/simpledc20021212.xsd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Simple DC XML Schema, 2002-10-09 6 | by Pete Johnston (p.johnston@ukoln.ac.uk), 7 | Carl Lagoze (lagoze@cs.cornell.edu), Andy Powell (a.powell@ukoln.ac.uk), 8 | Herbert Van de Sompel (hvdsomp@yahoo.com). 9 | This schema defines terms for Simple Dublin Core, i.e. the 15 10 | elements from the http://purl.org/dc/elements/1.1/ namespace, with 11 | no use of encoding schemes or element refinements. 12 | Default content type for all elements is xs:string with xml:lang 13 | attribute available. 14 | 15 | Supercedes version of 2002-03-12. 16 | Amended to remove namespace declaration for http://www.w3.org/XML/1998/namespace namespace, 17 | and to reference lang attribute via built-in xml: namespace prefix. 18 | xs:appinfo also removed. 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /test/schemas/xccdf_114/xccdfp-1.1.xsd: -------------------------------------------------------------------------------- 1 | 2 | 31 | 37 | 38 | 39 | 40 | This is an XML Schema for defining information 41 | structure about IT platforms, mainly for use with 42 | the eXtensible Common Checklist Description Format 43 | (XCCDF). This version of the XCCDF Platform 44 | Specification (XCCDF-P) is designed to be used 45 | with XCCDF 1.0 or 1.1, and may also be used 46 | with other XML data formats that need to describe 47 | aspects of IT product and system platforms. 48 | 49 | This specification was written by Neal Ziring, based 50 | on ideas from the DISA FSO VMS/Gold Disk team, from 51 | David Waltermire and David Proulx, and from Drew 52 | Buttner. 53 | 1.1.0.0 54 | 55 | 56 | 57 | 58 | 59 | 60 | 62 | 63 | 64 | Import the XML namespace schema so that we can use 65 | the xml: attribute groups (particularly xml:lang). 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | This element can act as a top-level container for the 78 | Fact definitions and Platform definitions that make up 79 | a full XCCDF-P specification. It should be used 80 | when a XCCDF-P spec is being distributed as a 81 | standalone document, or included in an XCCDF 1.1 82 | specification. 83 | 84 | This element schema used to include a keyref for 85 | Fact names, but it has been removed to allow for 86 | pre-defined Fact dictionaries. 87 | 88 | 89 | 90 | 91 | 93 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | This element denotes a single named Fact. Every fact 108 | has the following: 109 | - name, a URI, which must be a unique key 110 | - title, arbitrary text with xml:lang, optional 111 | - remark, arbitrary text with xml:lang, optional 112 | - check, XML content, optional 113 | 114 | 115 | 116 | 117 | 119 | 121 | 123 | 124 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | This element denotes a single Platform definition. 137 | A Platform definition represents the qualifications 138 | an IT asset or target must have to be considered an 139 | instance of a particular Platform. A Platform has 140 | the following: 141 | - id, a locally unique id 142 | - name, a URI, which must be a unique key 143 | - title, arbitrary text with xml:lang, optional 144 | - remark, arbitrary text with xml:lang, optional 145 | - definition ref, either a fact ref or a logical 146 | test (boolean combination of fact refs) 147 | 148 | 149 | 150 | 151 | 153 | 155 | 156 | 158 | 160 | 161 | 162 | 164 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | Type for a reference to Fact; the reference 177 | is always by name. This is the type for the 178 | element fact-ref, which can appear in a Platform 179 | definition or in a logical-test in a Platform 180 | definition. 181 | 182 | 183 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | Type for a test against several Facts; the content 193 | is one or more fact-refs and nested logical-tests. 194 | Allowed operators are AND and OR. 195 | The negate attribute, if set, makes the test 196 | its logical inverse (so you get NAND and NOR). 197 | Note that the output of a logical-test is always 198 | TRUE or FALSE, Unknowns map to FALSE. 199 | 200 | 201 | 202 | 204 | 206 | 207 | 209 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | Data type for the check element, a checking system 218 | specification URI, and XML content. The check 219 | element may appear inside a Fact, giving a means 220 | to ascertain the value of that Fact using a 221 | particular checking engine. (This checkType is 222 | based on the one in XCCDF, but is somewhat simpler. 223 | It does not include the notion of exporting values 224 | from the scope of an XCCDF document to the checking 225 | engine.) 226 | 227 | 228 | 229 | 230 | 232 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | Data type for the check-content-ref element, which 243 | points to the code for a detached check in another file. 244 | This element has no body, just a couple of attributes: 245 | href and name. The name is optional, if it does not appear 246 | then this reference is to the entire other document. 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | Data type for the check-content element, which holds 257 | the actual code of an enveloped check in some other 258 | (non-XCCDF-P) language. This element can hold almost 259 | anything. The content is not meaningful as XCCDF-P, 260 | though tools may process it or hand it off to other 261 | tools. 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | Allowed operators for logic tests: we only 278 | have two, AND and OR. They're capitalized 279 | for consistency with usage in OVAL v4. 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | Type for a string with an xml:lang attribute. 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | -------------------------------------------------------------------------------- /test/schemas/xccdf_114/xml.xsd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | See http://www.w3.org/XML/1998/namespace.html and 8 | http://www.w3.org/TR/REC-xml for information about this namespace. 9 | 10 | This schema document describes the XML namespace, in a form 11 | suitable for import by other schema documents. 12 | 13 | Note that local names in this namespace are intended to be defined 14 | only by the World Wide Web Consortium or its subgroups. The 15 | following names are currently defined in this namespace and should 16 | not be used with conflicting semantics by any Working Group, 17 | specification, or document instance: 18 | 19 | base (as an attribute name): denotes an attribute whose value 20 | provides a URI to be used as the base for interpreting any 21 | relative URIs in the scope of the element on which it 22 | appears; its value is inherited. This name is reserved 23 | by virtue of its definition in the XML Base specification. 24 | 25 | lang (as an attribute name): denotes an attribute whose value 26 | is a language code for the natural language of the content of 27 | any element; its value is inherited. This name is reserved 28 | by virtue of its definition in the XML specification. 29 | 30 | space (as an attribute name): denotes an attribute whose 31 | value is a keyword indicating what whitespace processing 32 | discipline is intended for the content of the element; its 33 | value is inherited. This name is reserved by virtue of its 34 | definition in the XML specification. 35 | 36 | Father (in any context at all): denotes Jon Bosak, the chair of 37 | the original XML Working Group. This name is reserved by 38 | the following decision of the W3C XML Plenary and 39 | XML Coordination groups: 40 | 41 | In appreciation for his vision, leadership and dedication 42 | the W3C XML Plenary on this 10th day of February, 2000 43 | reserves for Jon Bosak in perpetuity the XML name 44 | xml:Father 45 | 46 | 47 | 48 | 49 | This schema defines attributes and an attribute group 50 | suitable for use by 51 | schemas wishing to allow xml:base, xml:lang or xml:space attributes 52 | on elements they define. 53 | 54 | To enable this, such a schema must import this schema 55 | for the XML namespace, e.g. as follows: 56 | <schema . . .> 57 | . . . 58 | <import namespace="http://www.w3.org/XML/1998/namespace" 59 | schemaLocation="http://www.w3.org/2001/03/xml.xsd"/> 60 | 61 | Subsequently, qualified reference to any of the attributes 62 | or the group defined below will have the desired effect, e.g. 63 | 64 | <type . . .> 65 | . . . 66 | <attributeGroup ref="xml:specialAttrs"/> 67 | 68 | will define a type which will schema-validate an instance 69 | element with any of those attributes 70 | 71 | 72 | 73 | In keeping with the XML Schema WG's standard versioning 74 | policy, this schema document will persist at 75 | http://www.w3.org/2001/03/xml.xsd. 76 | At the date of issue it can also be found at 77 | http://www.w3.org/2001/xml.xsd. 78 | The schema document at that URI may however change in the future, 79 | in order to remain compatible with the latest version of XML Schema 80 | itself. In other words, if the XML Schema namespace changes, the version 81 | of this document at 82 | http://www.w3.org/2001/xml.xsd will change 83 | accordingly; the version at 84 | http://www.w3.org/2001/03/xml.xsd will not change. 85 | 86 | 87 | 88 | 89 | 90 | In due course, we should install the relevant ISO 2- and 3-letter 91 | codes as the enumerated possible values . . . 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | See http://www.w3.org/TR/xmlbase/ for 107 | information about this attribute. 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | -------------------------------------------------------------------------------- /test/unit/inspec_tools/csv_test.rb: -------------------------------------------------------------------------------- 1 | require 'csv' 2 | require 'yaml' 3 | require_relative '../test_helper' 4 | require_relative '../../../lib/inspec_tools/csv' 5 | 6 | class CSVTest < Minitest::Test 7 | def test_that_csv_exists 8 | refute_nil ::InspecTools::CSVTool 9 | end 10 | 11 | def test_csv_init_with_valid_params 12 | csv = CSV.read('examples/csv2inspec/stig.csv', encoding: 'ISO8859-1') 13 | mapping = YAML.load_file('examples/csv2inspec/mapping.yml') 14 | assert(InspecTools::CSVTool.new(csv, mapping, 'test', false)) 15 | end 16 | 17 | def test_csv_init_with_invalid_params 18 | csv = nil 19 | mapping = nil 20 | assert_raises(StandardError) { InspecTools::CSVTool.new(csv, mapping, 'test', false) } 21 | end 22 | 23 | def test_csv_to_inspec 24 | csv = CSV.read('examples/csv2inspec/stig.csv', encoding: 'ISO8859-1') 25 | mapping = YAML.load_file('examples/csv2inspec/mapping.yml') 26 | csv_tool = InspecTools::CSVTool.new(csv, mapping, 'test', false) 27 | inspec_json = csv_tool.to_inspec 28 | assert(inspec_json) 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /test/unit/inspec_tools/happymapper_test.rb: -------------------------------------------------------------------------------- 1 | require 'minitest/spec' 2 | require 'minitest/autorun' 3 | 4 | describe 'HappyMapperTools::Benchmark::Ident correctly determines the system for each identifier' do 5 | # test values (tv); tv[0] == identifier, tv[1] == system 6 | vs_list = { 7 | 'CCI-000213' => 'http://cyber.mil/cci', 8 | 'V-72859' => 'http://cyber.mil/legacy', 9 | 'SV-87511' => 'http://cyber.mil/legacy', 10 | 'CCI-00213' => 'https://public.cyber.mil/stigs/cci/', 11 | 'CCI-0000213' => 'https://public.cyber.mil/stigs/cci/' 12 | } 13 | 14 | vs_list.each do |identifier, system| 15 | it identifier do 16 | # Ident.new automatically determines ident.system 17 | ident = HappyMapperTools::Benchmark::Ident.new identifier 18 | assert_equal(system, ident.system) 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /test/unit/inspec_tools/metadata_test.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | require_relative '../test_helper' 3 | require 'stringio' 4 | require 'fakefs/safe' 5 | require 'o_stream_catcher' 6 | 7 | class MetadataTest < Minitest::Test 8 | def setup 9 | OStreamCatcher.catch do 10 | @inspec_tools_instance = InspecTools::CLI.new([], {}, {}) 11 | end 12 | end 13 | 14 | def test_metadata_inspec 15 | metadata = {} 16 | inspec_expected = { 'maintainer'=>'y', 'copyright'=>'y', 'copyright_email'=>'y', 'license'=>'y', 'version'=>'y' } 17 | 18 | FakeFS do 19 | test_populate_stdin(5) do 20 | OStreamCatcher.catch do 21 | @inspec_tools_instance.generate_inspec_metadata 22 | end 23 | end 24 | 25 | metadata = JSON.parse(File.read('metadata.json')) 26 | end 27 | assert_equal metadata, inspec_expected 28 | end 29 | 30 | def test_metadata_ckl 31 | metadata = {} 32 | ckl_expected = { 'benchmark'=>{ 'title'=>'y', 'version'=>'y', 'plaintext'=>'y' }, 'stigid'=>'y', 'role'=>'y', 'type'=>'y', 'hostname'=>'y', 'ip'=>'y', 'mac'=>'y', 'fqdn'=>'y', 'tech_area'=>'y', 'target_key'=>'y', 'web_or_database'=>'y', 'web_db_site'=>'y', 'web_db_instance'=>'y' } 33 | 34 | FakeFS do 35 | test_populate_stdin(15) do 36 | OStreamCatcher.catch do 37 | @inspec_tools_instance.generate_ckl_metadata 38 | end 39 | end 40 | 41 | metadata = JSON.parse(File.read('metadata.json')) 42 | end 43 | assert_equal metadata, ckl_expected 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /test/unit/inspec_tools/pdf_test.rb: -------------------------------------------------------------------------------- 1 | require_relative '../test_helper' 2 | 3 | class PDFTest < Minitest::Test 4 | def test_that_csv_exists 5 | refute_nil ::InspecTools::PDF 6 | end 7 | 8 | def test_pdf_init_with_valid_params 9 | pdf = File.open('examples/CIS_Ubuntu_Linux_16.04_LTS_Benchmark_v1.0.0.pdf') 10 | assert(InspecTools::PDF.new(pdf, 'test', ENV['INSPEC_TOOLS_DEBUG'] || false)) 11 | end 12 | 13 | def test_pdf_init_with_invalid_params 14 | pdf = nil 15 | assert_raises(StandardError) { InspecTools::PDF.new(pdf, 'test', ENV['INSPEC_TOOLS_DEBUG'] || false) } 16 | end 17 | 18 | def test_pdf_to_inspec 19 | pdf = File.open('examples/CIS_Ubuntu_Linux_16.04_LTS_Benchmark_v1.0.0.pdf') 20 | pdf_tool = InspecTools::PDF.new(pdf, 'test', ENV['INSPEC_TOOLS_DEBUG'] || false) 21 | inspec_json = pdf_tool.to_inspec 22 | assert(inspec_json) 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /test/unit/inspec_tools/summary_test.rb: -------------------------------------------------------------------------------- 1 | require 'csv' 2 | require 'yaml' 3 | require_relative '../test_helper' 4 | require_relative '../../../lib/inspec_tools/csv' 5 | 6 | class SummaryTest < Minitest::Test 7 | def test_that_summary_exists 8 | refute_nil ::InspecTools::Summary 9 | end 10 | 11 | def test_summary_init_with_valid_params 12 | options = { options: {} } 13 | options[:options][:inspec_json] = 'examples/sample_json/rhel-simp.json' 14 | assert(InspecTools::Summary.new(**options)) 15 | end 16 | 17 | def test_summary_init_with_invalid_params 18 | # File not found 19 | options = { options: {} } 20 | options[:options][:inspec_json] = 'does_not_exist' 21 | assert_raises(StandardError) { InspecTools::Summary.new(**options) } 22 | end 23 | 24 | def test_summary 25 | options = { options: {} } 26 | options[:options][:inspec_json] = 'examples/sample_json/rhel-simp.json' 27 | inspec_tools = InspecTools::Summary.new(**options) 28 | assert_equal(77, inspec_tools.summary[:compliance]) 29 | assert_equal(33, inspec_tools.summary[:status][:failed][:medium]) 30 | end 31 | 32 | def test_inspec_results_compliance_pass 33 | options = { options: {} } 34 | options[:options][:inspec_json] = 'examples/sample_json/rhel-simp.json' 35 | options[:options][:threshold_inline] = '{compliance.min: 77, failed.critical.max: 0, failed.high.max: 3}' 36 | inspec_tools = InspecTools::Summary.new(**options) 37 | assert_output(/Overall compliance threshold of \d\d% met/) { inspec_tools.results_meet_threshold? } 38 | end 39 | 40 | def test_inspec_results_compliance_fail 41 | options = { options: {} } 42 | options[:options][:inspec_json] = 'examples/sample_json/rhel-simp.json' 43 | options[:options][:threshold_inline] = '{compliance.min: 80, failed.critical.max: 0, failed.high.max: 0}' 44 | inspec_tools = InspecTools::Summary.new(**options) 45 | assert_output(/Expected compliance.min:80% got:77%(\r\n|\r|\n)Expected failed.high.max:0 got:3/) { inspec_tools.results_meet_threshold? } 46 | 47 | options[:options][:threshold_inline] = '{compliance.max: 50}' 48 | inspec_tools = InspecTools::Summary.new(**options) 49 | assert_output(/Expected compliance.max:50% got:77%/) { inspec_tools.results_meet_threshold? } 50 | 51 | options[:options][:threshold_file] = 'examples/sample_yaml/threshold.yaml' 52 | options[:options][:threshold_inline] = nil 53 | inspec_tools = InspecTools::Summary.new(**options) 54 | assert_output(/Expected compliance.min:81% got:77%/) { inspec_tools.results_meet_threshold? } 55 | end 56 | 57 | def test_inline_overrides 58 | options = { options: {} } 59 | options[:options][:inspec_json] = 'examples/sample_json/rhel-simp.json' 60 | options[:options][:threshold_inline] = '{compliance.min: 77}' 61 | options[:options][:threshold_file] = 'examples/sample_yaml/threshold.yaml' 62 | inspec_tools = InspecTools::Summary.new(**options) 63 | inspec_tools.results_meet_threshold? 64 | assert_equal(77, inspec_tools.threshold['compliance.min']) 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /test/unit/inspec_tools/xccdf_test.rb: -------------------------------------------------------------------------------- 1 | require_relative '../test_helper' 2 | 3 | class XCCDFTest < Minitest::Test 4 | def test_that_xccdf_exists 5 | refute_nil ::InspecTools::XCCDF 6 | end 7 | 8 | def test_xccdf_init_with_valid_params 9 | xccdf = File.read('examples/xccdf2inspec/data/U_RHEL_7_STIG_V3R3_Manual-xccdf.xml') 10 | assert(InspecTools::XCCDF.new(xccdf, true)) 11 | end 12 | 13 | def test_xccdf_init_with_invalid_params 14 | xccdf = nil 15 | assert_raises(StandardError) { InspecTools::XCCDF.new(xccdf, true) } 16 | end 17 | 18 | def test_xccdf_attributes 19 | xccdf = InspecTools::XCCDF.new(File.read('examples/xccdf2inspec/data/U_RHEL_7_STIG_V3R3_Manual-xccdf.xml'), true) 20 | assert_equal('DISA', xccdf.publisher) 21 | assert_equal('2021-03-01', xccdf.published) 22 | end 23 | 24 | def test_to_inspec 25 | xccdf = InspecTools::XCCDF.new(File.read('examples/xccdf2inspec/data/U_RHEL_7_STIG_V3R3_Manual-xccdf.xml'), true) 26 | assert(xccdf.to_inspec) 27 | end 28 | 29 | def test_to_inspec_metadata 30 | xccdf = InspecTools::XCCDF.new(File.read('examples/xccdf2inspec/data/U_RHEL_7_STIG_V3R3_Manual-xccdf.xml'), true) 31 | inspec_json = xccdf.to_inspec 32 | assert_equal('RHEL_7_STIG', inspec_json['name']) 33 | assert_equal('Red Hat Enterprise Linux 7 Security Technical Implementation Guide', inspec_json['title']) 34 | assert_equal('The Authors', inspec_json['maintainer']) 35 | assert_equal('The Authors', inspec_json['copyright']) 36 | assert_equal('you@example.com', inspec_json['copyright_email']) 37 | assert_equal('Apache-2.0', inspec_json['license']) 38 | assert_equal('"This Security Technical Implementation Guide is published as a tool to improve the security of Department of Defense (DoD) information systems. The requirements are derived from the National Institute of Standards and Technology (NIST) 800-53 and related documents. Comments or proposed revisions to this document should be sent via email to the following address: disa.stig_spt@mail.mil."', inspec_json['summary']) 39 | assert_equal('0.1.0', inspec_json['version']) 40 | assert_empty(inspec_json['supports']) 41 | assert_empty(inspec_json['attributes']) 42 | end 43 | 44 | def test_controls_count 45 | xccdf = InspecTools::XCCDF.new(File.read('examples/xccdf2inspec/data/U_RHEL_7_STIG_V3R3_Manual-xccdf.xml'), true) 46 | inspec_json = xccdf.to_inspec 47 | assert_equal(249, inspec_json['controls'].count) 48 | end 49 | 50 | def test_vuln_id_as_control_id 51 | xccdf = InspecTools::XCCDF.new(File.read('examples/xccdf2inspec/data/U_RHEL_7_STIG_V3R3_Manual-xccdf.xml'), true) 52 | vuln_id_inspec_json = xccdf.to_inspec 53 | assert(vuln_id_inspec_json['controls'].first['id'].start_with?('V-')) 54 | end 55 | 56 | def test_rule_id_as_control_id 57 | xccdf = InspecTools::XCCDF.new(File.read('examples/xccdf2inspec/data/U_RHEL_7_STIG_V3R3_Manual-xccdf.xml'), false) 58 | rule_id_inspec_json = xccdf.to_inspec 59 | assert(rule_id_inspec_json['controls'].first['id'].start_with?('SV-')) 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /test/unit/inspec_tools/xlsx_tool_test.rb: -------------------------------------------------------------------------------- 1 | require_relative '../test_helper' 2 | require 'roo' 3 | require 'yaml' 4 | 5 | class XLSXToolTest < Minitest::Test 6 | def setup 7 | @xlsx = Roo::Spreadsheet.open('examples/cis.xlsx') 8 | @mapping = YAML.load_file('examples/xlsx2inspec/mapping.cis.yml') 9 | end 10 | 11 | def test_that_xlsx_exists 12 | refute_nil ::InspecTools::XLSXTool 13 | end 14 | 15 | def test_to_inspec 16 | xlsx = InspecTools::XLSXTool.new(@xlsx, @mapping, 'test') 17 | profile = xlsx.to_inspec('test') 18 | assert_equal('test', profile['name']) 19 | assert_equal profile['generator'][:version], ::InspecTools::VERSION 20 | assert_equal(['IA-2 (1)', 'Rev_4'], profile['controls'].first['tags']['nist']) 21 | assert_equal(['4.5', 'Rev_7'], profile['controls'].first['tags']['cis_controls']) 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /test/unit/inspec_tools_test.rb: -------------------------------------------------------------------------------- 1 | require_relative 'test_helper' 2 | 3 | class InspecToolsTest < Minitest::Test 4 | def test_that_it_has_a_version_number 5 | refute_nil ::InspecTools::VERSION 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /test/unit/test_helper.rb: -------------------------------------------------------------------------------- 1 | require 'simplecov' 2 | SimpleCov.start 3 | require 'minitest/autorun' 4 | $LOAD_PATH.unshift File.expand_path('../../lib', __dir__) 5 | require 'inspec_tools' 6 | 7 | require 'minitest/reporters' 8 | 9 | reporter_options = { color: true, slow_count: 5 } 10 | Minitest::Reporters.use! [Minitest::Reporters::SpecReporter.new(reporter_options)] 11 | 12 | # Populate stdin with the specified number of 'y' characters 13 | def test_populate_stdin(number) 14 | string_io = StringIO.new 15 | number.times do 16 | string_io.puts 'y' 17 | end 18 | string_io.rewind 19 | 20 | $stdin = string_io 21 | 22 | yield 23 | 24 | $stdin = STDIN 25 | end 26 | -------------------------------------------------------------------------------- /test/unit/utils/inspec_util_test.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | require 'fileutils' 3 | require_relative '../test_helper' 4 | require_relative '../../../lib/utilities/inspec_util' 5 | 6 | class InspecUtilTest < Minitest::Test 7 | def test_inspec_util_exists 8 | refute_nil Utils::InspecUtil 9 | end 10 | 11 | def test_string_to_impact 12 | # CVSS Terms True 13 | ['none', 'na', 'n/a', 'N/A', 'NONE', 'not applicable', 'not_applicable', 'NOT_APPLICABLE'].each do |word| 14 | assert_in_delta(0.0, Utils::InspecUtil.get_impact(word)) 15 | end 16 | 17 | ['low', 'cat iii', 'cat iii', 'CATEGORY III', 'cat 3'].each do |word| 18 | assert_in_delta(0.3, Utils::InspecUtil.get_impact(word)) 19 | end 20 | 21 | ['medium', 'med', 'cat ii', 'cat ii', 'CATEGORY II', 'cat 2'].each do |word| 22 | assert_in_delta(0.5, Utils::InspecUtil.get_impact(word)) 23 | end 24 | 25 | ['high', 'cat i', 'cat i', 'CATEGORY I', 'cat 1'].each do |word| 26 | assert_in_delta(0.7, Utils::InspecUtil.get_impact(word)) 27 | end 28 | 29 | %w{critical crit severe}.each do |word| 30 | assert_in_delta(0.7, Utils::InspecUtil.get_impact(word)) 31 | end 32 | 33 | # CVSS Terms False 34 | 35 | ['none', 'na', 'n/a', 'N/A', 'NONE', 'not applicable', 'not_applicable', 'NOT_APPLICABLE'].each do |word| 36 | assert_in_delta(0.0, Utils::InspecUtil.get_impact(word, use_cvss_terms: false)) 37 | end 38 | 39 | ['low', 'cat iii', 'cat iii', 'CATEGORY III', 'cat 3'].each do |word| 40 | assert_in_delta(0.3, Utils::InspecUtil.get_impact(word, use_cvss_terms: false)) 41 | end 42 | 43 | ['medium', 'med', 'cat ii', 'cat ii', 'CATEGORY II', 'cat 2'].each do |word| 44 | assert_in_delta(0.5, Utils::InspecUtil.get_impact(word, use_cvss_terms: false)) 45 | end 46 | 47 | ['high', 'cat i', 'cat i', 'CATEGORY I', 'cat 1'].each do |word| 48 | assert_in_delta(0.7, Utils::InspecUtil.get_impact(word, use_cvss_terms: false)) 49 | end 50 | 51 | %w{critical crit severe}.each do |word| 52 | assert_in_delta(1.0, Utils::InspecUtil.get_impact(word, use_cvss_terms: false)) 53 | end 54 | end 55 | 56 | def test_float_to_impact 57 | # CVSS Terms True 58 | assert_in_delta(0.0, Utils::InspecUtil.get_impact(0.01)) 59 | assert_in_delta(0.3, Utils::InspecUtil.get_impact(0.1)) 60 | assert_in_delta(0.3, Utils::InspecUtil.get_impact(0.2)) 61 | assert_in_delta(0.3, Utils::InspecUtil.get_impact(0.3)) 62 | assert_in_delta(0.5, Utils::InspecUtil.get_impact(0.4)) 63 | assert_in_delta(0.5, Utils::InspecUtil.get_impact(0.5)) 64 | assert_in_delta(0.5, Utils::InspecUtil.get_impact(0.6)) 65 | assert_in_delta(0.7, Utils::InspecUtil.get_impact(0.7)) 66 | assert_in_delta(0.7, Utils::InspecUtil.get_impact(0.8)) 67 | assert_in_delta(0.7, Utils::InspecUtil.get_impact(0.9)) 68 | 69 | # CVSS Terms False 70 | assert_in_delta(0.0, Utils::InspecUtil.get_impact(0.01, use_cvss_terms: false)) 71 | assert_in_delta(0.3, Utils::InspecUtil.get_impact(0.1, use_cvss_terms: false)) 72 | assert_in_delta(0.3, Utils::InspecUtil.get_impact(0.2, use_cvss_terms: false)) 73 | assert_in_delta(0.3, Utils::InspecUtil.get_impact(0.3, use_cvss_terms: false)) 74 | assert_in_delta(0.5, Utils::InspecUtil.get_impact(0.4, use_cvss_terms: false)) 75 | assert_in_delta(0.5, Utils::InspecUtil.get_impact(0.5, use_cvss_terms: false)) 76 | assert_in_delta(0.5, Utils::InspecUtil.get_impact(0.6, use_cvss_terms: false)) 77 | assert_in_delta(0.7, Utils::InspecUtil.get_impact(0.7, use_cvss_terms: false)) 78 | assert_in_delta(0.7, Utils::InspecUtil.get_impact(0.8, use_cvss_terms: false)) 79 | assert_in_delta(1.0, Utils::InspecUtil.get_impact(0.9, use_cvss_terms: false)) 80 | end 81 | 82 | def test_get_impact_error 83 | assert_raises(Utils::InspecUtil::SeverityInputError) do 84 | Utils::InspecUtil.get_impact('bad value') 85 | end 86 | assert_raises(Utils::InspecUtil::SeverityInputError) do 87 | Utils::InspecUtil.get_impact(9001) 88 | end 89 | assert_raises(Utils::InspecUtil::SeverityInputError) do 90 | Utils::InspecUtil.get_impact(9001.1) 91 | end 92 | end 93 | 94 | def test_get_impact_string 95 | # CVSS True 96 | assert_equal('none', Utils::InspecUtil.get_impact_string(0)) 97 | assert_equal('none', Utils::InspecUtil.get_impact_string(0.01)) 98 | assert_equal('low', Utils::InspecUtil.get_impact_string(0.1)) 99 | assert_equal('low', Utils::InspecUtil.get_impact_string(0.2)) 100 | assert_equal('low', Utils::InspecUtil.get_impact_string(0.3)) 101 | assert_equal('medium', Utils::InspecUtil.get_impact_string(0.4)) 102 | assert_equal('medium', Utils::InspecUtil.get_impact_string(0.5)) 103 | assert_equal('medium', Utils::InspecUtil.get_impact_string(0.6)) 104 | assert_equal('high', Utils::InspecUtil.get_impact_string(0.7)) 105 | assert_equal('high', Utils::InspecUtil.get_impact_string(0.8)) 106 | assert_equal('high', Utils::InspecUtil.get_impact_string(0.9)) 107 | assert_equal('high', Utils::InspecUtil.get_impact_string(1.0)) 108 | assert_equal('high', Utils::InspecUtil.get_impact_string(1)) 109 | 110 | # CVSS False 111 | assert_equal('none', Utils::InspecUtil.get_impact_string(0, use_cvss_terms: false)) 112 | assert_equal('none', Utils::InspecUtil.get_impact_string(0.01, use_cvss_terms: false)) 113 | assert_equal('low', Utils::InspecUtil.get_impact_string(0.1, use_cvss_terms: false)) 114 | assert_equal('low', Utils::InspecUtil.get_impact_string(0.2, use_cvss_terms: false)) 115 | assert_equal('low', Utils::InspecUtil.get_impact_string(0.3, use_cvss_terms: false)) 116 | assert_equal('medium', Utils::InspecUtil.get_impact_string(0.4, use_cvss_terms: false)) 117 | assert_equal('medium', Utils::InspecUtil.get_impact_string(0.5, use_cvss_terms: false)) 118 | assert_equal('medium', Utils::InspecUtil.get_impact_string(0.6, use_cvss_terms: false)) 119 | assert_equal('high', Utils::InspecUtil.get_impact_string(0.7, use_cvss_terms: false)) 120 | assert_equal('high', Utils::InspecUtil.get_impact_string(0.8, use_cvss_terms: false)) 121 | assert_equal('critical', Utils::InspecUtil.get_impact_string(0.9, use_cvss_terms: false)) 122 | assert_equal('critical', Utils::InspecUtil.get_impact_string(1.0, use_cvss_terms: false)) 123 | assert_equal('critical', Utils::InspecUtil.get_impact_string(1, use_cvss_terms: false)) 124 | end 125 | 126 | def test_get_impact_string_error 127 | assert_raises(Utils::InspecUtil::ImpactInputError) do 128 | Utils::InspecUtil.get_impact_string(9001) 129 | end 130 | 131 | assert_raises(Utils::InspecUtil::ImpactInputError) do 132 | Utils::InspecUtil.get_impact_string(9001.1) 133 | end 134 | 135 | assert_raises(Utils::InspecUtil::ImpactInputError) do 136 | Utils::InspecUtil.get_impact_string(-1) 137 | end 138 | end 139 | 140 | def test_unpack_inspec_json 141 | json = JSON.parse(File.read('./examples/sample_json/single_control_profile.json')) 142 | dir = Dir.mktmpdir 143 | begin 144 | Utils::InspecUtil.unpack_inspec_json(dir, json, false, 'ruby') 145 | assert_path_exists("#{dir}/inspec.yml") 146 | assert_path_exists("#{dir}/README.md") 147 | assert(Dir.exist?("#{dir}/libraries")) 148 | assert(Dir.exist?("#{dir}/controls")) 149 | ensure 150 | FileUtils.rm_rf dir 151 | end 152 | end 153 | 154 | def test_parse_data_for_ckl 155 | json = JSON.parse(File.read('./examples/sample_json/single_control_results.json')) 156 | ckl_json = Utils::InspecUtil.parse_data_for_ckl(json) 157 | assert_equal('Use human readable security markings', ckl_json[:"V-26680"][:rule_title]) 158 | assert_equal('AC-16 (5) Rev_4', ckl_json[:"V-26680"][:nist]) 159 | end 160 | end 161 | -------------------------------------------------------------------------------- /test/unit/utils/xccdf/xccdf_score_test.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../test_helper' 2 | require_relative '../../../../lib/utilities/xccdf/xccdf_score' 3 | require_relative '../../../../lib/happy_mapper_tools/benchmark' 4 | 5 | describe Utils::XCCDFScore do 6 | before do 7 | @dci = Utils::XCCDFScore.new(groups, rule_results) 8 | end 9 | 10 | let(:groups) { [] } 11 | let(:rule_results) { [] } 12 | let(:rule_result_pass1) { HappyMapperTools::Benchmark::RuleResultType.new.tap { |r| r.result = 'pass' }.tap { |r| r.idref = 'rule1' } } 13 | let(:rule_result_pass2) { HappyMapperTools::Benchmark::RuleResultType.new.tap { |r| r.result = 'pass' }.tap { |r| r.idref = 'rule2' } } 14 | let(:rule_result_pass3) { HappyMapperTools::Benchmark::RuleResultType.new.tap { |r| r.result = 'pass' }.tap { |r| r.idref = 'rule5' } } 15 | let(:rule_result_fail1) { HappyMapperTools::Benchmark::RuleResultType.new.tap { |r| r.result = 'fail' }.tap { |r| r.idref = 'rule3' } } 16 | let(:rule_result_fail2) { HappyMapperTools::Benchmark::RuleResultType.new.tap { |r| r.result = 'fail' }.tap { |r| r.idref = 'rule4' } } 17 | let(:rule1) { HappyMapperTools::Benchmark::Rule.new.tap { |r| r.id = 'rule1' } } 18 | let(:group1) { HappyMapperTools::Benchmark::Group.new.tap { |g| g.rule = rule1 } } 19 | let(:rule2) { HappyMapperTools::Benchmark::Rule.new.tap { |r| r.id = 'rule2' } } 20 | let(:group2) { HappyMapperTools::Benchmark::Group.new.tap { |g| g.rule = rule2 } } 21 | let(:rule3) { HappyMapperTools::Benchmark::Rule.new.tap { |r| r.id = 'rule3' } } 22 | let(:group3) { HappyMapperTools::Benchmark::Group.new.tap { |g| g.rule = rule3 } } 23 | let(:rule4) { HappyMapperTools::Benchmark::Rule.new.tap { |r| r.id = 'rule4' }.tap { |r| r.weight = 10.0 } } 24 | let(:group4) { HappyMapperTools::Benchmark::Group.new.tap { |g| g.rule = rule4 } } 25 | let(:rule5) { HappyMapperTools::Benchmark::Rule.new.tap { |r| r.id = 'rule5' }.tap { |r| r.weight = 0.0 } } 26 | let(:group5) { HappyMapperTools::Benchmark::Group.new.tap { |g| g.rule = rule5 } } 27 | 28 | describe '#default_score' do 29 | it 'returns the correct system identifier' do 30 | assert_equal 'urn:xccdf:scoring:default', @dci.default_score.system 31 | end 32 | 33 | it 'returns the maximum score of 100' do 34 | assert_equal 100, @dci.default_score.maximum 35 | end 36 | 37 | describe 'when there are no results' do 38 | it 'returns a score of 0' do 39 | assert_equal 0, @dci.default_score.score 40 | end 41 | end 42 | 43 | describe 'when all tests pass' do 44 | let(:groups) { [group1, group2] } 45 | let(:rule_results) { [rule_result_pass1, rule_result_pass2] } 46 | 47 | it 'returns a score of 100' do 48 | assert_equal 100, @dci.default_score.score 49 | end 50 | end 51 | 52 | describe 'when some tests pass' do 53 | let(:groups) { [group1, group2, group3] } 54 | let(:rule_results) { [rule_result_pass1, rule_result_pass2, rule_result_fail1] } 55 | 56 | it 'returns a score of 66.67' do 57 | assert_in_delta(66.67, @dci.default_score.score) 58 | end 59 | end 60 | 61 | describe 'when no tests pass' do 62 | it 'returns a score of 0' do 63 | assert_in_delta(0.0, @dci.default_score.score) 64 | end 65 | end 66 | end 67 | 68 | describe '#flat_score' do 69 | let(:groups) { [group1, group2, group3] } 70 | let(:rule_results) { [rule_result_pass1, rule_result_pass2, rule_result_fail1] } 71 | 72 | it 'returns the correct system identifier' do 73 | assert_equal 'urn:xccdf:scoring:flat', @dci.flat_score.system 74 | end 75 | 76 | it 'returns the maximum score of 3' do 77 | assert_equal 3, @dci.flat_score.maximum 78 | end 79 | 80 | describe 'when a rule is weighted' do 81 | let(:groups) { [group1, group3, group4] } 82 | let(:rule_results) { [rule_result_pass1, rule_result_fail1, rule_result_fail2] } 83 | 84 | it 'applies weighting to the score' do 85 | score = @dci.flat_score 86 | assert_equal 1, score.score 87 | assert_equal 12, score.maximum 88 | end 89 | end 90 | 91 | describe 'when a rule has a weight of 0' do 92 | let(:groups) { [group1, group3, group5] } 93 | let(:rule_results) { [rule_result_pass1, rule_result_fail1, rule_result_pass3] } 94 | 95 | it 'is not included in the score' do 96 | score = @dci.flat_score 97 | assert_equal 1, score.score 98 | assert_equal 2, score.maximum 99 | end 100 | end 101 | 102 | describe 'when all tests pass' do 103 | let(:groups) { [group1, group2] } 104 | let(:rule_results) { [rule_result_pass1, rule_result_pass2] } 105 | 106 | it 'returns a score of 2' do 107 | assert_equal 2, @dci.flat_score.score 108 | end 109 | end 110 | 111 | describe 'when some tests pass' do 112 | let(:groups) { [group1, group2, group3] } 113 | let(:rule_results) { [rule_result_pass1, rule_result_pass2, rule_result_fail1] } 114 | 115 | it 'returns a score of 2' do 116 | assert_equal 2, @dci.flat_score.score 117 | end 118 | end 119 | 120 | describe 'when no tests pass' do 121 | let(:groups) { [group3] } 122 | let(:rule_results) { [rule_result_fail1] } 123 | 124 | it 'returns a score of 0' do 125 | assert_in_delta(0.0, @dci.flat_score.score) 126 | end 127 | end 128 | end 129 | 130 | describe '#flat_unweighted_score' do 131 | let(:groups) { [group1, group2, group3] } 132 | let(:rule_results) { [rule_result_pass1, rule_result_pass2, rule_result_fail1] } 133 | 134 | it 'returns the correct system identifier' do 135 | assert_equal 'urn:xccdf:scoring:flat-unweighted', @dci.flat_unweighted_score.system 136 | end 137 | 138 | it 'returns the maximum score of 3' do 139 | assert_equal 3, @dci.flat_unweighted_score.maximum 140 | end 141 | 142 | describe 'when a rule is weighted' do 143 | let(:groups) { [group1, group3, group4] } 144 | let(:rule_results) { [rule_result_pass1, rule_result_fail1, rule_result_fail2] } 145 | 146 | it 'applies weighting to the score' do 147 | score = @dci.flat_unweighted_score 148 | assert_equal 1, score.score 149 | assert_equal 3, score.maximum 150 | end 151 | end 152 | 153 | describe 'when a rule has a weight of 0' do 154 | let(:groups) { [group1, group3, group5] } 155 | let(:rule_results) { [rule_result_pass1, rule_result_fail1, rule_result_pass3] } 156 | 157 | it 'is not included in the score' do 158 | score = @dci.flat_unweighted_score 159 | assert_equal 1, score.score 160 | assert_equal 2, score.maximum 161 | end 162 | end 163 | 164 | describe 'when all tests pass' do 165 | it 'returns a score of 2' do 166 | assert_equal 2, @dci.flat_unweighted_score.score 167 | end 168 | end 169 | 170 | describe 'when some tests pass' do 171 | it 'returns a score of 2' do 172 | assert_equal 2, @dci.flat_unweighted_score.score 173 | end 174 | end 175 | 176 | describe 'when no tests pass' do 177 | let(:groups) { [group3] } 178 | let(:rule_results) { [rule_result_fail1] } 179 | 180 | it 'returns a score of 0' do 181 | assert_in_delta(0.0, @dci.flat_unweighted_score.score) 182 | end 183 | end 184 | end 185 | 186 | describe '#absolute_score' do 187 | it 'returns the correct system identifier' do 188 | assert_equal 'urn:xccdf:scoring:absolute', @dci.absolute_score.system 189 | end 190 | 191 | it 'returns the maximum score of 1' do 192 | assert_equal 1, @dci.absolute_score.maximum 193 | end 194 | 195 | describe 'when there are no results' do 196 | it 'returns a score of 0' do 197 | assert_equal 0, @dci.absolute_score.score 198 | end 199 | end 200 | 201 | describe 'when all tests pass' do 202 | let(:groups) { [group1, group2] } 203 | let(:rule_results) { [rule_result_pass1, rule_result_pass2] } 204 | 205 | it 'returns a score of 1' do 206 | assert_equal 1, @dci.absolute_score.score 207 | end 208 | end 209 | 210 | describe 'when some tests pass' do 211 | it 'returns a score of 0.0' do 212 | assert_in_delta(0.0, @dci.absolute_score.score) 213 | end 214 | end 215 | 216 | describe 'when no tests pass' do 217 | it 'returns a score of 0.0' do 218 | assert_in_delta(0.0, @dci.absolute_score.score) 219 | end 220 | end 221 | end 222 | 223 | describe '#rule_counts_and_score' do 224 | describe 'when no results are provided' do 225 | it 'returns count of 0 and score of 0' do 226 | results = [] 227 | assert_equal({ rule_count: 0, rule_score: 0 }, @dci.send(:rule_counts_and_score, results)) 228 | end 229 | end 230 | 231 | describe 'when a result is notapplicable' do 232 | let(:result_not_applicable) { HappyMapperTools::Benchmark::RuleResultType.new.tap { |r| r.result = 'notapplicable' } } 233 | 234 | it 'is not counted in the rule count or score' do 235 | results = [result_not_applicable] 236 | assert_equal({ rule_count: 0, rule_score: 0 }, @dci.send(:rule_counts_and_score, results)) 237 | end 238 | end 239 | 240 | describe 'when a result is notchecked' do 241 | let(:result_not_checked) { HappyMapperTools::Benchmark::RuleResultType.new.tap { |r| r.result = 'notchecked' } } 242 | 243 | it 'is not counted in the rule count or score' do 244 | results = [result_not_checked] 245 | assert_equal({ rule_count: 0, rule_score: 0 }, @dci.send(:rule_counts_and_score, results)) 246 | end 247 | end 248 | 249 | describe 'when a result is informational' do 250 | let(:result_informational) { HappyMapperTools::Benchmark::RuleResultType.new.tap { |r| r.result = 'informational' } } 251 | 252 | it 'is not counted in the rule count or score' do 253 | results = [result_informational] 254 | assert_equal({ rule_count: 0, rule_score: 0 }, @dci.send(:rule_counts_and_score, results)) 255 | end 256 | end 257 | 258 | describe 'when a result is notselected' do 259 | let(:result_not_selected) { HappyMapperTools::Benchmark::RuleResultType.new.tap { |r| r.result = 'notselected' } } 260 | 261 | it 'is not counted in the rule count or score' do 262 | results = [result_not_selected] 263 | assert_equal({ rule_count: 0, rule_score: 0 }, @dci.send(:rule_counts_and_score, results)) 264 | end 265 | end 266 | 267 | describe 'when a result is pass' do 268 | let(:result_pass) { HappyMapperTools::Benchmark::RuleResultType.new.tap { |r| r.result = 'pass' } } 269 | 270 | it 'is counted in the rule count and score' do 271 | results = [result_pass] 272 | assert_equal({ rule_count: 1, rule_score: 1 }, @dci.send(:rule_counts_and_score, results)) 273 | end 274 | end 275 | 276 | describe 'when a result is fail' do 277 | let(:result_fail) { HappyMapperTools::Benchmark::RuleResultType.new.tap { |r| r.result = 'fail' } } 278 | 279 | it 'is counted in the rule count and score' do 280 | results = [result_fail] 281 | assert_equal({ rule_count: 1, rule_score: 0 }, @dci.send(:rule_counts_and_score, results)) 282 | end 283 | end 284 | end 285 | end 286 | --------------------------------------------------------------------------------