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

add support for dependencies #61

Merged
merged 3 commits into from
Jan 1, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
23 changes: 20 additions & 3 deletions lib/cyclonedx/cocoapods/bom_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ def add_to_bom(xml)
end
end
xml.purl purl
xml.bomRef purl
unless homepage.nil?
xml.externalReferences do
xml.reference(type: HOMEPAGE_REFERENCE_TYPE) do
Expand Down Expand Up @@ -119,10 +120,12 @@ def add_to_bom(xml)
class BOMBuilder
NAMESPACE = 'http://cyclonedx.org/schema/bom/1.4'

attr_reader :component, :pods
attr_reader :component, :pods, :dependencies

def initialize(component: nil, pods:)
@component, @pods = component, pods
def initialize(pods:, component: nil, dependencies: nil)
@pods = pods
@component = component
@dependencies = dependencies
end

def bom(version: 1)
Expand All @@ -136,12 +139,26 @@ def bom(version: 1)
pod.add_to_bom(xml)
end
end

xml.dependencies do
bom_dependencies(xml, dependencies)
end
end
end.to_xml
end

private

def bom_dependencies(xml, dependencies)
dependencies&.each do |key, array|
xml.dependency(ref: key) do
array.each do |value|
xml.dependency(ref: value)
end
end
end
end

def bom_metadata(xml)
xml.metadata do
xml.timestamp Time.now.getutc.strftime('%Y-%m-%dT%H:%M:%SZ')
Expand Down
5 changes: 3 additions & 2 deletions lib/cyclonedx/cocoapods/cli_runner.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,11 @@ def run

analyzer = PodfileAnalyzer.new(logger: @logger, exclude_test_targets: options[:exclude_test_targets])
podfile, lockfile = analyzer.ensure_podfile_and_lock_are_present(options)
pods = analyzer.parse_pods(podfile, lockfile)
pods, dependencies = analyzer.parse_pods(podfile, lockfile)
analyzer.populate_pods_with_additional_info(pods)

bom = BOMBuilder.new(component: component_from_options(options), pods: pods).bom(version: options[:bom_version] || 1)
builder = BOMBuilder.new(pods: pods, component: component_from_options(options), dependencies: dependencies)
bom = builder.bom(version: options[:bom_version] || 1)
write_bom_to_file(bom: bom, options: options)
rescue StandardError => e
@logger.error ([e.message] + e.backtrace).join($/)
Expand Down
42 changes: 33 additions & 9 deletions lib/cyclonedx/cocoapods/podfile_analyzer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def load_plugins(podfile_path)
podfile_contents = File.read(podfile_path)
plugin_syntax = /\s*plugin\s+['"]([^'"]+)['"]/
plugin_names = podfile_contents.scan(plugin_syntax).flatten

plugin_names.each do |plugin_name|
@logger.debug("Loading plugin #{plugin_name}")
begin
Expand All @@ -67,17 +67,32 @@ def ensure_podfile_and_lock_are_present(options)
lockfile = ::Pod::Lockfile.from_file(options[:podfile_lock_path])
verify_synced_sandbox(lockfile)
load_plugins(options[:podfile_path])

return ::Pod::Podfile.from_file(options[:podfile_path]), lockfile
end


def parse_pods(podfile, lockfile)
@logger.debug "Parsing pods from #{podfile.defined_in_file}"
included_pods = create_list_of_included_pods(podfile, lockfile)
return lockfile.pod_names.select { |name| included_pods.include?(name) }.map do |name|
included_pods, dependencies = create_list_of_included_pods(podfile, lockfile)

pods = lockfile.pod_names.select { |name| included_pods.include?(name) }.map do |name|
Pod.new(name: name, version: lockfile.version(name), source: source_for_pod(podfile, lockfile, name), checksum: lockfile.checksum(name))
end

pod_dependencies = { }
dependencies.each {|key, value|
if lockfile.pod_names.include? key
pod = Pod.new(name: key, version: lockfile.version(key), source: source_for_pod(podfile, lockfile, key), checksum: lockfile.checksum(key))

pod_dependencies[pod.purl] = lockfile.pod_names.select { |name| value.include?(name) }.map do |name|
pod = Pod.new(name: name, version: lockfile.version(name), source: source_for_pod(podfile, lockfile, name), checksum: lockfile.checksum(name))
pod.purl
end
end
}

return pods, pod_dependencies
end


Expand All @@ -89,7 +104,6 @@ def populate_pods_with_additional_info(pods)
return pods
end


private


Expand Down Expand Up @@ -124,23 +138,33 @@ def simple_hash_of_lockfile_pods(lockfile)
pods_hash
end


def append_all_pod_dependencies(pods_used, pods_cache)
result = pods_used
original_number = 0
dependencies_hash = { }

# Loop adding pod dependencies until we are not adding any more dependencies to the result
# This brings in all the transitive dependencies of every top level pod.
# Note this also handles two edge cases:
# 1. Having a Podfile with no pods used.
# 2. Having a pod that has a platform-specific dependency that is unused for this Podfile.
while result.length != original_number
original_number = result.length

pods_used.each { |pod_name|
result.push(*pods_cache[pod_name]) unless !pods_cache.key?(pod_name) || pods_cache[pod_name].empty?
if pods_cache.key?(pod_name)
result.push(*pods_cache[pod_name])
dependencies_hash[pod_name] = pods_cache[pod_name].empty? ? [] : pods_cache[pod_name]
end
}

result = result.uniq
# maybe additional dependency processing needed here???
pods_used = result
end
result

return result, dependencies_hash
end

def create_list_of_included_pods(podfile, lockfile)
Expand All @@ -152,9 +176,9 @@ def create_list_of_included_pods(podfile, lockfile)

topLevelDeps = includedTargets.map(&:dependencies).flatten.uniq
pods_used = topLevelDeps.map(&:name).uniq
pods_used = append_all_pod_dependencies(pods_used, pods_cache)
pods_used, dependencies = append_all_pod_dependencies(pods_used, pods_cache)

return pods_used.sort
return pods_used.sort, dependencies
end


Expand Down
51 changes: 44 additions & 7 deletions spec/cyclonedx/cocoapods/bom_builder_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -253,12 +253,27 @@
end
end


RSpec.describe CycloneDX::CocoaPods::BOMBuilder do
context 'when generating a BOM' do
let(:pods) { {
'Alamofire' => '5.4.2', 'FirebaseAnalytics' => '7.10.0', 'RxSwift' => '5.1.2', 'Realm' => '5.5.1'
}.map { |name, version| CycloneDX::CocoaPods::Pod.new(name: name, version: version) } }
let(:pods) do
{
'Alamofire' => '5.6.2',
'FirebaseAnalytics' => '7.10.0',
'RxSwift' => '5.1.2',
'Realm' => '5.5.1',
'MSAL' => '1.2.1',
'MSAL/app-lib' => '1.2.1'
}.map { |name, version| CycloneDX::CocoaPods::Pod.new(name: name, version: version) }
end
let(:dependencies) do
{
'pkg:cocoapods/[email protected]' => [],
'pkg:cocoapods/[email protected]' => ['pkg:cocoapods/[email protected]#app-lib'],
'pkg:cocoapods/[email protected]#app-lib' => [],
'pkg:cocoapods/[email protected]#app-lib' => [],
'pkg:cocoapods/[email protected]#app-lib' => []
}
end

shared_examples "bom_generator" do
context 'with an incorrect version' do
Expand Down Expand Up @@ -328,7 +343,7 @@
it 'should generate component metadata when a component is available' do
if bom_builder.component
component_metadata = Nokogiri::XML(Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml|
xml.metadata('xmlns': described_class::NAMESPACE) do
xml.metadata(xmlns: described_class::NAMESPACE) do
component.add_to_bom(xml)
end
end.to_xml).at('metadata/component')
Expand All @@ -348,13 +363,35 @@
components_generated_from_bom_builder = xml.at('bom/components')

components = Nokogiri::XML(Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml|
xml.components('xmlns': described_class::NAMESPACE) do
xml.components(xmlns: described_class::NAMESPACE) do
bom_builder.pods.each { |pod| pod.add_to_bom(xml) }
end
end.to_xml).at('components')

expect(components_generated_from_bom_builder).to be_equivalent_to(components)
end

it 'should generate a child dependencies node' do
expect(xml.at('bom/dependencies')).not_to be_nil
end

it 'shoudl properly set dependencies node' do
dependencies_generated_from_bom_builder = xml.at('bom/dependencies')

dependencies = Nokogiri::XML(Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml|
xml.dependencies(xmlns: described_class::NAMESPACE) do
bom_builder.dependencies.to_a.map do |key, array|
xml.dependency(ref: key) do
array.each do |value|
xml.dependency(ref: value)
end
end
end
end
end.to_xml).at('dependencies')

expect(dependencies_generated_from_bom_builder).to be_equivalent_to(dependencies)
end
end
end

Expand All @@ -366,7 +403,7 @@

context 'with a component' do
let(:component) { CycloneDX::CocoaPods::Component.new(name: 'Application', version: '1.3.5', type: 'application') }
let(:bom_builder) { described_class.new(component: component, pods: pods) }
let(:bom_builder) { described_class.new(component: component, pods: pods, dependencies: dependencies) }

it_behaves_like "bom_generator"
end
Expand Down
37 changes: 30 additions & 7 deletions spec/cyclonedx/cocoapods/podfile_analyzer_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@

require 'cyclonedx/cocoapods/podfile_analyzer'
require 'rspec'

RSpec.describe CycloneDX::CocoaPods::PodfileAnalyzer do
let(:fixtures) { Pathname.new(File.expand_path('../../../fixtures/', __FILE__)) }
let(:empty_podfile) { 'EmptyPodfile/Podfile' }
Expand All @@ -46,10 +45,12 @@
lock_file = ::Pod::Lockfile.from_file(fixtures + (empty_podfile + '.lock'))
expect(lock_file).not_to be_nil

included_pods = analyzer.parse_pods(pod_file, lock_file)
included_pods, dependencies = analyzer.parse_pods(pod_file, lock_file)

pod_names = included_pods.map(&:name)
expect(pod_names).to eq([])
expect(dependencies).to eq({})
expect(pod_names.length).to eq(dependencies.length)
end

it 'should find all simple pods' do
Expand All @@ -60,10 +61,16 @@
lock_file = ::Pod::Lockfile.from_file(fixtures + (simple_pod + '.lock'))
expect(lock_file).not_to be_nil

included_pods = analyzer.parse_pods(pod_file, lock_file)
included_pods, dependencies = analyzer.parse_pods(pod_file, lock_file)

pod_names = included_pods.map(&:name)
expect(pod_names).to eq(['Alamofire', 'MSAL', 'MSAL/app-lib'])
expect(dependencies).to eq({
'pkg:cocoapods/[email protected]' => [],
'pkg:cocoapods/[email protected]' => ['pkg:cocoapods/[email protected]#app-lib'],
'pkg:cocoapods/[email protected]#app-lib' => []
})
expect(pod_names.length).to eq(dependencies.length)
end

it 'should find all pods actually used' do
Expand All @@ -74,10 +81,12 @@
lock_file = ::Pod::Lockfile.from_file(fixtures + (restricted_pod + '.lock'))
expect(lock_file).not_to be_nil

included_pods = analyzer.parse_pods(pod_file, lock_file)
included_pods, dependencies = analyzer.parse_pods(pod_file, lock_file)

pod_names = included_pods.map(&:name)
expect(pod_names).to eq(['EFQRCode'])
expect(dependencies).to eq({ 'pkg:cocoapods/[email protected]' => [] })
expect(pod_names.length).to eq(dependencies.length)
end

it 'should find all pods' do
Expand All @@ -88,10 +97,16 @@
lock_file = ::Pod::Lockfile.from_file(fixtures + (tests_pod + '.lock'))
expect(lock_file).not_to be_nil

included_pods = analyzer.parse_pods(pod_file, lock_file)
included_pods, dependencies = analyzer.parse_pods(pod_file, lock_file)

pod_names = included_pods.map(&:name)
expect(pod_names).to eq(['Alamofire', 'MSAL', 'MSAL/app-lib'])
expect(dependencies).to eq({
'pkg:cocoapods/[email protected]' => [],
'pkg:cocoapods/[email protected]' => ['pkg:cocoapods/[email protected]#app-lib'],
'pkg:cocoapods/[email protected]#app-lib' => []
})
expect(pod_names.length).to eq(dependencies.length)
end
end

Expand All @@ -104,10 +119,16 @@
lock_file = ::Pod::Lockfile.from_file(fixtures + (simple_pod + '.lock'))
expect(lock_file).not_to be_nil

included_pods = analyzer.parse_pods(pod_file, lock_file)
included_pods, dependencies = analyzer.parse_pods(pod_file, lock_file)

pod_names = included_pods.map(&:name)
expect(pod_names).to eq(['Alamofire', 'MSAL', 'MSAL/app-lib'])
expect(dependencies).to eq({
'pkg:cocoapods/[email protected]' => [],
'pkg:cocoapods/[email protected]' => ['pkg:cocoapods/[email protected]#app-lib'],
'pkg:cocoapods/[email protected]#app-lib' => []
})
expect(pod_names.length).to eq(dependencies.length)
end

it 'should not include testing pods' do
Expand All @@ -118,10 +139,12 @@
lock_file = ::Pod::Lockfile.from_file(fixtures + (tests_pod + '.lock'))
expect(lock_file).not_to be_nil

included_pods = analyzer.parse_pods(pod_file, lock_file)
included_pods, dependencies = analyzer.parse_pods(pod_file, lock_file)

pod_names = included_pods.map(&:name)
expect(pod_names).to eq(['Alamofire'])
expect(dependencies).to eq({ 'pkg:cocoapods/[email protected]' => [] })
expect(pod_names.length).to eq(dependencies.length)
end
end
end
Expand Down