cyclonedx-cocoapods 1.2.0 → 1.4.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: 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