cyclonedx-cocoapods 1.1.1 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4185f4f9ba77e7307a2f2a93ca2d96412775a6fa1a9d863b0955e0ccde099214
4
- data.tar.gz: 4df386bef89ea9bb2bc7bfd9299a582faec48e8887f7443acd2cfb321947c0fd
3
+ metadata.gz: f9377b14e9d7b5f41db0b12693b58dd37367e05e72f8ab208178c6d79f38234b
4
+ data.tar.gz: a8402aabb0a9eb157bbbaab4782a3fc6ce731003b48037c54cb1dc7e2d5fb289
5
5
  SHA512:
6
- metadata.gz: 847962664a8e0d9eca4ee42d2560151de4f56fbdcb3524ff1ad208f6cac0bc5b234d879f09cccdcaddf6df81096bdb89ea26923e2195e2127334276bfb32b856
7
- data.tar.gz: 799ca49eb4e2dd2caf9c93cd211692a364091ef8366895fe6595d109d8b455bf222c607e19b16bd410e927d7099a29a2d1dfad8b0f9d84b837705f7e7b448bb1
6
+ metadata.gz: 7fe8629cb7313126a55d2cbae4e7f69ea6fc365336b56093eab5a23e3a27d4fde848a892926ce2fc201509af9c9e4adb9cf59e0940841a03023e344817be2aa2
7
+ data.tar.gz: 8cc8b64ddcf4292e14c4698e55899a2b6c72de7ed1ed5ee7adcef316fa315286e52b108a5d000a5fa6f6e42333fd752633b7eeb0142fa603b615b05fef688c8e
data/CHANGELOG.md CHANGED
@@ -4,6 +4,24 @@ All notable changes to this project will be documented in this file.
4
4
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5
5
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [1.2.0]
8
+
9
+ ### Added
10
+ - Includes dependency relationship information for each of the components. ([Issue #58](https://github.com/CycloneDX/cyclonedx-cocoapods/issues/58)) [@fnxpt](https://github.com/fnxpt).
11
+
12
+ ### Changed
13
+ - Components and dependencies are output in alphabetically sorted order by `purl` to increase reproducability of BOM generation. ([Issue #59](https://github.com/CycloneDX/cyclonedx-cocoapods/issues/59)) [@macblazer](https://github.com/macblazer).
14
+
15
+ ## [1.1.2]
16
+
17
+ ### Changed
18
+ - Updated gem dependency for cocoapods to be minimum v1.10.1 up to anything less than v2. ([Issue #51](https://github.com/CycloneDX/cyclonedx-cocoapods/issues/51)) [@macblazer](https://github.com/macblazer).
19
+ - Updated gem dependency for nokogiri to be minimum v1.11.2 up to anything less than v2. [@macblazer](https://github.com/macblazer).
20
+ - Updated README.md with a description of what happens with pods or Podfiles that use subspecs. ([Issue #52](https://github.com/CycloneDX/cyclonedx-cocoapods/issues/52)) [@macblazer](https://github.com/macblazer).
21
+
22
+ ### Fixed
23
+ - Fixed parsing of a Podfile that uses CocoaPods plugins. ([PR #55](https://github.com/CycloneDX/cyclonedx-cocoapods/pull/55)) [@DwayneCoussement](https://github.com/DwayneCoussement).
24
+
7
25
  ## [1.1.1]
8
26
 
9
27
  ### Changed
data/README.md CHANGED
@@ -80,6 +80,41 @@ then these two commands were run in the checked out code directory.
80
80
  % cyclonedx-cocoapods -n "kizitonwose/PodsUpdater" -v 1.0.3 -t application --output example_bom.xml
81
81
  ```
82
82
 
83
+ ### A Note About CocoaPod Subspecs
84
+
85
+ Many CocoaPods make use of [subspec functionality](https://guides.cocoapods.org/syntax/podspec.html#subspec).
86
+ Podfiles can require whole pods, or just subspecs; pods themselves may require whole pods or subspecs of other
87
+ pods. In complex projects such as React Native apps this often results in a single pod being included as a
88
+ dependency multiple times as several of its subspecs are included individually.
89
+
90
+ *cyclonedx-cocoapods* works properly with this, and adds a dependency in the BOM output for each subspec that is
91
+ required by the Podfile and throughout the chain of dependencies. Each subspec will only appear once in the BOM
92
+ file. This gives you granular detail in the BOM of which subspecs of which pods are used. This is easiest seen
93
+ with an example.
94
+
95
+ The Podfile
96
+ ```ruby
97
+ target 'SampleProject' do
98
+ pod 'SamplePod/firstsubspec'
99
+ pod 'SamplePod/secondsubspec'
100
+ end
101
+ ```
102
+
103
+ If the SamplePod is at v2.1, running *cyclonedx-cocoapods* on this will output a BOM file with two `component`
104
+ dependencies:
105
+ - `pkg:cocoapods/SamplePod@2.1#firstsubspec` at `https://github.com/example/SamplePod`
106
+ - `pkg:cocoapods/SamplePod@2.1#secondsubspec` at `https://github.com/example/SamplePod`
107
+
108
+ [Dependency Track](https://dependencytrack.org) (DT) is a tool that many organizations use to help automate SBOM
109
+ related tasks. When uploading an SBOM that contains multiple subspecs from the same pod, or a single subspec
110
+ alongside the complete pod dependency, the initial upload will indicate a number of dependencies equal to the number
111
+ of `component` objects within the BOM. However, DT analysis then looks for unique repositories in use which will
112
+ merge all of the subspecs of a particular pod into a single entry. On later uploads to DT of the same or similar BOM
113
+ it will indicate just the number of unique repositories.
114
+
115
+ Uploading the above SamplePod BOM file to DT will initially see two dependencies. Later analysis by DT notices
116
+ that both dependencies resolve to the same repository, so DT will then only show a single dependency.
117
+
83
118
  ## Contributing
84
119
 
85
120
  To set up for local development, make a fork of this repo, make a branch on your fork named after the issue or workflow you are improving, checkout your branch, then run `bundle install`.
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  #
2
4
  # This file is part of CycloneDX CocoaPods
3
5
  #
@@ -26,8 +28,8 @@ module CycloneDX
26
28
  module CocoaPods
27
29
  module Source
28
30
  class CocoaPodsRepository
29
- LEGACY_REPOSITORY = 'https://github.com/CocoaPods/Specs.git'.freeze
30
- CDN_REPOSITORY = 'trunk'.freeze
31
+ LEGACY_REPOSITORY = 'https://github.com/CocoaPods/Specs.git'
32
+ CDN_REPOSITORY = 'trunk'
31
33
 
32
34
  def source_qualifier
33
35
  url == LEGACY_REPOSITORY || url == CDN_REPOSITORY ? {} : { repository_url: url }
@@ -54,8 +56,8 @@ module CycloneDX
54
56
  end
55
57
 
56
58
  class Pod
57
- CHECKSUM_ALGORITHM = 'SHA-1'.freeze
58
- HOMEPAGE_REFERENCE_TYPE = 'website'.freeze
59
+ CHECKSUM_ALGORITHM = 'SHA-1'
60
+ HOMEPAGE_REFERENCE_TYPE = 'website'
59
61
 
60
62
  def purl
61
63
  purl_name = CGI.escape(name.split('/').first)
@@ -82,6 +84,7 @@ module CycloneDX
82
84
  end
83
85
  end
84
86
  xml.purl purl
87
+ xml.bomRef purl
85
88
  unless homepage.nil?
86
89
  xml.externalReferences do
87
90
  xml.reference(type: HOMEPAGE_REFERENCE_TYPE) do
@@ -115,12 +118,14 @@ module CycloneDX
115
118
  end
116
119
 
117
120
  class BOMBuilder
118
- NAMESPACE = 'http://cyclonedx.org/schema/bom/1.4'.freeze
121
+ NAMESPACE = 'http://cyclonedx.org/schema/bom/1.4'
119
122
 
120
- attr_reader :component, :pods
123
+ attr_reader :component, :pods, :dependencies
121
124
 
122
- def initialize(component: nil, pods:)
123
- @component, @pods = component, pods
125
+ def initialize(pods:, component: nil, dependencies: nil)
126
+ @pods = pods.sort_by(&:purl)
127
+ @component = component
128
+ @dependencies = dependencies&.sort
124
129
  end
125
130
 
126
131
  def bom(version: 1)
@@ -134,12 +139,26 @@ module CycloneDX
134
139
  pod.add_to_bom(xml)
135
140
  end
136
141
  end
142
+
143
+ xml.dependencies do
144
+ bom_dependencies(xml, dependencies)
145
+ end
137
146
  end
138
147
  end.to_xml
139
148
  end
140
149
 
141
150
  private
142
151
 
152
+ def bom_dependencies(xml, dependencies)
153
+ dependencies&.each do |key, array|
154
+ xml.dependency(ref: key) do
155
+ array.sort.each do |value|
156
+ xml.dependency(ref: value)
157
+ end
158
+ end
159
+ end
160
+ end
161
+
143
162
  def bom_metadata(xml)
144
163
  xml.metadata do
145
164
  xml.timestamp Time.now.getutc.strftime('%Y-%m-%dT%H:%M:%SZ')
@@ -155,4 +174,4 @@ module CycloneDX
155
174
  end
156
175
  end
157
176
  end
158
- end
177
+ end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  #
3
4
  # This file is part of CycloneDX CocoaPods
4
5
  #
@@ -39,10 +40,11 @@ module CycloneDX
39
40
 
40
41
  analyzer = PodfileAnalyzer.new(logger: @logger, exclude_test_targets: options[:exclude_test_targets])
41
42
  podfile, lockfile = analyzer.ensure_podfile_and_lock_are_present(options)
42
- pods = analyzer.parse_pods(podfile, lockfile)
43
+ pods, dependencies = analyzer.parse_pods(podfile, lockfile)
43
44
  analyzer.populate_pods_with_additional_info(pods)
44
45
 
45
- bom = BOMBuilder.new(component: component_from_options(options), pods: pods).bom(version: options[:bom_version] || 1)
46
+ builder = BOMBuilder.new(pods: pods, component: component_from_options(options), dependencies: dependencies)
47
+ bom = builder.bom(version: options[:bom_version] || 1)
46
48
  write_bom_to_file(bom: bom, options: options)
47
49
  rescue StandardError => e
48
50
  @logger.error ([e.message] + e.backtrace).join($/)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  #
2
4
  # This file is part of CycloneDX CocoaPods
3
5
  #
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  #
2
4
  # This file is part of CycloneDX CocoaPods
3
5
  #
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  #
2
4
  # This file is part of CycloneDX CocoaPods
3
5
  #
@@ -105,4 +107,4 @@ module CycloneDX
105
107
  end
106
108
  end
107
109
  end
108
- end
110
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  #
2
4
  # This file is part of CycloneDX CocoaPods
3
5
  #
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  #
3
4
  # This file is part of CycloneDX CocoaPods
4
5
  #
@@ -19,6 +20,7 @@
19
20
  #
20
21
 
21
22
  require 'cocoapods'
23
+ require 'cocoapods-core'
22
24
  require 'logger'
23
25
 
24
26
  require_relative 'pod'
@@ -35,6 +37,23 @@ module CycloneDX
35
37
  @exclude_test_targets = exclude_test_targets
36
38
  end
37
39
 
40
+ def load_plugins(podfile_path)
41
+ podfile_contents = File.read(podfile_path)
42
+ plugin_syntax = /\s*plugin\s+['"]([^'"]+)['"]/
43
+ plugin_names = podfile_contents.scan(plugin_syntax).flatten
44
+
45
+ plugin_names.each do |plugin_name|
46
+ @logger.debug("Loading plugin #{plugin_name}")
47
+ begin
48
+ plugin_spec = Gem::Specification.find_by_name(plugin_name)
49
+ plugin_spec.activate if plugin_spec
50
+ load(plugin_spec.gem_dir + '/lib/cocoapods_plugin.rb') if plugin_spec
51
+ rescue Gem::LoadError => e
52
+ @logger.warn("Failed to load plugin #{plugin_name}. #{e.message}")
53
+ end
54
+ end
55
+ end
56
+
38
57
  def ensure_podfile_and_lock_are_present(options)
39
58
  project_dir = Pathname.new(options[:path] || Dir.pwd)
40
59
  raise PodfileParsingError, "#{options[:path]} is not a valid directory." unless File.directory?(project_dir)
@@ -47,6 +66,7 @@ module CycloneDX
47
66
 
48
67
  lockfile = ::Pod::Lockfile.from_file(options[:podfile_lock_path])
49
68
  verify_synced_sandbox(lockfile)
69
+ load_plugins(options[:podfile_path])
50
70
 
51
71
  return ::Pod::Podfile.from_file(options[:podfile_path]), lockfile
52
72
  end
@@ -54,10 +74,25 @@ module CycloneDX
54
74
 
55
75
  def parse_pods(podfile, lockfile)
56
76
  @logger.debug "Parsing pods from #{podfile.defined_in_file}"
57
- included_pods = create_list_of_included_pods(podfile, lockfile)
58
- return lockfile.pod_names.select { |name| included_pods.include?(name) }.map do |name|
77
+ included_pods, dependencies = create_list_of_included_pods(podfile, lockfile)
78
+
79
+ pods = lockfile.pod_names.select { |name| included_pods.include?(name) }.map do |name|
59
80
  Pod.new(name: name, version: lockfile.version(name), source: source_for_pod(podfile, lockfile, name), checksum: lockfile.checksum(name))
60
81
  end
82
+
83
+ pod_dependencies = { }
84
+ dependencies.each {|key, value|
85
+ if lockfile.pod_names.include? key
86
+ pod = Pod.new(name: key, version: lockfile.version(key), source: source_for_pod(podfile, lockfile, key), checksum: lockfile.checksum(key))
87
+
88
+ pod_dependencies[pod.purl] = lockfile.pod_names.select { |name| value.include?(name) }.map do |name|
89
+ pod = Pod.new(name: name, version: lockfile.version(name), source: source_for_pod(podfile, lockfile, name), checksum: lockfile.checksum(name))
90
+ pod.purl
91
+ end
92
+ end
93
+ }
94
+
95
+ return pods, pod_dependencies
61
96
  end
62
97
 
63
98
 
@@ -69,7 +104,6 @@ module CycloneDX
69
104
  return pods
70
105
  end
71
106
 
72
-
73
107
  private
74
108
 
75
109
 
@@ -104,9 +138,12 @@ module CycloneDX
104
138
  pods_hash
105
139
  end
106
140
 
141
+
107
142
  def append_all_pod_dependencies(pods_used, pods_cache)
108
143
  result = pods_used
109
144
  original_number = 0
145
+ dependencies_hash = { }
146
+
110
147
  # Loop adding pod dependencies until we are not adding any more dependencies to the result
111
148
  # This brings in all the transitive dependencies of every top level pod.
112
149
  # Note this also handles two edge cases:
@@ -114,13 +151,20 @@ module CycloneDX
114
151
  # 2. Having a pod that has a platform-specific dependency that is unused for this Podfile.
115
152
  while result.length != original_number
116
153
  original_number = result.length
154
+
117
155
  pods_used.each { |pod_name|
118
- result.push(*pods_cache[pod_name]) unless !pods_cache.key?(pod_name) || pods_cache[pod_name].empty?
156
+ if pods_cache.key?(pod_name)
157
+ result.push(*pods_cache[pod_name])
158
+ dependencies_hash[pod_name] = pods_cache[pod_name].empty? ? [] : pods_cache[pod_name]
159
+ end
119
160
  }
161
+
120
162
  result = result.uniq
163
+ # maybe additional dependency processing needed here???
121
164
  pods_used = result
122
165
  end
123
- result
166
+
167
+ return result, dependencies_hash
124
168
  end
125
169
 
126
170
  def create_list_of_included_pods(podfile, lockfile)
@@ -132,9 +176,9 @@ module CycloneDX
132
176
 
133
177
  topLevelDeps = includedTargets.map(&:dependencies).flatten.uniq
134
178
  pods_used = topLevelDeps.map(&:name).uniq
135
- pods_used = append_all_pod_dependencies(pods_used, pods_cache)
179
+ pods_used, dependencies = append_all_pod_dependencies(pods_used, pods_cache)
136
180
 
137
- return pods_used.sort
181
+ return pods_used.sort, dependencies
138
182
  end
139
183
 
140
184
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  #
2
4
  # This file is part of CycloneDX CocoaPods
3
5
  #
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  #
3
4
  # This file is part of CycloneDX CocoaPods
4
5
  #
@@ -20,10 +21,6 @@
20
21
 
21
22
  module CycloneDX
22
23
  module CocoaPods
23
- VERSION = '1.1.1'
24
- DEPENDENCIES = {
25
- cocoapods: '~> 1.10.1',
26
- nokogiri: '~> 1.11.2'
27
- }
24
+ VERSION = '1.2.0'
28
25
  end
29
26
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cyclonedx-cocoapods
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.1
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - José González
@@ -9,36 +9,48 @@ authors:
9
9
  autorequire:
10
10
  bindir: exe
11
11
  cert_chain: []
12
- date: 2022-10-12 00:00:00.000000000 Z
12
+ date: 2024-01-06 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: cocoapods
16
16
  requirement: !ruby/object:Gem::Requirement
17
17
  requirements:
18
- - - "~>"
18
+ - - ">="
19
19
  - !ruby/object:Gem::Version
20
20
  version: 1.10.1
21
+ - - "<"
22
+ - !ruby/object:Gem::Version
23
+ version: '2.0'
21
24
  type: :runtime
22
25
  prerelease: false
23
26
  version_requirements: !ruby/object:Gem::Requirement
24
27
  requirements:
25
- - - "~>"
28
+ - - ">="
26
29
  - !ruby/object:Gem::Version
27
30
  version: 1.10.1
31
+ - - "<"
32
+ - !ruby/object:Gem::Version
33
+ version: '2.0'
28
34
  - !ruby/object:Gem::Dependency
29
35
  name: nokogiri
30
36
  requirement: !ruby/object:Gem::Requirement
31
37
  requirements:
32
- - - "~>"
38
+ - - ">="
33
39
  - !ruby/object:Gem::Version
34
40
  version: 1.11.2
41
+ - - "<"
42
+ - !ruby/object:Gem::Version
43
+ version: '2.0'
35
44
  type: :runtime
36
45
  prerelease: false
37
46
  version_requirements: !ruby/object:Gem::Requirement
38
47
  requirements:
39
- - - "~>"
48
+ - - ">="
40
49
  - !ruby/object:Gem::Version
41
50
  version: 1.11.2
51
+ - - "<"
52
+ - !ruby/object:Gem::Version
53
+ version: '2.0'
42
54
  - !ruby/object:Gem::Dependency
43
55
  name: rake
44
56
  requirement: !ruby/object:Gem::Requirement