Skip to content

Commit

Permalink
feature: optionally output to json (#87)
Browse files Browse the repository at this point in the history
* feat: optionally output to json
resolves #62

Signed-off-by: Jeremy Long <[email protected]>

* style: codacy warnings

Signed-off-by: Jeremy Long <[email protected]>

* style: fix codacy warnings

Signed-off-by: Jeremy Long <[email protected]>

* style: fix codacy warnings

Signed-off-by: Jeremy Long <[email protected]>

* feat: add json tests

Signed-off-by: Jeremy Long <[email protected]>

* style: fix codacy warnings

Signed-off-by: Jeremy Long <[email protected]>

* style: fix codacy warnings

Signed-off-by: Jeremy Long <[email protected]>

* style: fix codacy warnings

Signed-off-by: Jeremy Long <[email protected]>

---------

Signed-off-by: Jeremy Long <[email protected]>
  • Loading branch information
jeremylong authored Jan 7, 2025
1 parent 93c3904 commit 9cbc432
Show file tree
Hide file tree
Showing 4 changed files with 739 additions and 19 deletions.
10 changes: 9 additions & 1 deletion .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,15 @@ Metrics/BlockLength:

# Allow some long methods because breaking them up doesn't help anything.
Metrics/MethodLength:
AllowedMethods: ['parse_options', 'add_to_bom', 'append_all_pod_dependencies', 'xml_add_evidence']
AllowedMethods:
- 'parse_options'
- 'add_to_bom'
- 'append_all_pod_dependencies'
- 'xml_add_evidence'
- 'generate_json_metadata'
- 'generate_json_evidence'
- 'to_json_component'
- 'to_json_manufacturer'
Metrics/AbcSize:
AllowedMethods: ['parse_options', 'add_to_bom', 'source_for_pod']

Expand Down
197 changes: 183 additions & 14 deletions lib/cyclonedx/cocoapods/bom_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,57 @@ def add_to_bom(xml, manifest_path, trim_strings_length = 0)
end
end

def to_json_component(manifest_path, trim_strings_length = 0)
{
type: 'library',
'bom-ref': purl,
author: trim(author, trim_strings_length),
publisher: trim(author, trim_strings_length),
name: name,
version: version.to_s,
description: description,
hashes: generate_json_hashes,
licenses: generate_json_licenses,
purl: purl,
externalReferences: generate_json_external_references,
evidence: generate_json_evidence(manifest_path)
}.compact
end

def generate_json_external_references
refs = []
refs << { type: HOMEPAGE_REFERENCE_TYPE, url: homepage } if homepage
refs.empty? ? nil : refs
end

def generate_json_evidence(manifest_path)
{
identity: {
field: 'purl',
confidence: 0.6,
methods: [
{
technique: 'manifest-analysis',
confidence: 0.6,
value: manifest_path
}
]
}
}
end

class License
def to_json_component
{
license: {
id: identifier_type == :id ? identifier : nil,
name: identifier_type == :name ? identifier : nil,
text: text,
url: url
}.compact
}
end

def add_to_bom(xml)
xml.license do
xml.id identifier if identifier_type == :id
Expand All @@ -160,6 +210,20 @@ def add_to_bom(xml)
end
end
end

private

def generate_json_licenses
license ? [license.to_json_component] : nil
end

def generate_json_hashes
checksum ? [{ alg: CHECKSUM_ALGORITHM, content: checksum }] : nil
end

def trim(str, trim_strings_length)
trim_strings_length.zero? ? str : str&.slice(0, trim_strings_length)
end
end

class Component
Expand Down Expand Up @@ -187,6 +251,27 @@ def add_to_bom(xml)
xml.purl bomref
end
end

def to_json_component
{
type: type,
'bom-ref': bomref,
group: group,
name: name,
version: version,
purl: bomref,
externalReferences: generate_json_external_references
}.compact
end

private

def generate_json_external_references
refs = []
refs << { type: 'build-system', url: build_system } if build_system
refs << { type: 'vcs', url: vcs } if vcs
refs.empty? ? nil : refs
end
end

# Represents manufacturer information in a CycloneDX BOM
Expand All @@ -202,8 +287,30 @@ def add_to_bom(xml)
end
end

def to_json_manufacturer
return nil if all_attributes_nil?

{
name: name,
url: url,
contact: generate_json_contact
}.compact
end

private

def generate_json_contact
return nil if contact_info_nil?

[
{
name: contact_name,
email: email,
phone: phone
}.compact
]
end

def all_attributes_nil?
[name, url, contact_name, email, phone].all?(&:nil?)
end
Expand Down Expand Up @@ -242,35 +349,97 @@ def initialize(pods:, manifest_path:, component: nil, dependencies: nil, manufac
@manufacturer = manufacturer
end

def bom(version: 1, trim_strings_length: 0)
unless version.to_i.positive?
raise ArgumentError,
"Incorrect version: #{version} should be an integer greater than 0"
end

unless trim_strings_length.is_a?(Integer) && (trim_strings_length.positive? || trim_strings_length.zero?)
raise ArgumentError,
"Incorrect string length: #{trim_strings_length} should be an integer greater than 0"
end
def bom(version: 1, trim_strings_length: 0, format: :xml)
validate_version(version)
validate_trim_length(trim_strings_length)
validate_format(format)

unchecked_bom(version: version, trim_strings_length: trim_strings_length)
unchecked_bom(version: version, trim_strings_length: trim_strings_length, format: format)
end

private

def validate_version(version)
return if version.to_i.positive?

raise ArgumentError, "Incorrect version: #{version} should be an integer greater than 0"
end

def validate_trim_length(trim_strings_length)
return if trim_strings_length.is_a?(Integer) && (trim_strings_length.positive? || trim_strings_length.zero?)

raise ArgumentError, "Incorrect string length: #{trim_strings_length} should be an integer greater than 0"
end

def validate_format(format)
return if %i[xml json].include?(format)

raise ArgumentError, "Incorrect format: #{format} should be either :xml or :json"
end

# does not verify parameters because the public method does that.
def unchecked_bom(version: 1, trim_strings_length: 0)
def unchecked_bom(version:, trim_strings_length:, format:)
case format
when :json
generate_json(version: version, trim_strings_length: trim_strings_length)
when :xml
generate_xml(version: version, trim_strings_length: trim_strings_length)
end
end

def generate_xml(version:, trim_strings_length:)
Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml|
xml.bom(xmlns: NAMESPACE, version: version.to_i.to_s, serialNumber: "urn:uuid:#{SecureRandom.uuid}") do
bom_metadata(xml)

bom_components(xml, pods, manifest_path, trim_strings_length)

bom_dependencies(xml, dependencies)
end
end.to_xml
end

def generate_json(version:, trim_strings_length:)
{
'$schema': 'https://cyclonedx.org/schema/bom-1.6.schema.json',
bomFormat: 'CycloneDX',
specVersion: '1.6',
serialNumber: "urn:uuid:#{SecureRandom.uuid}",
version: version.to_s,
metadata: generate_json_metadata,
components: generate_json_components(trim_strings_length),
dependencies: generate_json_dependencies
}.to_json
end

def generate_json_metadata
{
timestamp: Time.now.getutc.strftime('%Y-%m-%dT%H:%M:%SZ'),
tools: {
components: [{
type: 'application',
group: 'CycloneDX',
name: 'cyclonedx-cocoapods',
version: VERSION
}]
},
component: component&.to_json_component,
manufacturer: manufacturer&.to_json_manufacturer
}.compact
end

def generate_json_components(trim_strings_length)
pods.map { |pod| pod.to_json_component(manifest_path, trim_strings_length) }
end

def generate_json_dependencies
return nil unless dependencies

dependencies.map do |ref, deps|
{
ref: ref,
dependsOn: deps.sort
}
end
end
def bom_components(xml, pods, manifest_path, trim_strings_length)
xml.components do
pods.each do |pod|
Expand Down
11 changes: 9 additions & 2 deletions lib/cyclonedx/cocoapods/cli_runner.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ class CLIRunner
def run
setup_logger # Needed in case we have errors while processing CLI parameters
options = parse_options
determine_output_format(options)
setup_logger(verbose: options[:verbose])
@logger.debug "Running cyclonedx-cocoapods with options: #{options}"

Expand Down Expand Up @@ -78,7 +79,8 @@ def parse_options
parsed_options[:path] = path
end
options.on('-o', '--output bom_file_path',
'Path to output the bom.xml file to (default: "bom.xml")') do |bom_file_path|
'Path to output the bom file to (default: "bom.xml"); ' \
'if a *.json file is specified the output format will be json') do |bom_file_path|
parsed_options[:bom_file_path] = bom_file_path
end
options.on('-b', '--bom-version bom_version', Integer,
Expand Down Expand Up @@ -163,6 +165,10 @@ def parse_options
parsed_options
end

def determine_output_format(options)
options[:format] = options[:bom_file_path]&.end_with?('.json') ? :json : :xml
end

def analyze(options)
analyzer, dependencies, lockfile, podfile, pods = analyze_podfile(options)
podspec = analyze_podspec(options)
Expand Down Expand Up @@ -203,7 +209,8 @@ def build_and_write_bom(options, component, manufacturer, pods, manifest_path, d
builder = BOMBuilder.new(pods: pods, manifest_path: manifest_path,
component: component, manufacturer: manufacturer, dependencies: dependencies)
bom = builder.bom(version: options[:bom_version] || 1,
trim_strings_length: options[:trim_strings_length] || 0)
trim_strings_length: options[:trim_strings_length] || 0,
format: options[:format])
write_bom_to_file(bom: bom, options: options)
end

Expand Down
Loading

0 comments on commit 9cbc432

Please sign in to comment.