Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: optionally output to json #87

Merged
merged 8 commits into from
Jan 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -149,7 +149,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 @@ -159,6 +209,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 @@ -186,6 +250,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 @@ -201,8 +286,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 @@ -241,35 +348,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
Loading