cyclonedx-cocoapods 1.1.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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