cyclonedx-cocoapods 1.2.0 → 1.4.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: f9377b14e9d7b5f41db0b12693b58dd37367e05e72f8ab208178c6d79f38234b
4
- data.tar.gz: a8402aabb0a9eb157bbbaab4782a3fc6ce731003b48037c54cb1dc7e2d5fb289
3
+ metadata.gz: 4b3c1577d54844759e40218fb3b0876014aa8a2eac1a8ea2be2512cab13d79f9
4
+ data.tar.gz: dd99bd2aa09a1d6ecd956fdd3118a064df6662026b6816ee86f9a2f553347068
5
5
  SHA512:
6
- metadata.gz: 7fe8629cb7313126a55d2cbae4e7f69ea6fc365336b56093eab5a23e3a27d4fde848a892926ce2fc201509af9c9e4adb9cf59e0940841a03023e344817be2aa2
7
- data.tar.gz: 8cc8b64ddcf4292e14c4698e55899a2b6c72de7ed1ed5ee7adcef316fa315286e52b108a5d000a5fa6f6e42333fd752633b7eeb0142fa603b615b05fef688c8e
6
+ metadata.gz: 5e82c25c27de0fbede464d04a06b9b7f06c3fc79550041835395c6fe5aa32a0f1c4bba1d391b988ff6d39107f696960f064730de43ef8c0f0e8000d576cd1010
7
+ data.tar.gz: '048fa99979dd4e606b4952412dad3675bad2ebe3e45eccd8513089f18908d5594213031cb85130434f002d7a8edbafbb67818e602c648b49137768e8085c445b'
data/CHANGELOG.md CHANGED
@@ -4,6 +4,27 @@ 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.4.0]
8
+
9
+ ### Added
10
+ - Added `evidence` element to the component output to indicate that we are doing manifest analysis to generate the bom. ([Issue #69](https://github.com/CycloneDX/cyclonedx-cocoapods/issues/69)) [@macblazer](https://github.com/macblazer).
11
+
12
+ ### Fixed
13
+ - Added top level dependencies when the metadata/component is specified (by using the `--name`, `--version`, and `--type` parameters). ([PR #70](https://github.com/CycloneDX/cyclonedx-cocoapods/pull/70)) [@fnxpt](https://github.com/fnxpt)
14
+ - Properly concatenate paths to Podfile and Podfile.lock (with unit tests!). ([Issue #71](https://github.com/CycloneDX/cyclonedx-cocoapods/issues/71)) [@macblazer](https://github.com/macblazer).
15
+
16
+ ## [1.3.0]
17
+
18
+ ### Added
19
+ - Added optional `--shortened-strings` CLI parameter to limit the author, publisher, and purl lengths. ([Issue #65](https://github.com/CycloneDX/cyclonedx-cocoapods/issues/65)) [@macblazer](https://github.com/macblazer).
20
+
21
+ ### Changed
22
+ - Updated to use v1.5 of the CycloneDX specification. ([Issue #57](https://github.com/CycloneDX/cyclonedx-cocoapods/issues/57)) [@macblazer](https://github.com/macblazer)
23
+ - Code cleanup based on [RuboCop](https://rubocop.org/) analysis. ([Issue #45](https://github.com/CycloneDX/cyclonedx-cocoapods/issues/45)) [@macblazer](https://github.com/macblazer).
24
+
25
+ ### Fixed
26
+ - Following the specification to put the `bom-ref` attribute on `component` instead of as a `bomRef` element of `component`. [@macblazer](https://github.com/macblazer).
27
+
7
28
  ## [1.2.0]
8
29
 
9
30
  ### Added
data/README.md CHANGED
@@ -21,7 +21,7 @@ The CycloneDX CocoaPods Gem creates a valid CycloneDX software bill-of-material
21
21
 
22
22
  ### From Source
23
23
 
24
- First, clone/copy the source code from GitHub. Then in the source code directory run these ommands:
24
+ First, clone/copy the source code from GitHub. Then in the source code directory run these commands (substituting the actual version number for `x.x.x`):
25
25
 
26
26
  ```shell
27
27
  gem build cyclonedx-cocoapods.gemspec
@@ -32,7 +32,7 @@ Building from source requires Ruby 2.4.0 or newer.
32
32
 
33
33
  ## Compatibility
34
34
 
35
- *cyclonedx-cocoapods* aims to produce SBOMs according to the latest CycloneDX specification, which currently is [1.4](https://cyclonedx.org/docs/1.4/).
35
+ *cyclonedx-cocoapods* aims to produce SBOMs according to the latest CycloneDX specification, which currently is [1.5](https://cyclonedx.org/docs/1.5/xml/).
36
36
  You can use the [CycloneDX CLI](https://github.com/CycloneDX/cyclonedx-cli#convert-command) to convert between multiple BOM formats or specification versions.
37
37
 
38
38
  ## Usage
@@ -52,6 +52,7 @@ OPTIONS
52
52
  -o, --output bom_file_path Path to output the bom.xml file to (default: "bom.xml")
53
53
  -b, --bom-version bom_version Version of the generated BOM (default: "1")
54
54
  -x, --exclude-test-targets Eliminate Podfile targets whose name contains the word "test"
55
+ -s, --shortened-strings length Trim author, publisher, and purl to <length> characters; this may cause data loss but can improve compatibility with other systems
55
56
 
56
57
  Component Metadata
57
58
  -n, --name name (If specified version and type are also required) Name of the component for which the BOM is generated
@@ -1,4 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
2
4
  #
3
5
  # This file is part of CycloneDX CocoaPods
4
6
  #
@@ -18,6 +20,6 @@
18
20
  # Copyright (c) OWASP Foundation. All Rights Reserved.
19
21
  #
20
22
 
21
- require "cyclonedx/cocoapods/cli_runner"
23
+ require 'cyclonedx/cocoapods/cli_runner'
22
24
 
23
- CycloneDX::CocoaPods::CLIRunner.new().run
25
+ CycloneDX::CocoaPods::CLIRunner.new.run
@@ -59,18 +59,73 @@ module CycloneDX
59
59
  CHECKSUM_ALGORITHM = 'SHA-1'
60
60
  HOMEPAGE_REFERENCE_TYPE = 'website'
61
61
 
62
+ def source_qualifier
63
+ return '' if source.nil? || source.source_qualifier.empty?
64
+
65
+ "?#{source.source_qualifier.map do |key, value|
66
+ "#{key}=#{CGI.escape(value)}"
67
+ end.join('&')}"
68
+ end
69
+
70
+ def purl_subpath
71
+ return '' unless name.split('/').length > 1
72
+
73
+ "##{name.split('/').drop(1).map do |component|
74
+ CGI.escape(component)
75
+ end.join('/')}"
76
+ end
77
+
62
78
  def purl
63
79
  purl_name = CGI.escape(name.split('/').first)
64
- source_qualifier = source.nil? || source.source_qualifier.empty? ? '' : "?#{source.source_qualifier.map { |key, value| "#{key}=#{CGI.escape(value)}" }.join('&')}"
65
- purl_subpath = name.split('/').length > 1 ? "##{name.split('/').drop(1).map { |component| CGI.escape(component) }.join('/')}" : ''
66
- return "pkg:cocoapods/#{purl_name}@#{CGI.escape(version.to_s)}#{source_qualifier}#{purl_subpath}"
80
+ src_qualifier = source_qualifier
81
+ subpath = purl_subpath
82
+ "pkg:cocoapods/#{purl_name}@#{CGI.escape(version.to_s)}#{src_qualifier}#{subpath}"
67
83
  end
68
84
 
69
- def add_to_bom(xml)
70
- xml.component(type: 'library') do
71
- xml.author author unless author.nil?
72
- xml.publisher author unless author.nil?
73
- xml.name name
85
+ def xml_add_author(xml, trim_strings_length)
86
+ return if author.nil?
87
+
88
+ if trim_strings_length.zero?
89
+ xml.author author
90
+ xml.publisher author
91
+ else
92
+ xml.author author.slice(0, trim_strings_length)
93
+ xml.publisher author.slice(0, trim_strings_length)
94
+ end
95
+ end
96
+
97
+ def xml_add_homepage(xml)
98
+ return if homepage.nil?
99
+
100
+ xml.externalReferences do
101
+ xml.reference(type: HOMEPAGE_REFERENCE_TYPE) do
102
+ xml.url homepage
103
+ end
104
+ end
105
+ end
106
+
107
+ # Add evidence of the purl identity.
108
+ # See https://github.com/CycloneDX/guides/blob/main/SBOM/en/0x60-Evidence.md for more info
109
+ def xml_add_evidence(xml, manifest_path)
110
+ xml.evidence do
111
+ xml.identity do
112
+ xml.field 'purl'
113
+ xml.confidence '0.6'
114
+ xml.methods_ do
115
+ xml.method_ do
116
+ xml.technique 'manifest-analysis'
117
+ xml.confidence '0.6'
118
+ xml.value manifest_path
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end
124
+
125
+ def add_to_bom(xml, manifest_path, trim_strings_length = 0)
126
+ xml.component(type: 'library', 'bom-ref': purl) do
127
+ xml_add_author(xml, trim_strings_length)
128
+ xml.name_ name
74
129
  xml.version version.to_s
75
130
  xml.description { xml.cdata description } unless description.nil?
76
131
  unless checksum.nil?
@@ -83,15 +138,14 @@ module CycloneDX
83
138
  license.add_to_bom(xml)
84
139
  end
85
140
  end
86
- xml.purl purl
87
- xml.bomRef purl
88
- unless homepage.nil?
89
- xml.externalReferences do
90
- xml.reference(type: HOMEPAGE_REFERENCE_TYPE) do
91
- xml.url homepage
92
- end
93
- end
141
+ if trim_strings_length.zero?
142
+ xml.purl purl
143
+ else
144
+ xml.purl purl.slice(0, trim_strings_length)
94
145
  end
146
+ xml_add_homepage(xml)
147
+
148
+ xml_add_evidence(xml, manifest_path)
95
149
  end
96
150
  end
97
151
 
@@ -99,7 +153,7 @@ module CycloneDX
99
153
  def add_to_bom(xml)
100
154
  xml.license do
101
155
  xml.id identifier if identifier_type == :id
102
- xml.name identifier if identifier_type == :name
156
+ xml.name_ identifier if identifier_type == :name
103
157
  xml.text_ text unless text.nil?
104
158
  xml.url url unless url.nil?
105
159
  end
@@ -109,51 +163,71 @@ module CycloneDX
109
163
 
110
164
  class Component
111
165
  def add_to_bom(xml)
112
- xml.component(type: type) do
166
+ xml.component(type: type, 'bom-ref': bomref) do
113
167
  xml.group group unless group.nil?
114
- xml.name name
168
+ xml.name_ name
115
169
  xml.version version
116
170
  end
117
171
  end
118
172
  end
119
173
 
174
+ # Turns the internal model data into an XML bom.
120
175
  class BOMBuilder
121
- NAMESPACE = 'http://cyclonedx.org/schema/bom/1.4'
176
+ NAMESPACE = 'http://cyclonedx.org/schema/bom/1.5'
122
177
 
123
- attr_reader :component, :pods, :dependencies
178
+ attr_reader :component, :pods, :manifest_path, :dependencies
124
179
 
125
- def initialize(pods:, component: nil, dependencies: nil)
180
+ def initialize(pods:, manifest_path:, component: nil, dependencies: nil)
126
181
  @pods = pods.sort_by(&:purl)
182
+ @manifest_path = manifest_path
127
183
  @component = component
128
184
  @dependencies = dependencies&.sort
129
185
  end
130
186
 
131
- def bom(version: 1)
132
- raise ArgumentError, "Incorrect version: #{version} should be an integer greater than 0" unless version.to_i > 0
187
+ def bom(version: 1, trim_strings_length: 0)
188
+ unless version.to_i.positive?
189
+ raise ArgumentError,
190
+ "Incorrect version: #{version} should be an integer greater than 0"
191
+ end
192
+
193
+ unless trim_strings_length.is_a?(Integer) && (trim_strings_length.positive? || trim_strings_length.zero?)
194
+ raise ArgumentError,
195
+ "Incorrect string length: #{trim_strings_length} should be an integer greater than 0"
196
+ end
133
197
 
198
+ unchecked_bom(version: version, trim_strings_length: trim_strings_length)
199
+ end
200
+
201
+ private
202
+
203
+ # does not verify parameters because the public method does that.
204
+ def unchecked_bom(version: 1, trim_strings_length: 0)
134
205
  Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml|
135
- xml.bom('xmlns': NAMESPACE, 'version': version.to_i.to_s, 'serialNumber': "urn:uuid:#{SecureRandom.uuid}") do
206
+ xml.bom(xmlns: NAMESPACE, version: version.to_i.to_s, serialNumber: "urn:uuid:#{SecureRandom.uuid}") do
136
207
  bom_metadata(xml)
137
- xml.components do
138
- pods.each do |pod|
139
- pod.add_to_bom(xml)
140
- end
141
- end
142
208
 
143
- xml.dependencies do
144
- bom_dependencies(xml, dependencies)
145
- end
209
+ bom_components(xml, pods, manifest_path, trim_strings_length)
210
+
211
+ bom_dependencies(xml, dependencies)
146
212
  end
147
213
  end.to_xml
148
214
  end
149
215
 
150
- private
216
+ def bom_components(xml, pods, manifest_path, trim_strings_length)
217
+ xml.components do
218
+ pods.each do |pod|
219
+ pod.add_to_bom(xml, manifest_path, trim_strings_length)
220
+ end
221
+ end
222
+ end
151
223
 
152
224
  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)
225
+ xml.dependencies do
226
+ dependencies&.each do |key, array|
227
+ xml.dependency(ref: key) do
228
+ array.sort.each do |value|
229
+ xml.dependency(ref: value)
230
+ end
157
231
  end
158
232
  end
159
233
  end
@@ -162,14 +236,18 @@ module CycloneDX
162
236
  def bom_metadata(xml)
163
237
  xml.metadata do
164
238
  xml.timestamp Time.now.getutc.strftime('%Y-%m-%dT%H:%M:%SZ')
165
- xml.tools do
166
- xml.tool do
167
- xml.vendor 'CycloneDX'
168
- xml.name 'cyclonedx-cocoapods'
169
- xml.version VERSION
170
- end
239
+ bom_tools(xml)
240
+ component&.add_to_bom(xml)
241
+ end
242
+ end
243
+
244
+ def bom_tools(xml)
245
+ xml.tools do
246
+ xml.tool do
247
+ xml.vendor 'CycloneDX'
248
+ xml.name_ 'cyclonedx-cocoapods'
249
+ xml.version VERSION
171
250
  end
172
- component.add_to_bom(xml) unless component.nil?
173
251
  end
174
252
  end
175
253
  end
@@ -19,6 +19,7 @@
19
19
  # Copyright (c) OWASP Foundation. All Rights Reserved.
20
20
  #
21
21
 
22
+ require 'English'
22
23
  require 'logger'
23
24
  require 'optparse'
24
25
 
@@ -30,34 +31,26 @@ module CycloneDX
30
31
  module CocoaPods
31
32
  class BOMOutputError < StandardError; end
32
33
 
34
+ # Interprets CLI parameters and runs the main workflow.
33
35
  class CLIRunner
34
36
  def run
35
- begin
36
- setup_logger # Needed in case we have errors while processing CLI parameters
37
- options = parseOptions
38
- setup_logger(verbose: options[:verbose])
39
- @logger.debug "Running cyclonedx-cocoapods with options: #{options}"
40
-
41
- analyzer = PodfileAnalyzer.new(logger: @logger, exclude_test_targets: options[:exclude_test_targets])
42
- podfile, lockfile = analyzer.ensure_podfile_and_lock_are_present(options)
43
- pods, dependencies = analyzer.parse_pods(podfile, lockfile)
44
- analyzer.populate_pods_with_additional_info(pods)
45
-
46
- builder = BOMBuilder.new(pods: pods, component: component_from_options(options), dependencies: dependencies)
47
- bom = builder.bom(version: options[:bom_version] || 1)
48
- write_bom_to_file(bom: bom, options: options)
49
- rescue StandardError => e
50
- @logger.error ([e.message] + e.backtrace).join($/)
51
- exit 1
52
- end
53
- end
37
+ setup_logger # Needed in case we have errors while processing CLI parameters
38
+ options = parse_options
39
+ setup_logger(verbose: options[:verbose])
40
+ @logger.debug "Running cyclonedx-cocoapods with options: #{options}"
54
41
 
42
+ component, pods, manifest_path, dependencies = analyze(options)
55
43
 
56
- private
44
+ build_and_write_bom(options, component, pods, manifest_path, dependencies)
45
+ rescue StandardError => e
46
+ @logger.error ([e.message] + e.backtrace).join($INPUT_RECORD_SEPARATOR)
47
+ exit 1
48
+ end
57
49
 
50
+ private
58
51
 
59
- def parseOptions
60
- parsedOptions = {}
52
+ def parse_options
53
+ parsed_options = {}
61
54
  component_types = Component::VALID_COMPONENT_TYPES
62
55
  OptionParser.new do |options|
63
56
  options.banner = <<~BANNER
@@ -71,7 +64,7 @@ module CycloneDX
71
64
  BANNER
72
65
 
73
66
  options.on('--[no-]verbose', 'Show verbose debugging output') do |v|
74
- parsedOptions[:verbose] = v
67
+ parsed_options[:verbose] = v
75
68
  end
76
69
  options.on('-h', '--help', 'Show help message') do
77
70
  puts options
@@ -80,71 +73,128 @@ module CycloneDX
80
73
 
81
74
  options.separator("\n BOM Generation")
82
75
  options.on('-p', '--path path', 'Path to CocoaPods project directory (default: current directory)') do |path|
83
- parsedOptions[:path] = path
76
+ parsed_options[:path] = path
77
+ end
78
+ options.on('-o', '--output bom_file_path',
79
+ 'Path to output the bom.xml file to (default: "bom.xml")') do |bom_file_path|
80
+ parsed_options[:bom_file_path] = bom_file_path
84
81
  end
85
- options.on('-o', '--output bom_file_path', 'Path to output the bom.xml file to (default: "bom.xml")') do |bom_file_path|
86
- parsedOptions[:bom_file_path] = bom_file_path
82
+ options.on('-b', '--bom-version bom_version', Integer,
83
+ 'Version of the generated BOM (default: "1")') do |version|
84
+ parsed_options[:bom_version] = version
87
85
  end
88
- options.on('-b', '--bom-version bom_version', Integer, 'Version of the generated BOM (default: "1")') do |version|
89
- parsedOptions[:bom_version] = version
86
+ options.on('-x', '--exclude-test-targets',
87
+ 'Eliminate Podfile targets whose name contains the word "test"') do |exclude|
88
+ parsed_options[:exclude_test_targets] = exclude
90
89
  end
91
- options.on('-x', '--exclude-test-targets', 'Eliminate Podfile targets whose name contains the word "test"') do |exclude|
92
- parsedOptions[:exclude_test_targets] = exclude
90
+ options.on('-s', '--shortened-strings length', Integer,
91
+ 'Trim author, publisher, and purl to <length> characters; this may ' \
92
+ 'cause data loss but can improve compatibility with other systems') do |shortened_strings|
93
+ parsed_options[:trim_strings_length] = shortened_strings
93
94
  end
94
95
 
95
96
  options.separator("\n Component Metadata\n")
96
- options.on('-n', '--name name', '(If specified version and type are also required) Name of the component for which the BOM is generated') do |name|
97
- parsedOptions[:name] = name
97
+ options.on('-n', '--name name',
98
+ '(If specified version and type are also required) Name of the ' \
99
+ 'component for which the BOM is generated') do |name|
100
+ parsed_options[:name] = name
98
101
  end
99
102
  options.on('-v', '--version version', 'Version of the component for which the BOM is generated') do |version|
100
103
  begin
101
104
  Gem::Version.new(version)
102
- parsedOptions[:version] = version
105
+ parsed_options[:version] = version
103
106
  rescue StandardError => e
104
107
  raise OptionParser::InvalidArgument, e.message
105
108
  end
106
109
  end
107
- options.on('-t', '--type type', "Type of the component for which the BOM is generated (one of #{component_types.join('|')})") do |type|
108
- raise OptionParser::InvalidArgument, "Invalid value for component's type (#{type}). It must be one of #{component_types.join('|')}" unless component_types.include?(type)
109
- parsedOptions[:type] = type
110
+ options.on('-t', '--type type',
111
+ 'Type of the component for which the BOM is generated ' \
112
+ "(one of #{component_types.join('|')})") do |type|
113
+ unless component_types.include?(type)
114
+ raise OptionParser::InvalidArgument,
115
+ "Invalid value for component's type (#{type}). It must be one of #{component_types.join('|')}"
116
+ end
117
+
118
+ parsed_options[:type] = type
110
119
  end
111
120
  options.on('-g', '--group group', 'Group of the component for which the BOM is generated') do |group|
112
- parsedOptions[:group] = group
121
+ parsed_options[:group] = group
113
122
  end
114
123
  end.parse!
115
124
 
116
- raise OptionParser::InvalidArgument, 'You must also specify --version and --type if --name is provided' if !parsedOptions[:name].nil? && (parsedOptions[:version].nil? || parsedOptions[:type].nil?)
117
- return parsedOptions
125
+ if !parsed_options[:name].nil? && (parsed_options[:version].nil? || parsed_options[:type].nil?)
126
+ raise OptionParser::InvalidArgument,
127
+ 'You must also specify --version and --type if --name is provided'
128
+ end
129
+
130
+ parsed_options
118
131
  end
119
132
 
133
+ def analyze(options)
134
+ analyzer = PodfileAnalyzer.new(logger: @logger, exclude_test_targets: options[:exclude_test_targets])
135
+ podfile, lockfile = analyzer.ensure_podfile_and_lock_are_present(options)
136
+ pods, dependencies = analyzer.parse_pods(podfile, lockfile)
137
+ analyzer.populate_pods_with_additional_info(pods)
120
138
 
121
- def component_from_options(options)
122
- Component.new(group: options[:group], name: options[:name], version: options[:version], type: options[:type]) if options[:name]
139
+ component = component_from_options(options)
140
+
141
+ unless component.nil?
142
+ # add top level pods to main component
143
+ top_deps = analyzer.top_level_deps(podfile, lockfile)
144
+ dependencies[component.bomref] = top_deps
145
+ end
146
+
147
+ manifest_path = lockfile.defined_in_file
148
+ if manifest_path.absolute?
149
+ # Use the folder that we are building in, then the path to the manifest file
150
+ manifest_path = Pathname.pwd.basename + manifest_path.relative_path_from(Pathname.pwd)
151
+ end
152
+
153
+ [component, pods, manifest_path, dependencies]
123
154
  end
124
155
 
156
+ def build_and_write_bom(options, component, pods, manifest_path, dependencies)
157
+ builder = BOMBuilder.new(pods: pods, manifest_path: manifest_path,
158
+ component: component, dependencies: dependencies)
159
+ bom = builder.bom(version: options[:bom_version] || 1,
160
+ trim_strings_length: options[:trim_strings_length] || 0)
161
+ write_bom_to_file(bom: bom, options: options)
162
+ end
163
+
164
+ def component_from_options(options)
165
+ return unless options[:name]
166
+
167
+ Component.new(group: options[:group], name: options[:name], version: options[:version],
168
+ type: options[:type])
169
+ end
125
170
 
126
171
  def setup_logger(verbose: true)
127
172
  @logger ||= Logger.new($stdout)
128
173
  @logger.level = verbose ? Logger::DEBUG : Logger::INFO
129
174
  end
130
175
 
131
-
132
176
  def write_bom_to_file(bom:, options:)
177
+ bom_file_path = prep_for_bom_write(options)
178
+
179
+ begin
180
+ File.write(bom_file_path, bom)
181
+ @logger.info "BOM written to #{bom_file_path}"
182
+ rescue StandardError
183
+ raise BOMOutputError, "Unable to write the BOM to #{bom_file_path}"
184
+ end
185
+ end
186
+
187
+ def prep_for_bom_write(options)
133
188
  bom_file_path = Pathname.new(options[:bom_file_path] || './bom.xml').expand_path
134
189
  bom_dir = bom_file_path.dirname
135
190
 
136
191
  begin
137
192
  FileUtils.mkdir_p(bom_dir) unless bom_dir.directory?
138
- rescue
193
+ rescue StandardError
139
194
  raise BOMOutputError, "Unable to create the BOM output directory at #{bom_dir}"
140
195
  end
141
196
 
142
- begin
143
- File.open(bom_file_path, 'w') { |file| file.write(bom) }
144
- @logger.info "BOM written to #{bom_file_path}"
145
- rescue
146
- raise BOMOutputError, "Unable to write the BOM to #{bom_file_path}"
147
- end
197
+ bom_file_path
148
198
  end
149
199
  end
150
200
  end
@@ -24,15 +24,26 @@ module CycloneDX
24
24
  class Component
25
25
  VALID_COMPONENT_TYPES = %w[application framework library container operating-system device firmware file].freeze
26
26
 
27
- attr_reader :group, :name, :version, :type
27
+ attr_reader :group, :name, :version, :type, :bomref
28
+
29
+ def initialize(name:, version:, type:, group: nil)
30
+ raise ArgumentError, 'Group, if specified, must be non empty' if !group.nil? && group.to_s.strip.empty?
31
+ raise ArgumentError, 'Name must be non empty' if name.nil? || name.to_s.strip.empty?
28
32
 
29
- def initialize(group: nil, name:, version:, type:)
30
- raise ArgumentError, "Group, if specified, must be non empty" if !group.nil? && group.to_s.strip.empty?
31
- raise ArgumentError, "Name must be non empty" if name.nil? || name.to_s.strip.empty?
32
33
  Gem::Version.new(version) # To check that the version string is well formed
33
- raise ArgumentError, "#{type} is not valid component type (#{VALID_COMPONENT_TYPES.join('|')})" unless VALID_COMPONENT_TYPES.include?(type)
34
+ unless VALID_COMPONENT_TYPES.include?(type)
35
+ raise ArgumentError, "#{type} is not valid component type (#{VALID_COMPONENT_TYPES.join('|')})"
36
+ end
37
+
38
+ @group = group
39
+ @name = name
40
+ @version = version
41
+ @type = type
42
+ @bomref = "#{name}@#{version}"
43
+
44
+ return if group.nil?
34
45
 
35
- @group, @name, @version, @type = group, name, version, type
46
+ @bomref = "#{group}/#{@bomref}"
36
47
  end
37
48
  end
38
49
  end
@@ -26,15 +26,13 @@ module CycloneDX
26
26
  class Pod
27
27
  class License
28
28
  SPDX_LICENSES = JSON.parse(File.read("#{__dir__}/spdx-licenses.json")).freeze
29
- IDENTIFIER_TYPES = [:id, :name].freeze
29
+ IDENTIFIER_TYPES = %i[id name].freeze
30
30
 
31
- attr_reader :identifier
32
- attr_reader :identifier_type
33
- attr_accessor :text
34
- attr_accessor :url
31
+ attr_reader :identifier, :identifier_type
32
+ attr_accessor :text, :url
35
33
 
36
34
  def initialize(identifier:)
37
- raise ArgumentError, "License identifier must be non empty" if identifier.nil? || identifier.to_s.strip.empty?
35
+ raise ArgumentError, 'License identifier must be non empty' if identifier.nil? || identifier.to_s.strip.empty?
38
36
 
39
37
  @identifier = SPDX_LICENSES.find { |license_id| license_id.downcase == identifier.to_s.downcase }
40
38
  @identifier_type = @identifier.nil? ? :name : :id
@@ -25,26 +25,40 @@ require_relative 'license'
25
25
  module CycloneDX
26
26
  module CocoaPods
27
27
  class Pod
28
- attr_reader :name # xs:normalizedString
29
- attr_reader :version # xs:normalizedString
30
- attr_reader :source # Anything responding to :source_qualifier
31
- attr_reader :homepage # xs:anyURI - https://cyclonedx.org/docs/1.4/#type_externalReference
32
- attr_reader :checksum # https://cyclonedx.org/docs/1.4/#type_hashValue (We only use SHA-1 hashes - length == 40)
33
- attr_reader :author # xs:normalizedString
34
- attr_reader :description # xs:normalizedString
35
- attr_reader :license # https://cyclonedx.org/docs/1.4/#type_licenseType
36
- # We don't currently support several licenses or license expressions https://spdx.github.io/spdx-spec/appendix-IV-SPDX-license-expressions/
28
+ # xs:normalizedString
29
+ attr_reader :name
30
+ # xs:normalizedString
31
+ attr_reader :version
32
+ # Anything responding to :source_qualifier
33
+ attr_reader :source
34
+ # xs:anyURI - https://cyclonedx.org/docs/1.5/xml/#type_externalReference
35
+ attr_reader :homepage
36
+ # https://cyclonedx.org/docs/1.5/xml/#type_hashValue (We only use SHA-1 hashes - length == 40)
37
+ attr_reader :checksum
38
+ # xs:normalizedString
39
+ attr_reader :author
40
+ # xs:normalizedString
41
+ attr_reader :description
42
+ # https://cyclonedx.org/docs/1.5/xml/#type_licenseType
43
+ # We don't currently support several licenses or license expressions https://spdx.github.io/spdx-spec/appendix-IV-SPDX-license-expressions/
44
+ attr_reader :license
45
+
37
46
  def initialize(name:, version:, source: nil, checksum: nil)
38
- raise ArgumentError, "Name must be non empty" if name.nil? || name.to_s.empty?
47
+ raise ArgumentError, 'Name must be non empty' if name.nil? || name.to_s.empty?
39
48
  raise ArgumentError, "Name shouldn't contain spaces" if name.to_s.include?(' ')
40
49
  raise ArgumentError, "Name shouldn't start with a dot" if name.to_s.start_with?('.')
50
+
41
51
  # `pod create` also enforces no plus sign, but more than 500 public pods have a plus in the root name.
42
52
  # https://github.com/CocoaPods/CocoaPods/blob/9461b346aeb8cba6df71fd4e71661688138ec21b/lib/cocoapods/command/lib/create.rb#L35
43
53
 
44
54
  Gem::Version.new(version) # To check that the version string is well formed
45
- raise ArgumentError, "Invalid pod source" unless source.nil? || source.respond_to?(:source_qualifier)
55
+ raise ArgumentError, 'Invalid pod source' unless source.nil? || source.respond_to?(:source_qualifier)
46
56
  raise ArgumentError, "#{checksum} is not valid SHA-1 hash" unless checksum.nil? || checksum =~ /[a-fA-F0-9]{40}/
47
- @name, @version, @source, @checksum = name.to_s, version, source, checksum
57
+
58
+ @name = name.to_s
59
+ @version = version
60
+ @source = source
61
+ @checksum = checksum
48
62
  end
49
63
 
50
64
  def root_name
@@ -61,23 +75,21 @@ module CycloneDX
61
75
  end
62
76
 
63
77
  def to_s
64
- "Pod<#{name}, #{version.to_s}>"
78
+ "Pod<#{name}, #{version}>"
65
79
  end
66
80
 
67
81
  private
68
82
 
69
83
  def populate_author(attributes)
70
84
  authors = attributes[:author] || attributes[:authors]
71
- case authors
72
- when String
73
- @author = authors
74
- when Array
75
- @author = authors.join(', ')
76
- when Hash
77
- @author = authors.map { |name, email| "#{name} <#{email}>" }.join(', ')
78
- else
79
- @author = nil
80
- end
85
+ @author = case authors
86
+ when String
87
+ authors
88
+ when Array
89
+ authors.join(', ')
90
+ when Hash
91
+ authors.map { |name, email| "#{name} <#{email}>" }.join(', ')
92
+ end
81
93
  end
82
94
 
83
95
  def populate_description(attributes)
@@ -89,19 +101,23 @@ module CycloneDX
89
101
  when String
90
102
  @license = License.new(identifier: attributes[:license])
91
103
  when Hash
92
- attributes[:license].transform_keys!(&:to_sym)
93
- identifier = attributes[:license][:type]
94
- unless identifier.nil? || identifier.to_s.strip.empty?
95
- @license = License.new(identifier: identifier)
96
- @license.text = attributes[:license][:text]
97
- else
98
- @license = nil
99
- end
104
+ populate_hashed_license(attributes)
100
105
  else
101
106
  @license = nil
102
107
  end
103
108
  end
104
109
 
110
+ def populate_hashed_license(attributes)
111
+ attributes[:license].transform_keys!(&:to_sym)
112
+ identifier = attributes[:license][:type]
113
+ if identifier.nil? || identifier.to_s.strip.empty?
114
+ @license = nil
115
+ else
116
+ @license = License.new(identifier: identifier)
117
+ @license.text = attributes[:license][:text]
118
+ end
119
+ end
120
+
105
121
  def populate_homepage(attributes)
106
122
  @homepage = attributes[:homepage]
107
123
  end
@@ -30,19 +30,35 @@ module CycloneDX
30
30
  def self.searchable_source(url:, source_manager:)
31
31
  source = CocoaPodsRepository.new(url: url)
32
32
  source.source_manager = source_manager
33
- return source
33
+ source
34
34
  end
35
35
 
36
36
  def attributes_for(pod:)
37
37
  specification_sets = @source_manager.search_by_name("^#{Regexp.escape(pod.root_name)}$")
38
- raise SearchError, "No pod found named #{pod.name}; run 'pod repo update' and try again" if specification_sets.length == 0
39
- raise SearchError, "More than one pod found named #{pod.name}; a pod in a private spec repo should not have the same name as a public pod" if specification_sets.length > 1
38
+ validate_spec_sets(specification_sets, pod)
40
39
 
41
40
  paths = specification_sets[0].specification_paths_for_version(pod.version)
42
- raise SearchError, "Version #{pod.version} not found for pod #{pod.name}; run 'pod repo update' and try again" if paths.length == 0
41
+ if paths.empty?
42
+ raise SearchError,
43
+ "Version #{pod.version} not found for pod #{pod.name}; run 'pod repo update' and try again"
44
+ end
43
45
 
44
46
  ::Pod::Specification.from_file(paths[0]).attributes_hash
45
47
  end
48
+
49
+ private
50
+
51
+ def validate_spec_sets(specification_sets, pod)
52
+ if specification_sets.empty?
53
+ raise SearchError,
54
+ "No pod found named #{pod.name}; run 'pod repo update' and try again"
55
+ end
56
+ return unless specification_sets.length > 1
57
+
58
+ raise SearchError,
59
+ "More than one pod found named #{pod.name}; a pod in a private spec repo " \
60
+ 'should not have the same name as a public pod'
61
+ end
46
62
  end
47
63
 
48
64
  class GitRepository
@@ -64,7 +80,6 @@ module CycloneDX
64
80
  end
65
81
  end
66
82
 
67
-
68
83
  class Pod
69
84
  def complete_information_from_source
70
85
  populate(source.attributes_for(pod: self))
@@ -31,36 +31,17 @@ module CycloneDX
31
31
  module CocoaPods
32
32
  class PodfileParsingError < StandardError; end
33
33
 
34
+ # Uses cocoapods to analyze the Podfile and Podfile.lock for component dependency information
34
35
  class PodfileAnalyzer
35
36
  def initialize(logger:, exclude_test_targets: false)
36
37
  @logger = logger
37
38
  @exclude_test_targets = exclude_test_targets
38
39
  end
39
40
 
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
-
57
41
  def ensure_podfile_and_lock_are_present(options)
58
42
  project_dir = Pathname.new(options[:path] || Dir.pwd)
59
- raise PodfileParsingError, "#{options[:path]} is not a valid directory." unless File.directory?(project_dir)
60
- options[:podfile_path] = project_dir + 'Podfile'
61
- raise PodfileParsingError, "Missing Podfile in #{project_dir}. Please use the --path option if not running from the CocoaPods project directory." unless File.exist?(options[:podfile_path])
62
- options[:podfile_lock_path] = project_dir + 'Podfile.lock'
63
- raise PodfileParsingError, "Missing Podfile.lock, please run 'pod install' before generating BOM" unless File.exist?(options[:podfile_lock_path])
43
+
44
+ validate_options(project_dir, options)
64
45
 
65
46
  initialize_cocoapods_config(project_dir)
66
47
 
@@ -68,81 +49,145 @@ module CycloneDX
68
49
  verify_synced_sandbox(lockfile)
69
50
  load_plugins(options[:podfile_path])
70
51
 
71
- return ::Pod::Podfile.from_file(options[:podfile_path]), lockfile
52
+ [::Pod::Podfile.from_file(options[:podfile_path]), lockfile]
72
53
  end
73
54
 
74
-
75
55
  def parse_pods(podfile, lockfile)
76
56
  @logger.debug "Parsing pods from #{podfile.defined_in_file}"
77
57
  included_pods, dependencies = create_list_of_included_pods(podfile, lockfile)
78
58
 
79
59
  pods = lockfile.pod_names.select { |name| included_pods.include?(name) }.map do |name|
80
- Pod.new(name: name, version: lockfile.version(name), source: source_for_pod(podfile, lockfile, name), checksum: lockfile.checksum(name))
60
+ Pod.new(name: name, version: lockfile.version(name), source: source_for_pod(podfile, lockfile, name),
61
+ checksum: lockfile.checksum(name))
81
62
  end
82
63
 
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))
64
+ pod_dependencies = parse_dependencies(dependencies, podfile, lockfile)
87
65
 
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
66
+ [pods, pod_dependencies]
96
67
  end
97
68
 
98
-
99
69
  def populate_pods_with_additional_info(pods)
100
70
  pods.each do |pod|
101
71
  @logger.debug "Completing information for #{pod.name}"
102
72
  pod.complete_information_from_source
103
73
  end
104
- return pods
74
+ pods
75
+ end
76
+
77
+ def top_level_deps(podfile, lockfile)
78
+ pods_used = top_level_pods(podfile)
79
+ dependencies_for_pod(pods_used, podfile, lockfile)
105
80
  end
106
81
 
107
82
  private
108
83
 
84
+ def load_plugins(podfile_path)
85
+ podfile_contents = File.read(podfile_path)
86
+ plugin_syntax = /\s*plugin\s+['"]([^'"]+)['"]/
87
+ plugin_names = podfile_contents.scan(plugin_syntax).flatten
88
+
89
+ plugin_names.each do |plugin_name|
90
+ load_one_plugin(plugin_name)
91
+ end
92
+ end
93
+
94
+ def load_one_plugin(plugin_name)
95
+ @logger.debug("Loading plugin #{plugin_name}")
96
+ begin
97
+ plugin_spec = Gem::Specification.find_by_name(plugin_name)
98
+ plugin_spec&.activate
99
+ load("#{plugin_spec.gem_dir}/lib/cocoapods_plugin.rb") if plugin_spec
100
+ rescue Gem::LoadError => e
101
+ @logger.warn("Failed to load plugin #{plugin_name}. #{e.message}")
102
+ end
103
+ end
104
+
105
+ def validate_options(project_dir, options)
106
+ raise PodfileParsingError, "#{options[:path]} is not a valid directory." unless File.directory?(project_dir)
107
+
108
+ options[:podfile_path] = project_dir + 'Podfile'
109
+ unless File.exist?(options[:podfile_path])
110
+ raise PodfileParsingError, "Missing Podfile in #{project_dir}. Please use the --path option if " \
111
+ 'not running from the CocoaPods project directory.'
112
+ end
113
+
114
+ options[:podfile_lock_path] = project_dir + 'Podfile.lock'
115
+ return if File.exist?(options[:podfile_lock_path])
116
+
117
+ raise PodfileParsingError, "Missing Podfile.lock, please run 'pod install' before generating BOM"
118
+ end
119
+
120
+ def parse_dependencies(dependencies, podfile, lockfile)
121
+ pod_dependencies = {}
122
+ dependencies.each do |key, podname_array|
123
+ next unless lockfile.pod_names.include? key
124
+
125
+ pod = Pod.new(name: key, version: lockfile.version(key), source: source_for_pod(podfile, lockfile, key),
126
+ checksum: lockfile.checksum(key))
127
+
128
+ pod_dependencies[pod.purl] = dependencies_for_pod(podname_array, podfile, lockfile)
129
+ end
130
+
131
+ pod_dependencies
132
+ end
133
+
134
+ def dependencies_for_pod(podname_array, podfile, lockfile)
135
+ lockfile.pod_names.select { |name| podname_array.include?(name) }.map do |name|
136
+ pod = Pod.new(name: name,
137
+ version: lockfile.version(name),
138
+ source: source_for_pod(podfile, lockfile, name),
139
+ checksum: lockfile.checksum(name))
140
+ pod.purl
141
+ end
142
+ end
109
143
 
110
144
  def initialize_cocoapods_config(project_dir)
145
+ # First, reset the ::Pod::Config instance in case we need to use this analyzer on multiple pods
146
+ ::Pod::Config.instance = nil
111
147
  ::Pod::Config.instance.installation_root = project_dir
112
148
  end
113
149
 
114
-
115
150
  def verify_synced_sandbox(lockfile)
116
- manifestFile = ::Pod::Config.instance.sandbox.manifest
117
- raise PodfileParsingError, "Missing Manifest.lock, please run 'pod install' before generating BOM" if manifestFile.nil?
118
- raise PodfileParsingError, "The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation." unless lockfile == manifestFile
151
+ manifest_file = ::Pod::Config.instance.sandbox.manifest
152
+ if manifest_file.nil?
153
+ raise PodfileParsingError,
154
+ "Missing Manifest.lock, please run 'pod install' before generating BOM"
155
+ end
156
+ return if lockfile == manifest_file
157
+
158
+ raise PodfileParsingError,
159
+ "The sandbox is not in sync with the Podfile.lock. Run 'pod install' " \
160
+ 'or update your CocoaPods installation.'
119
161
  end
120
162
 
121
163
  def simple_hash_of_lockfile_pods(lockfile)
122
- pods_hash = { }
164
+ pods_hash = {}
123
165
 
124
166
  pods_used = lockfile.internal_data['PODS']
125
- pods_used&.each { |pod|
126
- if pod.is_a?(String)
127
- # Pods stored as String have no dependencies
128
- pod_name = pod.split.first
129
- pods_hash[pod_name] = []
130
- else
131
- # Pods stored as a hash have pod name and dependencies.
132
- pod.each { |pod, dependencies|
133
- pod_name = pod.split.first
134
- pods_hash[pod_name] = dependencies.map { |d| d.split.first }
135
- }
136
- end
137
- }
167
+ pods_used&.each do |pod|
168
+ map_single_pod(pod, pods_hash)
169
+ end
138
170
  pods_hash
139
171
  end
140
172
 
173
+ def map_single_pod(pod, pods_hash)
174
+ if pod.is_a?(String)
175
+ # Pods stored as String have no dependencies
176
+ pod_name = pod.split.first
177
+ pods_hash[pod_name] = []
178
+ else
179
+ # Pods stored as a hash have pod name and dependencies.
180
+ pod.each do |pod, dependencies|
181
+ pod_name = pod.split.first
182
+ pods_hash[pod_name] = dependencies.map { |d| d.split.first }
183
+ end
184
+ end
185
+ end
141
186
 
142
187
  def append_all_pod_dependencies(pods_used, pods_cache)
143
188
  result = pods_used
144
189
  original_number = 0
145
- dependencies_hash = { }
190
+ dependencies_hash = {}
146
191
 
147
192
  # Loop adding pod dependencies until we are not adding any more dependencies to the result
148
193
  # This brings in all the transitive dependencies of every top level pod.
@@ -152,76 +197,83 @@ module CycloneDX
152
197
  while result.length != original_number
153
198
  original_number = result.length
154
199
 
155
- pods_used.each { |pod_name|
200
+ pods_used.each do |pod_name|
156
201
  if pods_cache.key?(pod_name)
157
202
  result.push(*pods_cache[pod_name])
158
203
  dependencies_hash[pod_name] = pods_cache[pod_name].empty? ? [] : pods_cache[pod_name]
159
204
  end
160
- }
205
+ end
161
206
 
162
207
  result = result.uniq
163
- # maybe additional dependency processing needed here???
164
208
  pods_used = result
165
209
  end
166
210
 
167
- return result, dependencies_hash
211
+ [result, dependencies_hash]
212
+ end
213
+
214
+ def top_level_pods(podfile)
215
+ included_targets = podfile.target_definition_list.select { |target| include_target_named(target.label) }
216
+ included_target_names = included_targets.map(&:label)
217
+ @logger.debug "Including all pods for targets: #{included_target_names}"
218
+
219
+ top_level_deps = included_targets.map(&:dependencies).flatten.uniq
220
+ top_level_deps.map(&:name).uniq
168
221
  end
169
222
 
170
223
  def create_list_of_included_pods(podfile, lockfile)
171
224
  pods_cache = simple_hash_of_lockfile_pods(lockfile)
172
225
 
173
- includedTargets = podfile.target_definition_list.select{ |target| include_target_named(target.label) }
174
- includedTargetNames = includedTargets.map { |target| target.label }
175
- @logger.debug "Including all pods for targets: #{includedTargetNames}"
176
-
177
- topLevelDeps = includedTargets.map(&:dependencies).flatten.uniq
178
- pods_used = topLevelDeps.map(&:name).uniq
226
+ pods_used = top_level_pods(podfile)
179
227
  pods_used, dependencies = append_all_pod_dependencies(pods_used, pods_cache)
180
228
 
181
- return pods_used.sort, dependencies
229
+ [pods_used.sort, dependencies]
182
230
  end
183
231
 
184
-
185
232
  def include_target_named(targetname)
186
- !@exclude_test_targets || !targetname.downcase.include?('test')
233
+ !@exclude_test_targets || !targetname.downcase.include?('test')
187
234
  end
188
235
 
189
-
190
236
  def cocoapods_repository_source(podfile, lockfile, pod_name)
191
237
  @source_manager ||= create_source_manager(podfile)
192
- return Source::CocoaPodsRepository.searchable_source(url: lockfile.spec_repo(pod_name), source_manager: @source_manager)
238
+ Source::CocoaPodsRepository.searchable_source(url: lockfile.spec_repo(pod_name),
239
+ source_manager: @source_manager)
193
240
  end
194
241
 
195
-
196
242
  def git_source(lockfile, pod_name)
197
243
  checkout_options = lockfile.checkout_options_for_pod_named(pod_name)
198
244
  url = checkout_options[:git]
199
- [:tag, :branch, :commit].each do |type|
200
- return Source::GitRepository.new(url: url, type: type, label: checkout_options[type]) if checkout_options[type]
245
+ %i[tag branch commit].each do |type|
246
+ if checkout_options[type]
247
+ return Source::GitRepository.new(url: url, type: type,
248
+ label: checkout_options[type])
249
+ end
201
250
  end
202
- return Source::GitRepository.new(url: url)
251
+ Source::GitRepository.new(url: url)
203
252
  end
204
253
 
205
-
206
254
  def source_for_pod(podfile, lockfile, pod_name)
207
255
  root_name = pod_name.split('/').first
208
256
  return cocoapods_repository_source(podfile, lockfile, root_name) unless lockfile.spec_repo(root_name).nil?
209
257
  return git_source(lockfile, root_name) unless lockfile.checkout_options_for_pod_named(root_name).nil?
210
- return Source::LocalPod.new(path: lockfile.to_hash['EXTERNAL SOURCES'][root_name][:path]) if lockfile.to_hash['EXTERNAL SOURCES'][root_name][:path]
211
- return Source::Podspec.new(url: lockfile.to_hash['EXTERNAL SOURCES'][root_name][:podspec]) if lockfile.to_hash['EXTERNAL SOURCES'][root_name][:podspec]
212
- return nil
213
- end
258
+ if lockfile.to_hash['EXTERNAL SOURCES'][root_name][:path]
259
+ return Source::LocalPod.new(path: lockfile.to_hash['EXTERNAL SOURCES'][root_name][:path])
260
+ end
261
+ if lockfile.to_hash['EXTERNAL SOURCES'][root_name][:podspec]
262
+ return Source::Podspec.new(url: lockfile.to_hash['EXTERNAL SOURCES'][root_name][:podspec])
263
+ end
214
264
 
265
+ nil
266
+ end
215
267
 
216
268
  def create_source_manager(podfile)
217
- sourceManager = ::Pod::Source::Manager.new(::Pod::Config::instance.repos_dir)
269
+ source_manager = ::Pod::Source::Manager.new(::Pod::Config.instance.repos_dir)
218
270
  @logger.debug "Parsing sources from #{podfile.defined_in_file}"
219
271
  podfile.sources.each do |source|
220
272
  @logger.debug "Ensuring #{source} is available for searches"
221
- sourceManager.find_or_create_source_with_url(source)
273
+ source_manager.find_or_create_source_with_url(source)
222
274
  end
223
- @logger.debug "Source manager successfully created with all needed sources"
224
- return sourceManager
275
+ @logger.debug 'Source manager successfully created with all needed sources'
276
+ source_manager
225
277
  end
226
278
  end
227
279
  end
@@ -31,13 +31,16 @@ module CycloneDX
31
31
  end
32
32
 
33
33
  class GitRepository
34
- VALID_TYPES = [:branch, :tag, :commit].freeze
34
+ VALID_TYPES = %i[branch tag commit].freeze
35
35
 
36
36
  attr_reader :url, :type, :label
37
37
 
38
38
  def initialize(url:, type: nil, label: nil)
39
- raise ArgumentError, "Invalid checkout information" if !type.nil? && !VALID_TYPES.include?(type)
40
- @url, @type, @label = url, type, label
39
+ raise ArgumentError, 'Invalid checkout information' if !type.nil? && !VALID_TYPES.include?(type)
40
+
41
+ @url = url
42
+ @type = type
43
+ @label = label
41
44
  end
42
45
  end
43
46
 
@@ -21,6 +21,6 @@
21
21
 
22
22
  module CycloneDX
23
23
  module CocoaPods
24
- VERSION = '1.2.0'
24
+ VERSION = '1.4.0'
25
25
  end
26
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.2.0
4
+ version: 1.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - José González
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: exe
11
11
  cert_chain: []
12
- date: 2024-01-06 00:00:00.000000000 Z
12
+ date: 2024-10-15 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: cocoapods
@@ -52,47 +52,47 @@ dependencies:
52
52
  - !ruby/object:Gem::Version
53
53
  version: '2.0'
54
54
  - !ruby/object:Gem::Dependency
55
- name: rake
55
+ name: equivalent-xml
56
56
  requirement: !ruby/object:Gem::Requirement
57
57
  requirements:
58
58
  - - "~>"
59
59
  - !ruby/object:Gem::Version
60
- version: '13.0'
60
+ version: 0.6.0
61
61
  type: :development
62
62
  prerelease: false
63
63
  version_requirements: !ruby/object:Gem::Requirement
64
64
  requirements:
65
65
  - - "~>"
66
66
  - !ruby/object:Gem::Version
67
- version: '13.0'
67
+ version: 0.6.0
68
68
  - !ruby/object:Gem::Dependency
69
- name: rspec
69
+ name: rake
70
70
  requirement: !ruby/object:Gem::Requirement
71
71
  requirements:
72
72
  - - "~>"
73
73
  - !ruby/object:Gem::Version
74
- version: '3.0'
74
+ version: '13.0'
75
75
  type: :development
76
76
  prerelease: false
77
77
  version_requirements: !ruby/object:Gem::Requirement
78
78
  requirements:
79
79
  - - "~>"
80
80
  - !ruby/object:Gem::Version
81
- version: '3.0'
81
+ version: '13.0'
82
82
  - !ruby/object:Gem::Dependency
83
- name: equivalent-xml
83
+ name: rspec
84
84
  requirement: !ruby/object:Gem::Requirement
85
85
  requirements:
86
86
  - - "~>"
87
87
  - !ruby/object:Gem::Version
88
- version: 0.6.0
88
+ version: '3.0'
89
89
  type: :development
90
90
  prerelease: false
91
91
  version_requirements: !ruby/object:Gem::Requirement
92
92
  requirements:
93
93
  - - "~>"
94
94
  - !ruby/object:Gem::Version
95
- version: 0.6.0
95
+ version: '3.0'
96
96
  description: CycloneDX is a lightweight software bill-of-material (SBOM) specification
97
97
  designed for use in application security contexts and supply chain component analysis.
98
98
  This Gem generates CycloneDX BOMs from CocoaPods projects.
@@ -140,7 +140,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
140
140
  - !ruby/object:Gem::Version
141
141
  version: '0'
142
142
  requirements: []
143
- rubygems_version: 3.3.7
143
+ rubygems_version: 3.5.16
144
144
  signing_key:
145
145
  specification_version: 4
146
146
  summary: CycloneDX software bill-of-material (SBOM) generation utility