Skip to content

Commit a7ba5ef

Browse files
authored
Merge pull request #15 from jeffreysfllo24/add-json-format
Add support for JSON bom output
2 parents c96d229 + f095682 commit a7ba5ef

File tree

5 files changed

+94
-5
lines changed

5 files changed

+94
-5
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,10 @@ 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
3233
`-h, --help` Show help message
3334

34-
**Output:** bom.xml file in project directory
35+
**Output:** bom.xml or bom.json file in project directory
3536

3637
#### Example
3738
```bash

cyclonedx-ruby.gemspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ Gem::Specification.new do |spec|
88
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.'
99
spec.authors = ['Joseph Kobti', 'Steve Springett']
1010
spec.email = '[email protected]'
11-
spec.files = ['lib/bom_builder.rb', 'lib/bom_helpers.rb', 'lib/licenses.json']
11+
spec.files = ['lib/bom_builder.rb', 'lib/bom_helpers.rb', 'lib/licenses.json', 'lib/bom_component.rb']
1212
spec.homepage = 'https://github.com/CycloneDX/cyclonedx-ruby-gem'
1313
spec.license = 'Apache-2.0'
1414
spec.executables << 'cyclonedx-ruby'

lib/bom_builder.rb

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,16 @@
3131
require 'rest_client'
3232
require 'securerandom'
3333
require_relative 'bom_helpers'
34+
require 'active_support/core_ext/hash'
3435

3536
class Bombuilder
37+
SUPPORTED_BOM_FORMATS = %w[xml json]
38+
3639
def self.build(path)
3740
original_working_directory = Dir.pwd
3841
setup(path)
3942
specs_list
40-
bom = build_bom(@gems)
43+
bom = build_bom(@gems, @bom_output_format)
4144

4245
begin
4346
@logger.info("Changing directory to the original working directory located at #{original_working_directory}")
@@ -84,6 +87,9 @@ def self.setup(path)
8487
opts.on('-o', '--output bom_file_path', '(Optional) Path to output the bom.xml file to') do |bom_file_path|
8588
@options[:bom_file_path] = bom_file_path
8689
end
90+
opts.on('-f', '--format bom_output_format', '(Optional) Output format for bom. Currently support xml (default) and json.') do |bom_output_format|
91+
@options[:bom_output_format] = bom_output_format
92+
end
8793
opts.on_tail('-h', '--help', 'Show help message') do
8894
puts opts
8995
exit
@@ -119,8 +125,17 @@ def self.setup(path)
119125
abort
120126
end
121127

128+
if @options[:bom_output_format].nil?
129+
@bom_output_format = 'xml'
130+
elsif SUPPORTED_BOM_FORMATS.include?(@options[:bom_output_format])
131+
@bom_output_format = @options[:bom_output_format]
132+
else
133+
@logger.error("Unrecognized cyclonedx bom output format provided. Please choose one of #{SUPPORTED_BOM_FORMATS}")
134+
abort
135+
end
136+
122137
@bom_file_path = if @options[:bom_file_path].nil?
123-
'./bom.xml'
138+
"./bom.#{@bom_output_format}"
124139
else
125140
@options[:bom_file_path]
126141
end

lib/bom_component.rb

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
2+
class BomComponent
3+
DEFAULT_TYPE = "library".freeze
4+
HASH_ALG = 'SHA-256'.freeze
5+
6+
def initialize(gem)
7+
@name = gem['name']
8+
@version = gem['version']
9+
@description = gem['description']
10+
@hash = gem['hash']
11+
@purl = gem['purl']
12+
@gem = gem
13+
end
14+
15+
def hash_val
16+
component_hash = {
17+
"type": DEFAULT_TYPE,
18+
"name": @name,
19+
"version": @version,
20+
"description": @description,
21+
"purl": @purl,
22+
"hashes": [
23+
"alg": HASH_ALG,
24+
"content": @hash
25+
]
26+
}
27+
28+
if @gem['license_id']
29+
component_hash[:"licenses"] = [
30+
"license": {
31+
"id": @gem['license_id']
32+
}
33+
]
34+
elsif @gem['license_name']
35+
component_hash[:"licenses"] = [
36+
"license": {
37+
"name": @gem['license_name']
38+
}
39+
]
40+
end
41+
42+
[component_hash]
43+
44+
end
45+
end

lib/bom_helpers.rb

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@
2121
# Copyright (c) OWASP Foundation. All Rights Reserved.
2222
#
2323
# frozen_string_literal: true
24+
25+
require_relative 'bom_component'
26+
2427
def purl(name, version)
2528
"pkg:gem/#{name}@#{version}"
2629
end
@@ -29,7 +32,31 @@ def random_urn_uuid
2932
"urn:uuid:#{SecureRandom.uuid}"
3033
end
3134

32-
def build_bom(gems)
35+
def build_bom(gems, format)
36+
if format == 'json'
37+
build_json_bom(gems)
38+
else
39+
build_bom_xml(gems)
40+
end
41+
end
42+
43+
def build_json_bom(gems)
44+
bom_hash = {
45+
"bomFormat": "CycloneDX",
46+
"specVersion": "1.1",
47+
"serialNumber": random_urn_uuid,
48+
"version": 1,
49+
"components": []
50+
}
51+
52+
gems.each do |gem|
53+
bom_hash[:components] += BomComponent.new(gem).hash_val()
54+
end
55+
56+
JSON.pretty_generate(bom_hash)
57+
end
58+
59+
def build_bom_xml(gems)
3360
builder = Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml|
3461
attributes = { 'xmlns' => 'http://cyclonedx.org/schema/bom/1.1', 'version' => '1', 'serialNumber' => random_urn_uuid }
3562
xml.bom(attributes) do
@@ -61,6 +88,7 @@ def build_bom(gems)
6188
end
6289
end
6390
end
91+
6492
builder.to_xml
6593
end
6694

0 commit comments

Comments
 (0)