Skip to content

Commit c4e441c

Browse files
committed
✨ Core v1.7 Enablement
- 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/<version>.</version> - 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 <[email protected]>
1 parent ae47421 commit c4e441c

File tree

10 files changed

+71
-30
lines changed

10 files changed

+71
-30
lines changed

Gemfile.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ PLATFORMS
186186

187187
DEPENDENCIES
188188
aruba (~> 2.2)
189-
cucumber (~> 10.0)
189+
cucumber (~> 10.1, >= 10.1.1)
190190
cyclonedx-ruby!
191191
rake (~> 13)
192192
rspec (~> 3.12)

README.md

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,29 @@ cyclonedx-ruby [options]
2929

3030
`-v, --[no-]verbose` Run verbosely
3131
`-p, --path path` Path to Ruby project directory
32-
`-f, --format` Bom output format
32+
`-o, --output bom_file_path` Path to output the bom file
33+
`-f, --format bom_output_format` Output format for bom. Supported: xml (default), json
34+
`-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
3335
`-h, --help` Show help message
3436

3537
**Output:** bom.xml or bom.json file in project directory
3638

37-
#### Example
39+
- By default, outputs conform to CycloneDX spec version 1.7.
40+
- To generate an older spec version, use `--spec-version`.
41+
42+
#### Examples
3843
```bash
44+
# Default (XML, CycloneDX 1.7)
3945
cyclonedx-ruby -p /path/to/ruby/project
46+
47+
# JSON at CycloneDX 1.7
48+
cyclonedx-ruby -p /path/to/ruby/project -f json
49+
50+
# XML at CycloneDX 1.3
51+
cyclonedx-ruby -p /path/to/ruby/project -s 1.3
52+
53+
# JSON at CycloneDX 1.2 to a custom path
54+
cyclonedx-ruby -p /path/to/ruby/project -f json -s 1.2 -o bom/out.json
4055
```
4156

4257

@@ -48,4 +63,3 @@ CycloneDX Ruby Gem is Copyright (c) OWASP Foundation. All Rights Reserved.
4863
Permission to modify and redistribute is granted under the terms of the Apache 2.0 license. See the [LICENSE] file for the full license.
4964

5065
[License]: https://github.com/CycloneDX/cyclonedx-ruby-gem/blob/master/LICENSE
51-

cyclonedx-ruby.gemspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ Gem::Specification.new do |spec|
6464
spec.add_dependency('activesupport', '~> 7.0')
6565
spec.add_development_dependency 'rake', '~> 13'
6666
spec.add_development_dependency 'rspec', '~> 3.12'
67-
spec.add_development_dependency 'cucumber', '~> 10.0'
67+
spec.add_development_dependency 'cucumber', '~> 10.1', '>= 10.1.1'
6868
spec.add_development_dependency 'aruba', '~> 2.2'
6969
spec.add_development_dependency 'simplecov', '~> 0.22.0'
7070
spec.add_development_dependency 'rubocop', '~> 1.54'

features/fixtures/simple/bom.json.expected

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"bomFormat": "CycloneDX",
3-
"specVersion": "1.1",
3+
"specVersion": "1.7",
44
"serialNumber": "urn:uuid:d498cdc2-5494-4031-b37d-ff3d10d336bf",
55
"version": 1,
66
"components": [
@@ -105,4 +105,4 @@
105105
]
106106
}
107107
]
108-
}
108+
}

features/fixtures/simple/bom.xml.expected

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<?xml version="1.0" encoding="UTF-8"?>
2-
<bom xmlns="http://cyclonedx.org/schema/bom/1.1" version="1" serialNumber="urn:uuid:ffc51349-2d7d-408e-b2c1-3e3f220e6d2f">
2+
<bom xmlns="http://cyclonedx.org/schema/bom/1.7" version="1" serialNumber="urn:uuid:ffc51349-2d7d-408e-b2c1-3e3f220e6d2f">
33
<components>
44
<component type="library">
55
<name>activesupport</name>

features/help.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,6 @@ Scenario: Generate help on demand
1212
-p, --path path (Required) Path to Ruby project directory
1313
-o, --output bom_file_path (Optional) Path to output the bom.xml file to
1414
-f, --format bom_output_format (Optional) Output format for bom. Currently support xml (default) and json.
15+
-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
1516
-h, --help Show help message
1617
"""

features/json_format.feature

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,3 @@ Feature: Creating BOM using Json format
4040
"""
4141
And a file named "bom.json" should exist
4242
And the generated Json BOM file "bom.json" matches "bom.json.expected"
43-

features/step_definitions/json_bom_matching.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44

55
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}\"/
66
normalized_serial_number = '"serialNumber": "urn:uuid:00000000-0000-0000-0000-000000000000"'
7-
normalized_generated_file_contents = generated_file_contents.gsub(serial_number_matcher, normalized_serial_number)
8-
normalized_expected_file_contents = expected_file_contents.gsub(serial_number_matcher, normalized_serial_number)
7+
normalized_generated_file_contents = generated_file_contents.gsub(serial_number_matcher, normalized_serial_number).rstrip
8+
normalized_expected_file_contents = expected_file_contents.gsub(serial_number_matcher, normalized_serial_number).rstrip
99

1010
expect(normalized_expected_file_contents).to eq(normalized_generated_file_contents)
1111
end

lib/cyclonedx/bom_builder.rb

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,29 @@
11
module Cyclonedx
22
class BomBuilder
33
SUPPORTED_BOM_FORMATS = %w[xml json]
4+
SUPPORTED_SPEC_VERSIONS = %w[1.1 1.2 1.3 1.4 1.5 1.6 1.7]
45

56
extend Cyclonedx::BomHelpers
67

78
def self.build(path)
89
original_working_directory = Dir.pwd
910
setup(path)
1011
specs_list
11-
bom = build_bom(@gems, @bom_output_format)
12+
bom = build_bom(@gems, @bom_output_format, @spec_version)
1213

1314
begin
1415
@logger.info("Changing directory to the original working directory located at #{original_working_directory}")
1516
Dir.chdir original_working_directory
1617
rescue StandardError => e
17-
@logger.error("Unable to change directory the original working directory located at #{original_working_directory}. #{e.message}: #{e.backtrace.join('\n')}")
18+
@logger.error("Unable to change directory the original working directory located at #{original_working_directory}. #{e.message}: #{Array(e.backtrace).join("\n")}")
1819
abort
1920
end
2021

2122
bom_directory = File.dirname(@bom_file_path)
2223
begin
2324
FileUtils.mkdir_p(bom_directory) unless File.directory?(bom_directory)
2425
rescue StandardError => e
25-
@logger.error("Unable to create the directory to hold the BOM output at #{@bom_directory}. #{e.message}: #{e.backtrace.join('\n')}")
26+
@logger.error("Unable to create the directory to hold the BOM output at #{bom_directory}. #{e.message}: #{Array(e.backtrace).join("\n")}")
2627
abort
2728
end
2829

@@ -36,7 +37,7 @@ def self.build(path)
3637
puts "#{@gems.size} gems were written to BOM located at #{@bom_file_path}"
3738
end
3839
rescue StandardError => e
39-
@logger.error("Unable to write BOM to #{@bom_file_path}. #{e.message}: #{e.backtrace.join('\n')}")
40+
@logger.error("Unable to write BOM to #{@bom_file_path}. #{e.message}: #{Array(e.backtrace).join("\n")}")
4041
abort
4142
end
4243
end
@@ -49,21 +50,27 @@ def self.setup(path)
4950
opts.on('-v', '--[no-]verbose', 'Run verbosely') do |v|
5051
@options[:verbose] = v
5152
end
52-
opts.on('-p', '--path path', '(Required) Path to Ruby project directory') do |path|
53-
@options[:path] = path
53+
opts.on('-p', '--path path', '(Required) Path to Ruby project directory') do |proj_path_opt|
54+
@options[:path] = proj_path_opt
5455
end
5556
opts.on('-o', '--output bom_file_path', '(Optional) Path to output the bom.xml file to') do |bom_file_path|
5657
@options[:bom_file_path] = bom_file_path
5758
end
5859
opts.on('-f', '--format bom_output_format', '(Optional) Output format for bom. Currently support xml (default) and json.') do |bom_output_format|
5960
@options[:bom_output_format] = bom_output_format
6061
end
62+
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|
63+
@options[:spec_version] = spec_version
64+
end
6165
opts.on_tail('-h', '--help', 'Show help message') do
6266
puts opts
6367
exit
6468
end
6569
end.parse!
6670

71+
# Allow passing the path as a positional arg via exe wrapper
72+
@options[:path] ||= path
73+
6774
@logger = Logger.new($stdout)
6875
@logger.level = if @options[:verbose]
6976
Logger::INFO
@@ -87,11 +94,15 @@ def self.setup(path)
8794
abort
8895
end
8996

97+
# Normalize to an absolute project path to avoid relative path issues later
98+
@project_path = File.expand_path(@options[:path])
99+
@provided_path = @options[:path]
100+
90101
begin
91-
@logger.info("Changing directory to Ruby project directory located at #{@options[:path]}")
92-
Dir.chdir @options[:path]
102+
@logger.info("Changing directory to Ruby project directory located at #{@provided_path}")
103+
Dir.chdir @project_path
93104
rescue StandardError => e
94-
@logger.error("Unable to change directory to Ruby project directory located at #{@options[:path]}. #{e.message}: #{e.backtrace.join('\n')}")
105+
@logger.error("Unable to change directory to Ruby project directory located at #{@provided_path}. #{e.message}: #{Array(e.backtrace).join("\n")}")
95106
abort
96107
end
97108

@@ -104,6 +115,15 @@ def self.setup(path)
104115
abort
105116
end
106117

118+
# Spec version selection
119+
requested_spec = @options[:spec_version] || '1.7'
120+
if SUPPORTED_SPEC_VERSIONS.include?(requested_spec)
121+
@spec_version = requested_spec
122+
else
123+
@logger.error("Unrecognized CycloneDX spec version '#{requested_spec}'. Please choose one of #{SUPPORTED_SPEC_VERSIONS}")
124+
abort
125+
end
126+
107127
@bom_file_path = if @options[:bom_file_path].nil?
108128
"./bom.#{@bom_output_format}"
109129
else
@@ -113,13 +133,16 @@ def self.setup(path)
113133
@logger.info("BOM will be written to #{@bom_file_path}")
114134

115135
begin
116-
gemfile_path = "#{@options[:path]}/Gemfile.lock"
117-
@logger.info("Parsing specs from #{gemfile_path}...")
136+
# Use absolute path so it's correct regardless of current working directory
137+
gemfile_path = File.join(@project_path, 'Gemfile.lock')
138+
# Compute display path for logs: './Gemfile.lock' when provided path is '.', else '<provided>/Gemfile.lock'
139+
display_gemfile_path = (@provided_path == '.' ? './Gemfile.lock' : File.join(@provided_path, 'Gemfile.lock'))
140+
@logger.info("Parsing specs from #{display_gemfile_path}...")
118141
gemfile_contents = File.read(gemfile_path)
119142
@specs = Bundler::LockfileParser.new(gemfile_contents).specs
120143
@logger.info('Specs successfully parsed!')
121144
rescue StandardError => e
122-
@logger.error("Unable to parse specs from #{gemfile_path}. #{e.message}: #{e.backtrace.join('\n')}")
145+
@logger.error("Unable to parse specs from #{gemfile_path}. #{e.message}: #{Array(e.backtrace).join("\n")}")
123146
abort
124147
end
125148
end

lib/cyclonedx/bom_helpers.rb

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ module Cyclonedx
2828
module BomHelpers
2929
module_function
3030

31+
def cyclonedx_xml_namespace(spec_version)
32+
"http://cyclonedx.org/schema/bom/#{spec_version}"
33+
end
34+
3135
def purl(name, version)
3236
"pkg:gem/#{name}@#{version}"
3337
end
@@ -36,18 +40,18 @@ def random_urn_uuid
3640
"urn:uuid:#{SecureRandom.uuid}"
3741
end
3842

39-
def build_bom(gems, format)
43+
def build_bom(gems, format, spec_version)
4044
if format == 'json'
41-
build_json_bom(gems)
45+
build_json_bom(gems, spec_version)
4246
else
43-
build_bom_xml(gems)
47+
build_bom_xml(gems, spec_version)
4448
end
4549
end
4650

47-
def build_json_bom(gems)
51+
def build_json_bom(gems, spec_version)
4852
bom_hash = {
4953
"bomFormat": "CycloneDX",
50-
"specVersion": "1.1",
54+
"specVersion": spec_version,
5155
"serialNumber": random_urn_uuid,
5256
"version": 1,
5357
"components": []
@@ -60,9 +64,9 @@ def build_json_bom(gems)
6064
JSON.pretty_generate(bom_hash)
6165
end
6266

63-
def build_bom_xml(gems)
67+
def build_bom_xml(gems, spec_version)
6468
builder = Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml|
65-
attributes = { 'xmlns' => 'http://cyclonedx.org/schema/bom/1.1', 'version' => '1', 'serialNumber' => random_urn_uuid }
69+
attributes = { 'xmlns' => cyclonedx_xml_namespace(spec_version), 'version' => '1', 'serialNumber' => random_urn_uuid }
6670
xml.bom(attributes) do
6771
xml.components do
6872
gems.each do |gem|

0 commit comments

Comments
 (0)