Skip to content

Commit dfba2d5

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 0421fd1 commit dfba2d5

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
@@ -6,8 +6,8 @@
66

77
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}\"/
88
normalized_serial_number = '"serialNumber": "urn:uuid:00000000-0000-0000-0000-000000000000"'
9-
normalized_generated_file_contents = generated_file_contents.gsub(serial_number_matcher, normalized_serial_number)
10-
normalized_expected_file_contents = expected_file_contents.gsub(serial_number_matcher, normalized_serial_number)
9+
normalized_generated_file_contents = generated_file_contents.gsub(serial_number_matcher, normalized_serial_number).rstrip
10+
normalized_expected_file_contents = expected_file_contents.gsub(serial_number_matcher, normalized_serial_number).rstrip
1111

1212
expect(normalized_expected_file_contents).to eq(normalized_generated_file_contents)
1313
end

lib/cyclonedx/bom_builder.rb

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

78
extend Cyclonedx::BomHelpers
89

910
def self.build(path)
1011
original_working_directory = Dir.pwd
1112
setup(path)
1213
specs_list
13-
bom = build_bom(@gems, @bom_output_format)
14+
bom = build_bom(@gems, @bom_output_format, @spec_version)
1415

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

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

@@ -38,7 +39,7 @@ def self.build(path)
3839
puts "#{@gems.size} gems were written to BOM located at #{@bom_file_path}"
3940
end
4041
rescue StandardError => e
41-
@logger.error("Unable to write BOM to #{@bom_file_path}. #{e.message}: #{e.backtrace.join('\n')}")
42+
@logger.error("Unable to write BOM to #{@bom_file_path}. #{e.message}: #{Array(e.backtrace).join("\n")}")
4243
abort
4344
end
4445
end
@@ -51,21 +52,27 @@ def self.setup(path)
5152
opts.on('-v', '--[no-]verbose', 'Run verbosely') do |v|
5253
@options[:verbose] = v
5354
end
54-
opts.on('-p', '--path path', '(Required) Path to Ruby project directory') do |path|
55-
@options[:path] = path
55+
opts.on('-p', '--path path', '(Required) Path to Ruby project directory') do |proj_path_opt|
56+
@options[:path] = proj_path_opt
5657
end
5758
opts.on('-o', '--output bom_file_path', '(Optional) Path to output the bom.xml file to') do |bom_file_path|
5859
@options[:bom_file_path] = bom_file_path
5960
end
6061
opts.on('-f', '--format bom_output_format', '(Optional) Output format for bom. Currently support xml (default) and json.') do |bom_output_format|
6162
@options[:bom_output_format] = bom_output_format
6263
end
64+
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|
65+
@options[:spec_version] = spec_version
66+
end
6367
opts.on_tail('-h', '--help', 'Show help message') do
6468
puts opts
6569
exit
6670
end
6771
end.parse!
6872

73+
# Allow passing the path as a positional arg via exe wrapper
74+
@options[:path] ||= path
75+
6976
@logger = Logger.new($stdout)
7077
@logger.level = if @options[:verbose]
7178
Logger::INFO
@@ -89,11 +96,15 @@ def self.setup(path)
8996
abort
9097
end
9198

99+
# Normalize to an absolute project path to avoid relative path issues later
100+
@project_path = File.expand_path(@options[:path])
101+
@provided_path = @options[:path]
102+
92103
begin
93-
@logger.info("Changing directory to Ruby project directory located at #{@options[:path]}")
94-
Dir.chdir @options[:path]
104+
@logger.info("Changing directory to Ruby project directory located at #{@provided_path}")
105+
Dir.chdir @project_path
95106
rescue StandardError => e
96-
@logger.error("Unable to change directory to Ruby project directory located at #{@options[:path]}. #{e.message}: #{e.backtrace.join('\n')}")
107+
@logger.error("Unable to change directory to Ruby project directory located at #{@provided_path}. #{e.message}: #{Array(e.backtrace).join("\n")}")
97108
abort
98109
end
99110

@@ -106,6 +117,15 @@ def self.setup(path)
106117
abort
107118
end
108119

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

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

lib/cyclonedx/bom_helpers.rb

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

33+
def cyclonedx_xml_namespace(spec_version)
34+
"http://cyclonedx.org/schema/bom/#{spec_version}"
35+
end
36+
3337
def purl(name, version)
3438
"pkg:gem/#{name}@#{version}"
3539
end
@@ -38,18 +42,18 @@ def random_urn_uuid
3842
"urn:uuid:#{SecureRandom.uuid}"
3943
end
4044

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

49-
def build_json_bom(gems)
53+
def build_json_bom(gems, spec_version)
5054
bom_hash = {
5155
"bomFormat": "CycloneDX",
52-
"specVersion": "1.1",
56+
"specVersion": spec_version,
5357
"serialNumber": random_urn_uuid,
5458
"version": 1,
5559
"components": []
@@ -62,9 +66,9 @@ def build_json_bom(gems)
6266
JSON.pretty_generate(bom_hash)
6367
end
6468

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

0 commit comments

Comments
 (0)