From 7533f19a82798b8e02eac3dc588b0928a8b20181 Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Sat, 25 Oct 2025 16:25:34 -0600 Subject: [PATCH 1/3] =?UTF-8?q?=E2=9C=A8=20Core=20v1.7=20Enablement?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add spec version selection end-to-end with a new --spec-version flag (default 1.7). - Update JSON and XML outputs to honor the selected spec version. - Update fixtures, help text, tests, and docs. Files: - lib/bom_helpers.rb: - Added SUPPORTED_SPEC_VERSIONS, cyclonedx_xml_namespace helper. build_bom now accepts spec_version and routes to: - build_json_bom(gems, spec_version) sets specVersion to the provided version. - build_bom_xml(gems, spec_version) sets xmlns to http://cyclonedx.org/schema/bom/. - lib/bom_builder.rb: - Added --spec-version with validation; default is 1.7. - Pass @spec_version into build_bom(@gems, @bom_output_format, @spec_version). Signed-off-by: Peter H. Boling --- Gemfile.lock | 2 +- README.md | 20 ++++++-- cyclonedx-ruby.gemspec | 2 +- features/fixtures/simple/bom.json.expected | 4 +- features/fixtures/simple/bom.xml.expected | 2 +- features/help.feature | 1 + features/json_format.feature | 1 - .../step_definitions/json_bom_matching.rb | 4 +- lib/cyclonedx/bom_builder.rb | 47 ++++++++++++++----- lib/cyclonedx/bom_helpers.rb | 18 ++++--- 10 files changed, 71 insertions(+), 30 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 1fe1cca..fcb1cd3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -184,7 +184,7 @@ PLATFORMS DEPENDENCIES aruba (~> 2.2) - cucumber (~> 10.0) + cucumber (~> 10.1, >= 10.1.1) cyclonedx-ruby! rake (~> 13) rspec (~> 3.12) diff --git a/README.md b/README.md index 290f3c9..a96bc50 100644 --- a/README.md +++ b/README.md @@ -30,14 +30,29 @@ cyclonedx-ruby [options] `-v, --[no-]verbose` Run verbosely `-p, --path path` Path to Ruby project directory - `-f, --format` Bom output format + `-o, --output bom_file_path` Path to output the bom file + `-f, --format bom_output_format` Output format for bom. Supported: xml (default), json + `-s, --spec-version version` CycloneDX spec version to target (default: 1.7). Supported: 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7 `-h, --help` Show help message **Output:** bom.xml or bom.json file in project directory -#### Example +- By default, outputs conform to CycloneDX spec version 1.7. +- To generate an older spec version, use `--spec-version`. + +#### Examples ```bash +# Default (XML, CycloneDX 1.7) cyclonedx-ruby -p /path/to/ruby/project + +# JSON at CycloneDX 1.7 +cyclonedx-ruby -p /path/to/ruby/project -f json + +# XML at CycloneDX 1.3 +cyclonedx-ruby -p /path/to/ruby/project -s 1.3 + +# JSON at CycloneDX 1.2 to a custom path +cyclonedx-ruby -p /path/to/ruby/project -f json -s 1.2 -o bom/out.json ``` @@ -49,4 +64,3 @@ CycloneDX Ruby Gem is Copyright (c) OWASP Foundation. All Rights Reserved. Permission to modify and redistribute is granted under the terms of the Apache 2.0 license. See the [LICENSE] file for the full license. [License]: https://github.com/CycloneDX/cyclonedx-ruby-gem/blob/master/LICENSE - diff --git a/cyclonedx-ruby.gemspec b/cyclonedx-ruby.gemspec index 1395cf4..a299ad2 100644 --- a/cyclonedx-ruby.gemspec +++ b/cyclonedx-ruby.gemspec @@ -61,7 +61,7 @@ Gem::Specification.new do |spec| spec.add_dependency('activesupport', '~> 7.0') spec.add_development_dependency 'rake', '~> 13' spec.add_development_dependency 'rspec', '~> 3.12' - spec.add_development_dependency 'cucumber', '~> 10.0' + spec.add_development_dependency 'cucumber', '~> 10.1', '>= 10.1.1' spec.add_development_dependency 'aruba', '~> 2.2' spec.add_development_dependency 'simplecov', '~> 0.22.0' spec.add_development_dependency 'rubocop', '~> 1.54' diff --git a/features/fixtures/simple/bom.json.expected b/features/fixtures/simple/bom.json.expected index d9d7dce..39b6168 100644 --- a/features/fixtures/simple/bom.json.expected +++ b/features/fixtures/simple/bom.json.expected @@ -1,6 +1,6 @@ { "bomFormat": "CycloneDX", - "specVersion": "1.1", + "specVersion": "1.7", "serialNumber": "urn:uuid:d498cdc2-5494-4031-b37d-ff3d10d336bf", "version": 1, "components": [ @@ -105,4 +105,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/features/fixtures/simple/bom.xml.expected b/features/fixtures/simple/bom.xml.expected index b379102..9cd91a6 100644 --- a/features/fixtures/simple/bom.xml.expected +++ b/features/fixtures/simple/bom.xml.expected @@ -1,5 +1,5 @@ - + activesupport diff --git a/features/help.feature b/features/help.feature index f2ee863..31fda67 100644 --- a/features/help.feature +++ b/features/help.feature @@ -12,5 +12,6 @@ Scenario: Generate help on demand -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. + -s, --spec-version version (Optional) CycloneDX spec version to target (default: 1.7). Supported: 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7 -h, --help Show help message """ diff --git a/features/json_format.feature b/features/json_format.feature index ae231fb..7f24c00 100644 --- a/features/json_format.feature +++ b/features/json_format.feature @@ -40,4 +40,3 @@ Feature: Creating BOM using Json format """ 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/json_bom_matching.rb b/features/step_definitions/json_bom_matching.rb index d012084..65170ff 100644 --- a/features/step_definitions/json_bom_matching.rb +++ b/features/step_definitions/json_bom_matching.rb @@ -6,8 +6,8 @@ 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) + normalized_generated_file_contents = generated_file_contents.gsub(serial_number_matcher, normalized_serial_number).rstrip + normalized_expected_file_contents = expected_file_contents.gsub(serial_number_matcher, normalized_serial_number).rstrip expect(normalized_expected_file_contents).to eq(normalized_generated_file_contents) end diff --git a/lib/cyclonedx/bom_builder.rb b/lib/cyclonedx/bom_builder.rb index 17c2a25..727e8dd 100644 --- a/lib/cyclonedx/bom_builder.rb +++ b/lib/cyclonedx/bom_builder.rb @@ -3,6 +3,7 @@ module Cyclonedx class BomBuilder SUPPORTED_BOM_FORMATS = %w[xml json] + SUPPORTED_SPEC_VERSIONS = %w[1.1 1.2 1.3 1.4 1.5 1.6 1.7] extend Cyclonedx::BomHelpers @@ -10,13 +11,13 @@ def self.build(path) original_working_directory = Dir.pwd setup(path) specs_list - bom = build_bom(@gems, @bom_output_format) + bom = build_bom(@gems, @bom_output_format, @spec_version) begin @logger.info("Changing directory to the original working directory located at #{original_working_directory}") Dir.chdir original_working_directory rescue StandardError => e - @logger.error("Unable to change directory the original working directory located at #{original_working_directory}. #{e.message}: #{e.backtrace.join('\n')}") + @logger.error("Unable to change directory the original working directory located at #{original_working_directory}. #{e.message}: #{Array(e.backtrace).join("\n")}") abort end @@ -24,7 +25,7 @@ def self.build(path) begin FileUtils.mkdir_p(bom_directory) unless File.directory?(bom_directory) rescue StandardError => e - @logger.error("Unable to create the directory to hold the BOM output at #{bom_directory}. #{e.message}: #{e.backtrace.join('\n')}") + @logger.error("Unable to create the directory to hold the BOM output at #{bom_directory}. #{e.message}: #{Array(e.backtrace).join("\n")}") abort end @@ -38,7 +39,7 @@ def self.build(path) puts "#{@gems.size} gems were written to BOM located at #{@bom_file_path}" end rescue StandardError => e - @logger.error("Unable to write BOM to #{@bom_file_path}. #{e.message}: #{e.backtrace.join('\n')}") + @logger.error("Unable to write BOM to #{@bom_file_path}. #{e.message}: #{Array(e.backtrace).join("\n")}") abort end end @@ -51,8 +52,8 @@ def self.setup(_path) opts.on('-v', '--[no-]verbose', 'Run verbosely') do |v| @options[:verbose] = v end - opts.on('-p', '--path path', '(Required) Path to Ruby project directory') do |path| - @options[:path] = path + opts.on('-p', '--path path', '(Required) Path to Ruby project directory') do |proj_path_opt| + @options[:path] = proj_path_opt end opts.on('-o', '--output bom_file_path', '(Optional) Path to output the bom.xml file to') do |bom_file_path| @options[:bom_file_path] = bom_file_path @@ -60,12 +61,18 @@ def self.setup(_path) opts.on('-f', '--format bom_output_format', '(Optional) Output format for bom. Currently support xml (default) and json.') do |bom_output_format| @options[:bom_output_format] = bom_output_format end + opts.on('-s', '--spec-version version', '(Optional) CycloneDX spec version to target (default: 1.7). Supported: 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7') do |spec_version| + @options[:spec_version] = spec_version + end opts.on_tail('-h', '--help', 'Show help message') do puts opts exit end end.parse! + # Allow passing the path as a positional arg via exe wrapper + @options[:path] ||= path + @logger = Logger.new($stdout) @logger.level = if @options[:verbose] Logger::INFO @@ -89,11 +96,15 @@ def self.setup(_path) abort end + # Normalize to an absolute project path to avoid relative path issues later + @project_path = File.expand_path(@options[:path]) + @provided_path = @options[:path] + begin - @logger.info("Changing directory to Ruby project directory located at #{@options[:path]}") - Dir.chdir @options[:path] + @logger.info("Changing directory to Ruby project directory located at #{@provided_path}") + Dir.chdir @project_path rescue StandardError => e - @logger.error("Unable to change directory to Ruby project directory located at #{@options[:path]}. #{e.message}: #{e.backtrace.join('\n')}") + @logger.error("Unable to change directory to Ruby project directory located at #{@provided_path}. #{e.message}: #{Array(e.backtrace).join("\n")}") abort end @@ -106,6 +117,15 @@ def self.setup(_path) abort end + # Spec version selection + requested_spec = @options[:spec_version] || '1.7' + if SUPPORTED_SPEC_VERSIONS.include?(requested_spec) + @spec_version = requested_spec + else + @logger.error("Unrecognized CycloneDX spec version '#{requested_spec}'. Please choose one of #{SUPPORTED_SPEC_VERSIONS}") + abort + end + @bom_file_path = if @options[:bom_file_path].nil? "./bom.#{@bom_output_format}" else @@ -115,13 +135,16 @@ def self.setup(_path) @logger.info("BOM will be written to #{@bom_file_path}") begin - gemfile_path = "#{@options[:path]}/Gemfile.lock" - @logger.info("Parsing specs from #{gemfile_path}...") + # Use absolute path so it's correct regardless of current working directory + gemfile_path = File.join(@project_path, 'Gemfile.lock') + # Compute display path for logs: './Gemfile.lock' when provided path is '.', else '/Gemfile.lock' + display_gemfile_path = (@provided_path == '.' ? './Gemfile.lock' : File.join(@provided_path, 'Gemfile.lock')) + @logger.info("Parsing specs from #{display_gemfile_path}...") gemfile_contents = File.read(gemfile_path) @specs = Bundler::LockfileParser.new(gemfile_contents).specs @logger.info('Specs successfully parsed!') rescue StandardError => e - @logger.error("Unable to parse specs from #{gemfile_path}. #{e.message}: #{e.backtrace.join('\n')}") + @logger.error("Unable to parse specs from #{gemfile_path}. #{e.message}: #{Array(e.backtrace).join("\n")}") abort end end diff --git a/lib/cyclonedx/bom_helpers.rb b/lib/cyclonedx/bom_helpers.rb index 3985f51..ab07eaf 100644 --- a/lib/cyclonedx/bom_helpers.rb +++ b/lib/cyclonedx/bom_helpers.rb @@ -29,6 +29,10 @@ module Cyclonedx module BomHelpers module_function + def cyclonedx_xml_namespace(spec_version) + "http://cyclonedx.org/schema/bom/#{spec_version}" + end + def purl(name, version) "pkg:gem/#{name}@#{version}" end @@ -37,18 +41,18 @@ def random_urn_uuid "urn:uuid:#{SecureRandom.uuid}" end - def build_bom(gems, format) + def build_bom(gems, format, spec_version) if format == 'json' - build_json_bom(gems) + build_json_bom(gems, spec_version) else - build_bom_xml(gems) + build_bom_xml(gems, spec_version) end end - def build_json_bom(gems) + def build_json_bom(gems, spec_version) bom_hash = { bomFormat: 'CycloneDX', - specVersion: '1.1', + specVersion: spec_version, serialNumber: random_urn_uuid, version: 1, components: [] @@ -61,9 +65,9 @@ def build_json_bom(gems) JSON.pretty_generate(bom_hash) end - def build_bom_xml(gems) + def build_bom_xml(gems, spec_version) builder = Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml| - attributes = { 'xmlns' => 'http://cyclonedx.org/schema/bom/1.1', 'version' => '1', 'serialNumber' => random_urn_uuid } + attributes = { 'xmlns' => cyclonedx_xml_namespace(spec_version), 'version' => '1', 'serialNumber' => random_urn_uuid } xml.bom(attributes) do xml.components do gems.each do |gem| From deb3db3be5f0c410b22ac5662fe8549b73a980ca Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Sat, 25 Oct 2025 21:49:35 -0600 Subject: [PATCH 2/3] =?UTF-8?q?=F0=9F=99=88=20Ignore=20.idea?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Peter H. Boling --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 9aec75a..6fc92f7 100644 --- a/.gitignore +++ b/.gitignore @@ -45,6 +45,7 @@ Appraisal.*.gemfile.lock # Editors *~ +/.idea/ # vendor /vendor/ From d8900564d973c58c1f348daa9ed8ed298a02e60a Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Sat, 25 Oct 2025 23:15:19 -0600 Subject: [PATCH 3/3] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Fix=20typo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Peter H. Boling --- lib/cyclonedx/bom_builder.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/cyclonedx/bom_builder.rb b/lib/cyclonedx/bom_builder.rb index 727e8dd..4d7a4b5 100644 --- a/lib/cyclonedx/bom_builder.rb +++ b/lib/cyclonedx/bom_builder.rb @@ -17,7 +17,7 @@ def self.build(path) @logger.info("Changing directory to the original working directory located at #{original_working_directory}") Dir.chdir original_working_directory rescue StandardError => e - @logger.error("Unable to change directory the original working directory located at #{original_working_directory}. #{e.message}: #{Array(e.backtrace).join("\n")}") + @logger.error("Unable to change to the original working directory located at #{original_working_directory}. #{e.message}: #{Array(e.backtrace).join("\n")}") abort end @@ -44,7 +44,7 @@ def self.build(path) end end - def self.setup(_path) + def self.setup(path) @options = {} OptionParser.new do |opts| opts.banner = 'Usage: cyclonedx-ruby [options]'