Skip to content

Commit 01ec4b1

Browse files
committed
✨ --include-metadata (metadata.tools)
- When provided, metadata.tools identifies this producer: - vendor: CycloneDX - name: cyclonedx-ruby - version: the gem’s version - Emitted for both JSON and XML, and only when the selected spec supports metadata (>= 1.2). - Help and README updated. - features/metadata_tools.feature (integration) - spec/cyclonedx/metadata_tools_spec.rb (unit, offline-safe) Signed-off-by: Peter H. Boling <[email protected]>
1 parent 6c2656e commit 01ec4b1

File tree

7 files changed

+153
-9
lines changed

7 files changed

+153
-9
lines changed

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ cyclonedx-ruby [options]
3232
`-o, --output bom_file_path` Path to output the bom file
3333
`-f, --format bom_output_format` Output format for bom. Supported: xml (default), json
3434
`-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
35+
`--include-metadata` Include metadata.tools identifying cyclonedx-ruby as the producer
3536
`--validate` Validate the produced BOM against the selected CycloneDX schema
3637
`--validate-file PATH` Validate an existing BOM file instead of generating one
3738
`-h, --help` Show help message
@@ -40,6 +41,7 @@ cyclonedx-ruby [options]
4041

4142
- By default, outputs conform to CycloneDX spec version 1.7.
4243
- To generate an older spec version, use `--spec-version`.
44+
- To embed metadata about this tool (vendor/name/version) into the BOM, pass `--include-metadata` (supported for spec >= 1.2).
4345

4446
#### Examples
4547
```bash
@@ -54,6 +56,9 @@ cyclonedx-ruby -p /path/to/ruby/project -s 1.3
5456

5557
# JSON at CycloneDX 1.2 to a custom path
5658
cyclonedx-ruby -p /path/to/ruby/project -f json -s 1.2 -o bom/out.json
59+
60+
# Include producer metadata and validate
61+
cyclonedx-ruby -p /path/to/ruby/project --include-metadata --validate
5762
```
5863

5964

features/help.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,6 @@ Scenario: Generate help on demand
1515
-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
1616
--validate Validate the produced BOM against the selected CycloneDX schema
1717
--validate-file PATH Validate an existing BOM file instead of generating one
18+
--include-metadata Include metadata.tools identifying cyclonedx-ruby as the producer
1819
-h, --help Show help message
1920
"""

features/metadata_tools.feature

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
Feature: Include metadata.tools in BOM
2+
3+
Scenario: JSON output includes metadata.tools when flag is set
4+
Given I use a fixture named "simple"
5+
And I run `cyclonedx-ruby --path . --format json --include-metadata`
6+
Then a file named "bom.json" should exist
7+
And the output should contain:
8+
"""
9+
5 gems were written to BOM located at ./bom.json
10+
"""
11+
And the file "bom.json" should contain:
12+
"""
13+
"metadata": {
14+
"tools": [
15+
{
16+
"vendor": "CycloneDX",
17+
"name": "cyclonedx-ruby"
18+
}
19+
]
20+
}
21+
"""
22+
23+
Scenario: JSON metadata BOM validates against schema
24+
Given I use a fixture named "simple"
25+
And I run `cyclonedx-ruby --path . --format json --include-metadata --validate`
26+
Then the output should contain:
27+
"""
28+
5 gems were written to BOM located at ./bom.json
29+
"""
30+
And a file named "bom.json" should exist
31+
32+
Scenario: XML output includes metadata.tools when flag is set
33+
Given I use a fixture named "simple"
34+
And I run `cyclonedx-ruby --path . --format xml --include-metadata`
35+
Then a file named "bom.xml" should exist
36+
And the output should contain:
37+
"""
38+
5 gems were written to BOM located at ./bom.xml
39+
"""
40+
And the file "bom.xml" should contain:
41+
"""
42+
<metadata>
43+
<tools>
44+
<tool>
45+
<vendor>CycloneDX</vendor>
46+
<name>cyclonedx-ruby</name>
47+
"""
48+
49+
Scenario: XML metadata BOM validates against schema
50+
Given I use a fixture named "simple"
51+
And I run `cyclonedx-ruby --path . --format xml --include-metadata --validate`
52+
Then the output should contain:
53+
"""
54+
5 gems were written to BOM located at ./bom.xml
55+
"""
56+
And a file named "bom.xml" should exist

lib/cyclonedx/bom_builder.rb

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ def self.build(path)
3131
end
3232

3333
specs_list
34-
bom = build_bom(@gems, @bom_output_format, @spec_version)
34+
bom = build_bom(@gems, @bom_output_format, @spec_version, include_metadata: @options[:include_metadata])
3535

3636
begin
3737
@logger.info("Changing directory to the original working directory located at #{original_working_directory}")
@@ -106,6 +106,9 @@ def self.setup(path)
106106
opts.on('--validate-file PATH', 'Validate an existing BOM file instead of generating one') do |path|
107107
@options[:validate_file] = path
108108
end
109+
opts.on('--include-metadata', 'Include metadata.tools identifying cyclonedx-ruby as the producer') do
110+
@options[:include_metadata] = true
111+
end
109112
opts.on_tail('-h', '--help', 'Show help message') do
110113
puts opts
111114
exit

lib/cyclonedx/bom_helpers.rb

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
# Copyright (c) OWASP Foundation. All Rights Reserved.
2424
#
2525

26+
require 'securerandom'
27+
2628
require_relative 'bom_component'
2729

2830
module Cyclonedx
@@ -41,15 +43,29 @@ def random_urn_uuid
4143
"urn:uuid:#{SecureRandom.uuid}"
4244
end
4345

44-
def build_bom(gems, format, spec_version)
46+
# Determine if the selected spec version supports metadata/tools (>= 1.2)
47+
def metadata_supported?(spec_version)
48+
%w[1.2 1.3 1.4 1.5 1.6 1.7].include?(spec_version)
49+
end
50+
51+
# Identity of this producer tool
52+
def tool_identity
53+
{
54+
vendor: 'CycloneDX',
55+
name: 'cyclonedx-ruby',
56+
version: ::Cyclonedx::Ruby::Version::VERSION
57+
}
58+
end
59+
60+
def build_bom(gems, format, spec_version, include_metadata: false)
4561
if format == 'json'
46-
build_json_bom(gems, spec_version)
62+
build_json_bom(gems, spec_version, include_metadata: include_metadata)
4763
else
48-
build_bom_xml(gems, spec_version)
64+
build_bom_xml(gems, spec_version, include_metadata: include_metadata)
4965
end
5066
end
5167

52-
def build_json_bom(gems, spec_version)
68+
def build_json_bom(gems, spec_version, include_metadata: false)
5369
bom_hash = {
5470
bomFormat: 'CycloneDX',
5571
specVersion: spec_version,
@@ -58,17 +74,39 @@ def build_json_bom(gems, spec_version)
5874
components: []
5975
}
6076

77+
# Optionally include metadata.tools when supported by selected spec
78+
if include_metadata && metadata_supported?(spec_version)
79+
ti = tool_identity
80+
ti = ti.compact # omit nil values like version
81+
bom_hash[:metadata] = {
82+
tools: [ti]
83+
}
84+
end
85+
6186
gems.each do |gem|
6287
bom_hash[:components] += Cyclonedx::BomComponent.new(gem).hash_val
6388
end
6489

6590
JSON.pretty_generate(bom_hash)
6691
end
6792

68-
def build_bom_xml(gems, spec_version)
93+
def build_bom_xml(gems, spec_version, include_metadata: false)
6994
builder = Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml|
7095
attributes = { 'xmlns' => cyclonedx_xml_namespace(spec_version), 'version' => '1', 'serialNumber' => random_urn_uuid }
7196
xml.bom(attributes) do
97+
# Optionally include metadata.tools when supported by selected spec
98+
if include_metadata && metadata_supported?(spec_version)
99+
xml.metadata do
100+
xml.tools do
101+
xml.tool do
102+
xml.vendor tool_identity[:vendor]
103+
xml.name tool_identity[:name]
104+
xml.version tool_identity[:version] if tool_identity[:version]
105+
end
106+
end
107+
end
108+
end
109+
72110
xml.components do
73111
gems.each do |gem|
74112
xml.component('type' => 'library') do
@@ -165,7 +203,7 @@ def get_gem(name, version, logger)
165203
url = "https://rubygems.org/api/v1/versions/#{name}.json"
166204
begin
167205
RestClient.proxy = ENV.fetch('http_proxy', nil)
168-
response = RestClient.get(url)
206+
response = RestClient::Request.execute(method: :get, url: url, read_timeout: 2, open_timeout: 2)
169207
body = JSON.parse(response.body)
170208
body.select { |item| item['number'] == version.to_s }.first
171209
rescue StandardError

lib/cyclonedx/ruby.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@
1515

1616
# This gem
1717
require_relative 'ruby/version'
18-
require_relative 'bom_helpers'
18+
require_relative 'bom_component' # no dependencies
19+
require_relative 'bom_helpers' # depends on bom_component
1920
require_relative 'bom_builder' # depends on bom_helpers
20-
require_relative 'bom_component'
2121

2222
module Cyclonedx
2323
module Ruby
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# frozen_string_literal: true
2+
3+
require 'json'
4+
require 'nokogiri'
5+
require_relative '../../lib/cyclonedx/bom_helpers'
6+
require_relative '../../lib/cyclonedx/ruby/version'
7+
8+
RSpec.describe 'metadata.tools emission' do
9+
let(:spec_version) { '1.7' }
10+
11+
it 'adds metadata.tools in JSON when include_metadata is true and spec >= 1.2' do
12+
json = Cyclonedx::BomHelpers.build_json_bom([], spec_version, include_metadata: true)
13+
data = JSON.parse(json)
14+
expect(data['metadata']).to be_a(Hash)
15+
expect(data['metadata']['tools']).to be_a(Array)
16+
expect(data['metadata']['tools'].first['vendor']).to eq('CycloneDX')
17+
expect(data['metadata']['tools'].first['name']).to eq('cyclonedx-ruby')
18+
end
19+
20+
it 'does not add metadata when include_metadata is false' do
21+
json = Cyclonedx::BomHelpers.build_json_bom([], spec_version, include_metadata: false)
22+
data = JSON.parse(json)
23+
expect(data).not_to have_key('metadata')
24+
end
25+
26+
it 'adds metadata.tools in XML when include_metadata is true and spec >= 1.2' do
27+
xml = Cyclonedx::BomHelpers.build_bom_xml([], spec_version, include_metadata: true)
28+
doc = Nokogiri::XML(xml)
29+
ns = { 'c' => Cyclonedx::BomHelpers.cyclonedx_xml_namespace(spec_version) }
30+
expect(doc.at_xpath('/c:bom/c:metadata/c:tools/c:tool/c:vendor', ns)&.text).to eq('CycloneDX')
31+
expect(doc.at_xpath('/c:bom/c:metadata/c:tools/c:tool/c:name', ns)&.text).to eq('cyclonedx-ruby')
32+
end
33+
34+
it 'omits metadata in XML when flag is false' do
35+
xml = Cyclonedx::BomHelpers.build_bom_xml([], spec_version, include_metadata: false)
36+
doc = Nokogiri::XML(xml)
37+
ns = { 'c' => Cyclonedx::BomHelpers.cyclonedx_xml_namespace(spec_version) }
38+
expect(doc.at_xpath('/c:bom/c:metadata', ns)).to be_nil
39+
end
40+
end
41+

0 commit comments

Comments
 (0)