diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml index e10ffe2..645e586 100644 --- a/.github/workflows/ruby.yml +++ b/.github/workflows/ruby.yml @@ -1,20 +1,30 @@ -name: Ruby CI +name: Ruby -on: [push, pull_request] +on: + push: + branches: + - main + + pull_request: jobs: build: - runs-on: ubuntu-latest + name: Ruby ${{ matrix.ruby }} + strategy: + matrix: + ruby: + - '3.2.2' + - '3.1.4' + - '3.0.6' + - '2.7.7' steps: - uses: actions/checkout@v3 - - name: Set up Ruby 2.6 + - name: Set up Ruby ${{ matrix.ruby }} uses: ruby/setup-ruby@v1 with: - ruby-version: '2.6' - - name: Build and test with Rake - run: | - gem install bundler - bundle install --jobs 4 --retry 3 - bundle exec rake + ruby-version: ${{ matrix.ruby }} + bundler-cache: true + - name: Run the default task + run: bundle exec rake diff --git a/.gitignore b/.gitignore index 723ef36..29a7627 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ -.idea \ No newline at end of file +.idea +/Gemfile.lock +/tmp/aruba +/coverage \ No newline at end of file diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000..0dbd7ac --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,17 @@ +AllCops: + NewCops: enable + TargetRubyVersion: 2.7 + +inherit_from: .rubocop_todo.yml + +# The behavior of RuboCop can be controlled via the .rubocop.yml +# configuration file. It makes it possible to enable/disable +# certain cops (checks) and to alter their behavior if they accept +# any parameters. The file can be placed either in your home +# directory or in some project directory. +# +# RuboCop will start looking for the configuration file in the directory +# where the inspected file is and continue its way up to the root directory. +# +# See https://docs.rubocop.org/rubocop/configuration + diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml new file mode 100644 index 0000000..accbce0 --- /dev/null +++ b/.rubocop_todo.yml @@ -0,0 +1,290 @@ +# This configuration was generated by +# `rubocop --auto-gen-config` +# on 2023-04-05 19:57:43 UTC using RuboCop version 1.49.0. +# The point is for the user to remove these configuration records +# one by one as the offenses are removed from the code base. +# Note that changes in the inspected code, or installation of new +# versions of RuboCop, may require this file to be generated again. + +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: Severity, Include. +# Include: **/*.gemspec +Gemspec/DeprecatedAttributeAssignment: + Exclude: + - 'cyclonedx-ruby.gemspec' + +# Offense count: 4 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: TreatCommentsAsGroupSeparators, ConsiderPunctuation, Include. +# Include: **/*.gemspec +Gemspec/OrderedDependencies: + Exclude: + - 'cyclonedx-ruby.gemspec' + +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: Severity, Include. +# Include: **/*.gemspec +Gemspec/RequireMFA: + Exclude: + - 'cyclonedx-ruby.gemspec' + +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +Layout/EmptyLineAfterMagicComment: + Exclude: + - 'lib/bom_builder.rb' + +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle. +# SupportedStyles: around, only_before +Layout/EmptyLinesAroundAccessModifier: + Exclude: + - 'lib/bom_builder.rb' + +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +Layout/EmptyLinesAroundMethodBody: + Exclude: + - 'lib/bom_component.rb' + +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: AllowForAlignment, AllowBeforeTrailingComments, ForceEqualSignAlignment. +Layout/ExtraSpacing: + Exclude: + - 'cyclonedx-ruby.gemspec' + +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle, IndentationWidth. +# SupportedStyles: special_inside_parentheses, consistent, align_brackets +Layout/FirstArrayElementIndentation: + Exclude: + - 'lib/bom_component.rb' + +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +Layout/LeadingEmptyLines: + Exclude: + - 'lib/bom_component.rb' + +# Offense count: 2 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle. +# SupportedStyles: final_newline, final_blank_line +Layout/TrailingEmptyLines: + Exclude: + - 'Rakefile' + - 'lib/bom_component.rb' + +# Offense count: 3 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: AllowInHeredoc. +Layout/TrailingWhitespace: + Exclude: + - 'Rakefile' + - 'spec/bom_component_spec.rb' + +# Offense count: 2 +Lint/IneffectiveAccessModifier: + Exclude: + - 'lib/bom_builder.rb' + +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +Lint/ScriptPermission: + Exclude: + - 'Rakefile' + +# Offense count: 1 +Lint/ShadowingOuterLocalVariable: + Exclude: + - 'lib/bom_builder.rb' + +# Offense count: 19 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle. +# SupportedStyles: strict, consistent +Lint/SymbolConversion: + Exclude: + - 'lib/bom_component.rb' + - 'lib/bom_helpers.rb' + +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: AllowUnusedKeywordArguments, IgnoreEmptyMethods, IgnoreNotImplementedMethods. +Lint/UnusedMethodArgument: + Exclude: + - 'lib/bom_builder.rb' + +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: ContextCreatingMethods, MethodCreatingMethods. +Lint/UselessAccessModifier: + Exclude: + - 'lib/bom_builder.rb' + +# Offense count: 4 +# Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes. +Metrics/AbcSize: + Max: 67 + +# Offense count: 4 +# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns. +# AllowedMethods: refine +Metrics/BlockLength: + Max: 38 + +# Offense count: 1 +# Configuration parameters: CountComments, CountAsOne. +Metrics/ClassLength: + Max: 128 + +# Offense count: 1 +# Configuration parameters: AllowedMethods, AllowedPatterns. +Metrics/CyclomaticComplexity: + Max: 9 + +# Offense count: 6 +# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns. +Metrics/MethodLength: + Max: 68 + +# Offense count: 1 +# Configuration parameters: AllowedMethods, AllowedPatterns. +Metrics/PerceivedComplexity: + Max: 12 + +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: PreferredName. +Naming/RescuedExceptionsVariableName: + Exclude: + - 'Rakefile' + +# Offense count: 3 +# Configuration parameters: AllowedConstants. +Style/Documentation: + Exclude: + - 'spec/**/*' + - 'test/**/*' + - 'Rakefile' + - 'lib/bom_builder.rb' + - 'lib/bom_component.rb' + +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: AllowedVars. +Style/FetchEnvVar: + Exclude: + - 'lib/bom_helpers.rb' + +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +Style/FileWrite: + Exclude: + - 'lib/bom_builder.rb' + +# Offense count: 12 +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: EnforcedStyle. +# SupportedStyles: always, always_true, never +Style/FrozenStringLiteralComment: + Exclude: + - '.simplecov' + - 'Gemfile' + - 'Rakefile' + - 'features/fixtures/simple/Gemfile' + - 'features/step_definitions/json_bom_matching.rb' + - 'features/step_definitions/xml_bom_matching.rb' + - 'features/support/env.rb' + - 'features/support/simplecov_support.rb' + - 'lib/bom_component.rb' + - 'spec/bom_component_spec.rb' + - 'spec/bom_helpers_spec.rb' + - 'spec/spec_helper.rb' + +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: AllowedMethods, AllowedPatterns. +Style/MethodCallWithoutArgsParentheses: + Exclude: + - 'lib/bom_helpers.rb' + +# Offense count: 1 +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: EnforcedStyle. +# SupportedStyles: literals, strict +Style/MutableConstant: + Exclude: + - 'lib/bom_builder.rb' + +# Offense count: 2 +Style/OpenStructUse: + Exclude: + - 'lib/bom_builder.rb' + - 'spec/bom_component_spec.rb' + +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: PreferredDelimiters. +Style/PercentLiteralDelimiters: + Exclude: + - 'Rakefile' + +# Offense count: 19 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: . +# SupportedStyles: same_as_string_literals, single_quotes, double_quotes +Style/QuotedSymbols: + EnforcedStyle: double_quotes + +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +Style/RedundantBegin: + Exclude: + - 'Rakefile' + +# Offense count: 6 +# This cop supports safe autocorrection (--autocorrect). +Style/RedundantRegexpEscape: + Exclude: + - 'features/step_definitions/json_bom_matching.rb' + - 'features/step_definitions/xml_bom_matching.rb' + +# Offense count: 20 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle, ConsistentQuotesInMultiline. +# SupportedStyles: single_quotes, double_quotes +Style/StringLiterals: + Exclude: + - '.simplecov' + - 'Rakefile' + - 'cyclonedx-ruby.gemspec' + - 'lib/bom_component.rb' + - 'lib/bom_helpers.rb' + - 'spec/bom_component_spec.rb' + - 'spec/bom_helpers_spec.rb' + +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: MinSize. +# SupportedStyles: percent, brackets +Style/SymbolArray: + EnforcedStyle: brackets + +# Offense count: 2 +# This cop supports safe autocorrection (--autocorrect). +Style/SymbolLiteral: + Exclude: + - 'lib/bom_component.rb' + +# Offense count: 5 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns. +# URISchemes: http, https +Layout/LineLength: + Max: 237 diff --git a/.simplecov b/.simplecov new file mode 100644 index 0000000..f1709c5 --- /dev/null +++ b/.simplecov @@ -0,0 +1,38 @@ +# Copied from https://github.com/cucumber/aruba/blob/3b1a6cea6e3ba55370c3396eef0a955aeb40f287/.simplecov +# Licensed under MIT - https://github.com/cucumber/aruba/blob/3b1a6cea6e3ba55370c3396eef0a955aeb40f287/LICENSE + +SimpleCov.configure do + enable_for_subprocesses true + + # Activate branch coverage + enable_coverage :branch + + # ignore this file + add_filter ".simplecov" + add_filter "features" + + # Rake tasks aren't tested with rspec + add_filter "Rakefile" + add_filter "lib/tasks" + + # + # Changed Files in Git Group + # @see http://fredwu.me/post/35625566267/simplecov-test-coverage-for-changed-files-only + untracked = `git ls-files --exclude-standard --others` + unstaged = `git diff --name-only` + staged = `git diff --name-only --cached` + all = untracked + unstaged + staged + changed_filenames = all.split("\n") + + add_group "Changed" do |source_file| + changed_filenames.select do |changed_filename| + source_file.filename.end_with?(changed_filename) + end + end + + add_group "Libraries", "lib" + + # Specs are reported on to ensure that all examples are being run and all + # lets, befores, afters, etc are being used. + add_group "Specs", "spec/" +end diff --git a/Rakefile b/Rakefile index 5c4b046..a18e7a9 100644 --- a/Rakefile +++ b/Rakefile @@ -1,7 +1,47 @@ #!/usr/bin/env rake +$LOAD_PATH << File.expand_path(__dir__) + +require "aruba/platform" + +require "bundler" +Bundler.setup + require 'bundler/gem_tasks' -require 'rspec/core/rake_task' +require "cucumber/rake/task" +require "rspec/core/rake_task" +require 'rake/clean' + +# Work around a bug in `rake/clean` from `rake` versions older than 13. It's +# failing when it calls `FileUtils::rm_r` because that method needs to receive +# the `opts` parameter as parameters instead of as a `Hash`. +module Rake + module Cleaner + module_function + + def cleanup(file_name, **opts) + begin + opts = { verbose: Rake.application.options.trace }.merge(opts) + rm_r file_name, **opts + rescue StandardError => ex + puts "Failed to remove #{file_name}: #{ex}" unless file_already_gone?(file_name) + end + end + end +end + +# Remove the `coverage` directory when the `:clobber` task is run. +CLOBBER.include('coverage') + +Cucumber::Rake::Task.new do |t| + t.cucumber_opts = %w(--format progress) +end RSpec::Core::RakeTask.new('spec') -task default: :spec \ No newline at end of file +# Run the `clobber` task when running the entire test suite, because the +# coverage information reported by `simplecov` can be skewed when a `coverage` +# directory is already present. +desc "Run the whole test suite." +task test: [:clobber, :spec, :cucumber] + +task default: :test \ No newline at end of file diff --git a/cucumber.yml b/cucumber.yml new file mode 100644 index 0000000..fea5edc --- /dev/null +++ b/cucumber.yml @@ -0,0 +1 @@ +default: --publish-quiet diff --git a/cyclonedx-ruby.gemspec b/cyclonedx-ruby.gemspec index 50bc271..9d5d0b0 100644 --- a/cyclonedx-ruby.gemspec +++ b/cyclonedx-ruby.gemspec @@ -8,14 +8,29 @@ Gem::Specification.new do |spec| spec.description = 'CycloneDX is a lightweight software bill-of-material (SBOM) specification designed for use in application security contexts and supply chain component analysis. This Gem generates CycloneDX BOMs from Ruby projects.' spec.authors = ['Joseph Kobti', 'Steve Springett'] spec.email = 'josephkobti@outlook.com' - spec.files = ['lib/bom_builder.rb', 'lib/bom_helpers.rb', 'lib/licenses.json', 'lib/bom_component.rb'] spec.homepage = 'https://github.com/CycloneDX/cyclonedx-ruby-gem' spec.license = 'Apache-2.0' - spec.executables << 'cyclonedx-ruby' + + spec.required_ruby_version = ">= 2.7.0" + + spec.files = Dir.chdir(__dir__) do + `git ls-files -z`.split("\x0").reject do |f| + (File.expand_path(f) == __FILE__) || f.start_with?(*%w[bin/ test/ spec/ features/ .git .circleci appveyor]) + end + end + spec.bindir = 'exe' + spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) } + spec.require_paths = ['lib'] + spec.add_dependency('json', '~> 2.2') spec.add_dependency('nokogiri', '~> 1.8') spec.add_dependency('ostruct', '~> 0.1') spec.add_dependency('rest-client', '~> 2.0') + spec.add_dependency('activesupport', '~> 7.0') spec.add_development_dependency 'rake', '~> 12' spec.add_development_dependency 'rspec', '~> 3.7' + spec.add_development_dependency 'cucumber', '~> 8.0' + spec.add_development_dependency 'aruba', '~> 2.1' + spec.add_development_dependency 'simplecov', '~> 0.22.0' + spec.add_development_dependency 'rubocop', '~> 1.48' end diff --git a/bin/cyclonedx-ruby b/exe/cyclonedx-ruby similarity index 100% rename from bin/cyclonedx-ruby rename to exe/cyclonedx-ruby diff --git a/features/defaults.feature b/features/defaults.feature new file mode 100644 index 0000000..3157633 --- /dev/null +++ b/features/defaults.feature @@ -0,0 +1,13 @@ +Feature: Default parameter values + +Many of the options for the `cyclonedx-ruby` command are optional. + +Scenario: Running against simple fixture + Given I use a fixture named "simple" + And I run `cyclonedx-ruby --path .` + Then the output should contain: + """ + 5 gems were written to BOM located at ./bom.xml + """ + And a file named "bom.xml" should exist + And the generated XML BOM file "bom.xml" matches "bom.xml.expected" diff --git a/features/fixtures/simple/Gemfile b/features/fixtures/simple/Gemfile new file mode 100644 index 0000000..180e0f6 --- /dev/null +++ b/features/fixtures/simple/Gemfile @@ -0,0 +1,3 @@ +source 'https://rubygems.org' + +gem 'activesupport' diff --git a/features/fixtures/simple/Gemfile.lock b/features/fixtures/simple/Gemfile.lock new file mode 100644 index 0000000..db602fc --- /dev/null +++ b/features/fixtures/simple/Gemfile.lock @@ -0,0 +1,23 @@ +GEM + remote: https://rubygems.org/ + specs: + activesupport (7.0.4.3) + concurrent-ruby (~> 1.0, >= 1.0.2) + i18n (>= 1.6, < 2) + minitest (>= 5.1) + tzinfo (~> 2.0) + concurrent-ruby (1.2.2) + i18n (1.12.0) + concurrent-ruby (~> 1.0) + minitest (5.18.0) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + +PLATFORMS + arm64-darwin-22 + +DEPENDENCIES + activesupport + +BUNDLED WITH + 2.4.10 diff --git a/features/fixtures/simple/bom.json.expected b/features/fixtures/simple/bom.json.expected new file mode 100644 index 0000000..d9d7dce --- /dev/null +++ b/features/fixtures/simple/bom.json.expected @@ -0,0 +1,108 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.1", + "serialNumber": "urn:uuid:d498cdc2-5494-4031-b37d-ff3d10d336bf", + "version": 1, + "components": [ + { + "type": "library", + "name": "activesupport", + "version": "7.0.4.3", + "description": "A toolkit of support libraries and Ruby core extensions extracted from the Rails framework.", + "purl": "pkg:gem/activesupport@7.0.4.3", + "hashes": [ + { + "alg": "SHA-256", + "content": "571ed0fac8510f1fc8a1d66aa070d07ea269913bf9ef50960a8044536358a096" + } + ], + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ] + }, + { + "type": "library", + "name": "concurrent-ruby", + "version": "1.2.2", + "description": "Modern concurrency tools for Ruby. Inspired by Erlang, Clojure, Scala, Haskell, F#, C#, Java, and classic concurrency patterns.", + "purl": "pkg:gem/concurrent-ruby@1.2.2", + "hashes": [ + { + "alg": "SHA-256", + "content": "3879119b8b75e3b62616acc256c64a134d0b0a7a9a3fcba5a233025bcde22c4f" + } + ], + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ] + }, + { + "type": "library", + "name": "i18n", + "version": "1.12.0", + "description": "New wave Internationalization support for Ruby", + "purl": "pkg:gem/i18n@1.12.0", + "hashes": [ + { + "alg": "SHA-256", + "content": "91e3cc1b97616d308707eedee413d82ee021d751c918661fb82152793e64aced" + } + ], + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ] + }, + { + "type": "library", + "name": "minitest", + "version": "5.18.0", + "description": "minitest provides a complete suite of testing facilities supporting TDD, BDD, mocking, and benchmarking", + "purl": "pkg:gem/minitest@5.18.0", + "hashes": [ + { + "alg": "SHA-256", + "content": "06f43aa0692ce3acf19cb5bc539ad2c6095ca3d2c7e5fbafc58a7d847e898745" + } + ], + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ] + }, + { + "type": "library", + "name": "tzinfo", + "version": "2.0.6", + "description": "Time Zone Library", + "purl": "pkg:gem/tzinfo@2.0.6", + "hashes": [ + { + "alg": "SHA-256", + "content": "8daf828cc77bcf7d63b0e3bdb6caa47e2272dcfaf4fbfe46f8c3a9df087a829b" + } + ], + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ] + } + ] +} \ No newline at end of file diff --git a/features/fixtures/simple/bom.xml.expected b/features/fixtures/simple/bom.xml.expected new file mode 100644 index 0000000..b379102 --- /dev/null +++ b/features/fixtures/simple/bom.xml.expected @@ -0,0 +1,75 @@ + + + + + activesupport + 7.0.4.3 + A toolkit of support libraries and Ruby core extensions extracted from the Rails framework. + + 571ed0fac8510f1fc8a1d66aa070d07ea269913bf9ef50960a8044536358a096 + + + + MIT + + + pkg:gem/activesupport@7.0.4.3 + + + concurrent-ruby + 1.2.2 + Modern concurrency tools for Ruby. Inspired by Erlang, Clojure, Scala, Haskell, F#, C#, Java, and classic concurrency patterns. + + 3879119b8b75e3b62616acc256c64a134d0b0a7a9a3fcba5a233025bcde22c4f + + + + MIT + + + pkg:gem/concurrent-ruby@1.2.2 + + + i18n + 1.12.0 + New wave Internationalization support for Ruby + + 91e3cc1b97616d308707eedee413d82ee021d751c918661fb82152793e64aced + + + + MIT + + + pkg:gem/i18n@1.12.0 + + + minitest + 5.18.0 + minitest provides a complete suite of testing facilities supporting TDD, BDD, mocking, and benchmarking + + 06f43aa0692ce3acf19cb5bc539ad2c6095ca3d2c7e5fbafc58a7d847e898745 + + + + MIT + + + pkg:gem/minitest@5.18.0 + + + tzinfo + 2.0.6 + Time Zone Library + + 8daf828cc77bcf7d63b0e3bdb6caa47e2272dcfaf4fbfe46f8c3a9df087a829b + + + + MIT + + + pkg:gem/tzinfo@2.0.6 + + + diff --git a/features/help.feature b/features/help.feature new file mode 100644 index 0000000..f2ee863 --- /dev/null +++ b/features/help.feature @@ -0,0 +1,16 @@ +Feature: Command Help + +The `cyclonedx-ruby` needs to provide help output so that people know the +parameters that they can specify. + +Scenario: Generate help on demand + Given I run `cyclonedx-ruby --help` + Then the output should contain: + """ + Usage: cyclonedx-ruby [options] + -v, --[no-]verbose Run verbosely + -p, --path path (Required) Path to Ruby project directory + -o, --output bom_file_path (Optional) Path to output the bom.xml file to + -f, --format bom_output_format (Optional) Output format for bom. Currently support xml (default) and json. + -h, --help Show help message + """ diff --git a/features/json_format.feature b/features/json_format.feature new file mode 100644 index 0000000..ae231fb --- /dev/null +++ b/features/json_format.feature @@ -0,0 +1,43 @@ +Feature: Creating BOM using Json format + + Scenario: Using default output path + Given I use a fixture named "simple" + And I run `cyclonedx-ruby --path . --format json` + Then the output should contain: + """ + 5 gems were written to BOM located at ./bom.json + """ + And a file named "bom.json" should exist + And the generated Json BOM file "bom.json" matches "bom.json.expected" + + Scenario: Specifying the output path + Given I use a fixture named "simple" + And I run `cyclonedx-ruby --path . --format json --output bom/simple.bom.json` + Then the output should contain: + """ + 5 gems were written to BOM located at bom/simple.bom.json + """ + And a file named "bom/simple.bom.json" should exist + And the generated Json BOM file "bom/simple.bom.json" matches "bom.json.expected" + + Scenario: Verbose output + Given I use a fixture named "simple" + And I run `cyclonedx-ruby --path . --format json --verbose` + Then the output should match: + """ + I, \[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{6} #\d+\] INFO -- : Changing directory to Ruby project directory located at \. + I, \[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{6} #\d+\] INFO -- : BOM will be written to \./bom\.json + I, \[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{6} #\d+\] INFO -- : Parsing specs from \./Gemfile\.lock\.\.\. + I, \[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{6} #\d+\] INFO -- : Specs successfully parsed! + I, \[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{6} #\d+\] INFO -- : activesupport:7\.0\.4\.3 gem added + I, \[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{6} #\d+\] INFO -- : concurrent-ruby:1\.2\.2 gem added + I, \[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{6} #\d+\] INFO -- : i18n:1\.12\.0 gem added + I, \[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{6} #\d+\] INFO -- : minitest:5\.18\.0 gem added + I, \[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{6} #\d+\] INFO -- : tzinfo:2\.0\.6 gem added + I, \[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{6} #\d+\] INFO -- : Changing directory to the original working directory located at .*/tmp/aruba/simple + I, \[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{6} #\d+\] INFO -- : Writing BOM to \./bom\.json\.\.\. + I, \[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{6} #\d+\] INFO -- : 5 gems were written to BOM located at \./bom\.json + """ + And a file named "bom.json" should exist + And the generated Json BOM file "bom.json" matches "bom.json.expected" + diff --git a/features/step_definitions/.gitkeep b/features/step_definitions/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/features/step_definitions/json_bom_matching.rb b/features/step_definitions/json_bom_matching.rb new file mode 100644 index 0000000..0a81ea1 --- /dev/null +++ b/features/step_definitions/json_bom_matching.rb @@ -0,0 +1,11 @@ +Then('the generated Json BOM file {string} matches {string}') do |generated_file, expected_file| + generated_file_contents = File.read(expand_path(generated_file)) + expected_file_contents = File.read(expand_path(expected_file)) + + serial_number_matcher = /\"serialNumber\": \"urn:uuid:[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}\"/ + normalized_serial_number = '"serialNumber": "urn:uuid:00000000-0000-0000-0000-000000000000"' + normalized_generated_file_contents = generated_file_contents.gsub(serial_number_matcher, normalized_serial_number) + normalized_expected_file_contents = expected_file_contents.gsub(serial_number_matcher, normalized_serial_number) + + expect(normalized_expected_file_contents).to eq(normalized_generated_file_contents) +end diff --git a/features/step_definitions/xml_bom_matching.rb b/features/step_definitions/xml_bom_matching.rb new file mode 100644 index 0000000..004c9b2 --- /dev/null +++ b/features/step_definitions/xml_bom_matching.rb @@ -0,0 +1,11 @@ +Then('the generated XML BOM file {string} matches {string}') do |generated_file, expected_file| + generated_file_contents = File.read(expand_path(generated_file)) + expected_file_contents = File.read(expand_path(expected_file)) + + serial_number_matcher = /serialNumber=\"urn:uuid:[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}\"/ + normalized_serial_number = 'serialNumber="urn:uuid:00000000-0000-0000-0000-000000000000"' + normalized_generated_file_contents = generated_file_contents.gsub(serial_number_matcher, normalized_serial_number) + normalized_expected_file_contents = expected_file_contents.gsub(serial_number_matcher, normalized_serial_number) + + expect(normalized_expected_file_contents).to eq(normalized_generated_file_contents) +end diff --git a/features/support/env.rb b/features/support/env.rb new file mode 100644 index 0000000..331ff6a --- /dev/null +++ b/features/support/env.rb @@ -0,0 +1,33 @@ +# Based on https://github.com/cucumber/aruba/blob/3b1a6cea6e3ba55370c3396eef0a955aeb40f287/features/support/env.rb +# Licensed under MIT - https://github.com/cucumber/aruba/blob/3b1a6cea6e3ba55370c3396eef0a955aeb40f287/LICENSE + +$LOAD_PATH.unshift File.expand_path('../../lib', __dir__) + +# Has to be the first file required so that all other files show coverage information +require_relative 'simplecov_support' unless RUBY_PLATFORM.include?('java') + +require 'fileutils' +require 'pathname' + +require 'aruba/cucumber' +require 'rspec/expectations' + +Around do |test_case, block| + command_name = "#{test_case.location.file}:#{test_case.location.line} # #{test_case.name}" + + # Used in simplecov_setup so that each scenario has a different name and + # their coverage results are merged instead of overwriting each other as + # 'Cucumber Features' + set_environment_variable 'SIMPLECOV_COMMAND_NAME', command_name.to_s + + simplecov_setup_pathname = + Pathname.new(__FILE__).expand_path.parent.to_s + + # set environment variable so child processes will merge their coverage data + # with parent process's coverage data. + prepend_environment_variable 'RUBYOPT', "-I#{simplecov_setup_pathname} -rsimplecov_support " + + with_environment do + block.call + end +end diff --git a/features/support/simplecov_support.rb b/features/support/simplecov_support.rb new file mode 100644 index 0000000..b3a91a3 --- /dev/null +++ b/features/support/simplecov_support.rb @@ -0,0 +1,15 @@ +# Copied from https://github.com/cucumber/aruba/blob/3b1a6cea6e3ba55370c3396eef0a955aeb40f287/features/support/simplecov_setup.rb +# Licensed under MIT - https://github.com/cucumber/aruba/blob/3b1a6cea6e3ba55370c3396eef0a955aeb40f287/LICENSE + +# @note this file is loaded in env.rb to setup simplecov using RUBYOPTs for +# child processes and @in-process +unless RUBY_PLATFORM.include?('java') + require 'simplecov' + root = File.expand_path('../..', __dir__) + command_name = ENV['SIMPLECOV_COMMAND_NAME'] || 'Cucumber Features' + SimpleCov.command_name(command_name) + SimpleCov.root(root) + + # Run simplecov by default + SimpleCov.start unless ENV.key? 'ARUBA_NO_COVERAGE' +end diff --git a/features/xml_format.feature b/features/xml_format.feature new file mode 100644 index 0000000..2c66880 --- /dev/null +++ b/features/xml_format.feature @@ -0,0 +1,42 @@ +Feature: Creating BOM using XML format + + Scenario: Using default output path + Given I use a fixture named "simple" + And I run `cyclonedx-ruby --path . --format xml` + Then the output should contain: + """ + 5 gems were written to BOM located at ./bom.xml + """ + And a file named "bom.xml" should exist + And the generated XML BOM file "bom.xml" matches "bom.xml.expected" + + Scenario: Specifying the output path + Given I use a fixture named "simple" + And I run `cyclonedx-ruby --path . --format xml --output bom/simple.bom.xml` + Then the output should contain: + """ + 5 gems were written to BOM located at bom/simple.bom.xml + """ + And a file named "bom/simple.bom.xml" should exist + And the generated XML BOM file "bom/simple.bom.xml" matches "bom.xml.expected" + + Scenario: Verbose output + Given I use a fixture named "simple" + And I run `cyclonedx-ruby --path . --format xml --verbose` + Then the output should match: + """ + I, \[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{6} #\d+\] INFO -- : Changing directory to Ruby project directory located at \. + I, \[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{6} #\d+\] INFO -- : BOM will be written to \./bom\.xml + I, \[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{6} #\d+\] INFO -- : Parsing specs from \./Gemfile\.lock\.\.\. + I, \[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{6} #\d+\] INFO -- : Specs successfully parsed! + I, \[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{6} #\d+\] INFO -- : activesupport:7\.0\.4\.3 gem added + I, \[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{6} #\d+\] INFO -- : concurrent-ruby:1\.2\.2 gem added + I, \[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{6} #\d+\] INFO -- : i18n:1\.12\.0 gem added + I, \[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{6} #\d+\] INFO -- : minitest:5\.18\.0 gem added + I, \[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{6} #\d+\] INFO -- : tzinfo:2\.0\.6 gem added + I, \[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{6} #\d+\] INFO -- : Changing directory to the original working directory located at .*/tmp/aruba/simple + I, \[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{6} #\d+\] INFO -- : Writing BOM to \./bom\.xml\.\.\. + I, \[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{6} #\d+\] INFO -- : 5 gems were written to BOM located at \./bom\.xml + """ + And a file named "bom.xml" should exist + And the generated XML BOM file "bom.xml" matches "bom.xml.expected" diff --git a/spec/bom_component_spec.rb b/spec/bom_component_spec.rb new file mode 100644 index 0000000..e02f4d5 --- /dev/null +++ b/spec/bom_component_spec.rb @@ -0,0 +1,64 @@ +require 'spec_helper' +require 'bom_component' + +RSpec.shared_examples "a valid hash_val result for gem" do + it { expect(result.count).to eq(1) } + it { expect(result[0][:type]).to eq('library') } + it { expect(result[0][:name]).to eq(gem.name) } + it { expect(result[0][:version]).to eq(gem.version) } + it { expect(result[0][:description]).to eq(gem.description) } + it { expect(result[0][:purl]).to eq(gem.purl) } + it { expect(result[0][:hashes].count).to eq(1) } + it { expect(result[0][:hashes][0][:alg]).to eq('SHA-256') } + it { expect(result[0][:hashes][0][:content]).to eq(gem.hash) } +end + +RSpec.describe BomComponent do + context '#hash_val' do + let(:base_gem) do + OpenStruct.new( + name: 'Sample', + version: '1.0.0', + description: 'Sample description', + hash: '1f809ab336c437d894df9934a9fc9ffd2ea09b535dfa4e3f75db078531c260c8', + purl: 'pkg:gem/sample@1.0.0' + ) + end + + let(:gem) do + base_gem + end + + subject(:result) { BomComponent.new(gem).hash_val } + + context 'with a gem without a license' do + include_examples 'a valid hash_val result for gem' + end + + context 'with a gem that has a license_id' do + let(:gem) do + base_gem.tap do |value| + value.license_id = 'License ID' + end + end + + include_examples 'a valid hash_val result for gem' + + it { expect(result[0][:licenses].count).to eq(1) } + it { expect(result[0][:licenses][0][:license][:id]).to eq(gem.license_id) } + end + + context 'with a gem that has a license_name' do + let(:gem) do + base_gem.tap do |value| + value.license_name = 'License Name' + end + end + + include_examples 'a valid hash_val result for gem' + + it { expect(result[0][:licenses].count).to eq(1) } + it { expect(result[0][:licenses][0][:license][:name]).to eq(gem.license_name) } + end + end +end diff --git a/spec/bom_helpers_spec.rb b/spec/bom_helpers_spec.rb new file mode 100644 index 0000000..75adec2 --- /dev/null +++ b/spec/bom_helpers_spec.rb @@ -0,0 +1,10 @@ +require 'spec_helper' +require 'bom_helpers' + +RSpec.describe 'helper methods' do + context '#purl' do + it 'builds a purl' do + expect(purl('activesupport', '7.0.1')).to eq("pkg:gem/activesupport@7.0.1") + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..24b4245 --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,16 @@ +# Copied from https://github.com/cucumber/aruba/blob/3b1a6cea6e3ba55370c3396eef0a955aeb40f287/spec/spec_helper.rb +# Licensed under MIT - https://github.com/cucumber/aruba/blob/3b1a6cea6e3ba55370c3396eef0a955aeb40f287/LICENSE + +$LOAD_PATH << File.expand_path('../lib', __dir__) + +unless RUBY_PLATFORM.include?('java') + require 'simplecov' + SimpleCov.command_name 'RSpec' + + # Run simplecov by default + SimpleCov.start unless ENV.key? 'ARUBA_NO_COVERAGE' +end + +# Loading support files +Dir.glob(File.expand_path('support/*.rb', __dir__)).sort.each { |f| require_relative f } +Dir.glob(File.expand_path('support/**/*.rb', __dir__)).sort.each { |f| require_relative f }