cyclonedx-cocoapods 1.2.0 → 1.3.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: 5b02ca712eff5c74b3c7a4e3c59ab7a402a87cf2f3053be388f64a702ca0d3a1
4
+ data.tar.gz: ca6e3d81e4b255dfcd43add1017e1b5dc939274544b6bade803da0246c544ebc
5
5
  SHA512:
6
- metadata.gz: 7fe8629cb7313126a55d2cbae4e7f69ea6fc365336b56093eab5a23e3a27d4fde848a892926ce2fc201509af9c9e4adb9cf59e0940841a03023e344817be2aa2
7
- data.tar.gz: 8cc8b64ddcf4292e14c4698e55899a2b6c72de7ed1ed5ee7adcef316fa315286e52b108a5d000a5fa6f6e42333fd752633b7eeb0142fa603b615b05fef688c8e
6
+ metadata.gz: 74af3ed1ceded419670e4e4cc2b69a955730f963ef92510159505809a02681ae39347c91a24ac99d988f142f987465998fdd4bf4629e799d0dc33e01f96fbb9a
7
+ data.tar.gz: b1ce5ceae85c7b3ff993109203a53f9f7fa61397f8d58f6b18220ee2ed5e866ab1bc210b6ac48b375de3ec49e6928abe2245413fac56b437403317fb1c2ba783
data/CHANGELOG.md CHANGED
@@ -4,6 +4,18 @@ 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.3.0]
8
+
9
+ ### Added
10
+ - 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).
11
+
12
+ ### Changed
13
+ - Updated to use v1.5 of the CycloneDX specification. ([Issue #57](https://github.com/CycloneDX/cyclonedx-cocoapods/issues/57)) [@macblazer](https://github.com/macblazer)
14
+ - Code cleanup based on [RuboCop](https://rubocop.org/) analysis. ([Issue #45](https://github.com/CycloneDX/cyclonedx-cocoapods/issues/45)) [@macblazer](https://github.com/macblazer).
15
+
16
+ ### Fixed
17
+ - Following the specification to put the `bom-ref` attribute on `component` instead of as a `bomRef` element of `component`. [@macblazer](https://github.com/macblazer).
18
+
7
19
  ## [1.2.0]
8
20
 
9
21
  ### 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,17 +59,54 @@ 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?
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
+ def add_to_bom(xml, trim_strings_length = 0)
108
+ xml.component(type: 'library', 'bom-ref': purl) do
109
+ xml_add_author(xml, trim_strings_length)
73
110
  xml.name name
74
111
  xml.version version.to_s
75
112
  xml.description { xml.cdata description } unless description.nil?
@@ -83,15 +120,12 @@ module CycloneDX
83
120
  license.add_to_bom(xml)
84
121
  end
85
122
  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
123
+ if trim_strings_length.zero?
124
+ xml.purl purl
125
+ else
126
+ xml.purl purl.slice(0, trim_strings_length)
94
127
  end
128
+ xml_add_homepage(xml)
95
129
  end
96
130
  end
97
131
 
@@ -118,7 +152,7 @@ module CycloneDX
118
152
  end
119
153
 
120
154
  class BOMBuilder
121
- NAMESPACE = 'http://cyclonedx.org/schema/bom/1.4'
155
+ NAMESPACE = 'http://cyclonedx.org/schema/bom/1.5'
122
156
 
123
157
  attr_reader :component, :pods, :dependencies
124
158
 
@@ -128,32 +162,50 @@ module CycloneDX
128
162
  @dependencies = dependencies&.sort
129
163
  end
130
164
 
131
- def bom(version: 1)
132
- raise ArgumentError, "Incorrect version: #{version} should be an integer greater than 0" unless version.to_i > 0
165
+ def bom(version: 1, trim_strings_length: 0)
166
+ unless version.to_i.positive?
167
+ raise ArgumentError,
168
+ "Incorrect version: #{version} should be an integer greater than 0"
169
+ end
170
+
171
+ unless trim_strings_length.is_a?(Integer) && (trim_strings_length.positive? || trim_strings_length.zero?)
172
+ raise ArgumentError,
173
+ "Incorrect string length: #{trim_strings_length} should be an integer greater than 0"
174
+ end
175
+
176
+ unchecked_bom(version: version, trim_strings_length: trim_strings_length)
177
+ end
178
+
179
+ private
133
180
 
181
+ # does not verify parameters because the public method does that.
182
+ def unchecked_bom(version: 1, trim_strings_length: 0)
134
183
  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
184
+ xml.bom(xmlns: NAMESPACE, version: version.to_i.to_s, serialNumber: "urn:uuid:#{SecureRandom.uuid}") do
136
185
  bom_metadata(xml)
137
- xml.components do
138
- pods.each do |pod|
139
- pod.add_to_bom(xml)
140
- end
141
- end
142
186
 
143
- xml.dependencies do
144
- bom_dependencies(xml, dependencies)
145
- end
187
+ bom_components(xml, pods, trim_strings_length)
188
+
189
+ bom_dependencies(xml, dependencies)
146
190
  end
147
191
  end.to_xml
148
192
  end
149
193
 
150
- private
194
+ def bom_components(xml, pods, trim_strings_length)
195
+ xml.components do
196
+ pods.each do |pod|
197
+ pod.add_to_bom(xml, trim_strings_length)
198
+ end
199
+ end
200
+ end
151
201
 
152
202
  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)
203
+ xml.dependencies do
204
+ dependencies&.each do |key, array|
205
+ xml.dependency(ref: key) do
206
+ array.sort.each do |value|
207
+ xml.dependency(ref: value)
208
+ end
157
209
  end
158
210
  end
159
211
  end
@@ -162,14 +214,18 @@ module CycloneDX
162
214
  def bom_metadata(xml)
163
215
  xml.metadata do
164
216
  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
217
+ bom_tools(xml)
218
+ component&.add_to_bom(xml)
219
+ end
220
+ end
221
+
222
+ def bom_tools(xml)
223
+ xml.tools do
224
+ xml.tool do
225
+ xml.vendor 'CycloneDX'
226
+ xml.name 'cyclonedx-cocoapods'
227
+ xml.version VERSION
171
228
  end
172
- component.add_to_bom(xml) unless component.nil?
173
229
  end
174
230
  end
175
231
  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
+ pods, dependencies = analyze(options)
55
43
 
56
- private
44
+ build_and_write_bom(options, pods, 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,113 @@ 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
84
77
  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
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
87
81
  end
88
- options.on('-b', '--bom-version bom_version', Integer, 'Version of the generated BOM (default: "1")') do |version|
89
- parsedOptions[:bom_version] = version
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
90
85
  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
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
89
+ end
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
+ [pods, dependencies]
123
140
  end
124
141
 
142
+ def build_and_write_bom(options, pods, dependencies)
143
+ builder = BOMBuilder.new(pods: pods, component: component_from_options(options), dependencies: dependencies)
144
+ bom = builder.bom(version: options[:bom_version] || 1,
145
+ trim_strings_length: options[:trim_strings_length] || 0)
146
+ write_bom_to_file(bom: bom, options: options)
147
+ end
148
+
149
+ def component_from_options(options)
150
+ return unless options[:name]
151
+
152
+ Component.new(group: options[:group], name: options[:name], version: options[:version],
153
+ type: options[:type])
154
+ end
125
155
 
126
156
  def setup_logger(verbose: true)
127
157
  @logger ||= Logger.new($stdout)
128
158
  @logger.level = verbose ? Logger::DEBUG : Logger::INFO
129
159
  end
130
160
 
131
-
132
161
  def write_bom_to_file(bom:, options:)
162
+ bom_file_path = prep_for_bom_write(options)
163
+
164
+ begin
165
+ File.write(bom_file_path, bom)
166
+ @logger.info "BOM written to #{bom_file_path}"
167
+ rescue StandardError
168
+ raise BOMOutputError, "Unable to write the BOM to #{bom_file_path}"
169
+ end
170
+ end
171
+
172
+ def prep_for_bom_write(options)
133
173
  bom_file_path = Pathname.new(options[:bom_file_path] || './bom.xml').expand_path
134
174
  bom_dir = bom_file_path.dirname
135
175
 
136
176
  begin
137
177
  FileUtils.mkdir_p(bom_dir) unless bom_dir.directory?
138
- rescue
178
+ rescue StandardError
139
179
  raise BOMOutputError, "Unable to create the BOM output directory at #{bom_dir}"
140
180
  end
141
181
 
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
182
+ bom_file_path
148
183
  end
149
184
  end
150
185
  end
@@ -26,13 +26,19 @@ module CycloneDX
26
26
 
27
27
  attr_reader :group, :name, :version, :type
28
28
 
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?
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?
32
+
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
34
37
 
35
- @group, @name, @version, @type = group, name, version, type
38
+ @group = group
39
+ @name = name
40
+ @version = version
41
+ @type = type
36
42
  end
37
43
  end
38
44
  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,138 @@ 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))
87
-
88
- pod_dependencies[pod.purl] = lockfile.pod_names.select { |name| value.include?(name) }.map do |name|
89
- pod = Pod.new(name: name, version: lockfile.version(name), source: source_for_pod(podfile, lockfile, name), checksum: lockfile.checksum(name))
90
- pod.purl
91
- end
92
- end
93
- }
64
+ pod_dependencies = parse_dependencies(dependencies, podfile, lockfile)
94
65
 
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
105
75
  end
106
76
 
107
77
  private
108
78
 
79
+ def load_plugins(podfile_path)
80
+ podfile_contents = File.read(podfile_path)
81
+ plugin_syntax = /\s*plugin\s+['"]([^'"]+)['"]/
82
+ plugin_names = podfile_contents.scan(plugin_syntax).flatten
83
+
84
+ plugin_names.each do |plugin_name|
85
+ load_one_plugin(plugin_name)
86
+ end
87
+ end
88
+
89
+ def load_one_plugin(plugin_name)
90
+ @logger.debug("Loading plugin #{plugin_name}")
91
+ begin
92
+ plugin_spec = Gem::Specification.find_by_name(plugin_name)
93
+ plugin_spec&.activate
94
+ load("#{plugin_spec.gem_dir}/lib/cocoapods_plugin.rb") if plugin_spec
95
+ rescue Gem::LoadError => e
96
+ @logger.warn("Failed to load plugin #{plugin_name}. #{e.message}")
97
+ end
98
+ end
99
+
100
+ def validate_options(project_dir, options)
101
+ raise PodfileParsingError, "#{options[:path]} is not a valid directory." unless File.directory?(project_dir)
102
+
103
+ options[:podfile_path] = project_dir + 'Podfile'
104
+ unless File.exist?(options[:podfile_path])
105
+ raise PodfileParsingError, "Missing Podfile in #{project_dir}. Please use the --path option if " \
106
+ 'not running from the CocoaPods project directory.'
107
+ end
108
+
109
+ options[:podfile_lock_path] = project_dir + 'Podfile.lock'
110
+ return if File.exist?(options[:podfile_lock_path])
111
+
112
+ raise PodfileParsingError, "Missing Podfile.lock, please run 'pod install' before generating BOM"
113
+ end
114
+
115
+ def parse_dependencies(dependencies, podfile, lockfile)
116
+ pod_dependencies = {}
117
+ dependencies.each do |key, podname_array|
118
+ next unless lockfile.pod_names.include? key
119
+
120
+ pod = Pod.new(name: key, version: lockfile.version(key), source: source_for_pod(podfile, lockfile, key),
121
+ checksum: lockfile.checksum(key))
122
+
123
+ pod_dependencies[pod.purl] = dependencies_for_pod(podname_array, podfile, lockfile)
124
+ end
125
+
126
+ pod_dependencies
127
+ end
128
+
129
+ def dependencies_for_pod(podname_array, podfile, lockfile)
130
+ lockfile.pod_names.select { |name| podname_array.include?(name) }.map do |name|
131
+ pod = Pod.new(name: name,
132
+ version: lockfile.version(name),
133
+ source: source_for_pod(podfile, lockfile, name),
134
+ checksum: lockfile.checksum(name))
135
+ pod.purl
136
+ end
137
+ end
109
138
 
110
139
  def initialize_cocoapods_config(project_dir)
111
140
  ::Pod::Config.instance.installation_root = project_dir
112
141
  end
113
142
 
114
-
115
143
  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
144
+ manifest_file = ::Pod::Config.instance.sandbox.manifest
145
+ if manifest_file.nil?
146
+ raise PodfileParsingError,
147
+ "Missing Manifest.lock, please run 'pod install' before generating BOM"
148
+ end
149
+ return if lockfile == manifest_file
150
+
151
+ raise PodfileParsingError,
152
+ "The sandbox is not in sync with the Podfile.lock. Run 'pod install' " \
153
+ 'or update your CocoaPods installation.'
119
154
  end
120
155
 
121
156
  def simple_hash_of_lockfile_pods(lockfile)
122
- pods_hash = { }
157
+ pods_hash = {}
123
158
 
124
159
  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
- }
160
+ pods_used&.each do |pod|
161
+ map_single_pod(pod, pods_hash)
162
+ end
138
163
  pods_hash
139
164
  end
140
165
 
166
+ def map_single_pod(pod, pods_hash)
167
+ if pod.is_a?(String)
168
+ # Pods stored as String have no dependencies
169
+ pod_name = pod.split.first
170
+ pods_hash[pod_name] = []
171
+ else
172
+ # Pods stored as a hash have pod name and dependencies.
173
+ pod.each do |pod, dependencies|
174
+ pod_name = pod.split.first
175
+ pods_hash[pod_name] = dependencies.map { |d| d.split.first }
176
+ end
177
+ end
178
+ end
141
179
 
142
180
  def append_all_pod_dependencies(pods_used, pods_cache)
143
181
  result = pods_used
144
182
  original_number = 0
145
- dependencies_hash = { }
183
+ dependencies_hash = {}
146
184
 
147
185
  # Loop adding pod dependencies until we are not adding any more dependencies to the result
148
186
  # This brings in all the transitive dependencies of every top level pod.
@@ -152,76 +190,79 @@ module CycloneDX
152
190
  while result.length != original_number
153
191
  original_number = result.length
154
192
 
155
- pods_used.each { |pod_name|
193
+ pods_used.each do |pod_name|
156
194
  if pods_cache.key?(pod_name)
157
195
  result.push(*pods_cache[pod_name])
158
196
  dependencies_hash[pod_name] = pods_cache[pod_name].empty? ? [] : pods_cache[pod_name]
159
197
  end
160
- }
198
+ end
161
199
 
162
200
  result = result.uniq
163
- # maybe additional dependency processing needed here???
164
201
  pods_used = result
165
202
  end
166
203
 
167
- return result, dependencies_hash
204
+ [result, dependencies_hash]
168
205
  end
169
206
 
170
207
  def create_list_of_included_pods(podfile, lockfile)
171
208
  pods_cache = simple_hash_of_lockfile_pods(lockfile)
172
209
 
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}"
210
+ included_targets = podfile.target_definition_list.select { |target| include_target_named(target.label) }
211
+ included_target_names = included_targets.map(&:label)
212
+ @logger.debug "Including all pods for targets: #{included_target_names}"
176
213
 
177
- topLevelDeps = includedTargets.map(&:dependencies).flatten.uniq
178
- pods_used = topLevelDeps.map(&:name).uniq
214
+ top_level_deps = included_targets.map(&:dependencies).flatten.uniq
215
+ pods_used = top_level_deps.map(&:name).uniq
179
216
  pods_used, dependencies = append_all_pod_dependencies(pods_used, pods_cache)
180
217
 
181
- return pods_used.sort, dependencies
218
+ [pods_used.sort, dependencies]
182
219
  end
183
220
 
184
-
185
221
  def include_target_named(targetname)
186
- !@exclude_test_targets || !targetname.downcase.include?('test')
222
+ !@exclude_test_targets || !targetname.downcase.include?('test')
187
223
  end
188
224
 
189
-
190
225
  def cocoapods_repository_source(podfile, lockfile, pod_name)
191
226
  @source_manager ||= create_source_manager(podfile)
192
- return Source::CocoaPodsRepository.searchable_source(url: lockfile.spec_repo(pod_name), source_manager: @source_manager)
227
+ Source::CocoaPodsRepository.searchable_source(url: lockfile.spec_repo(pod_name),
228
+ source_manager: @source_manager)
193
229
  end
194
230
 
195
-
196
231
  def git_source(lockfile, pod_name)
197
232
  checkout_options = lockfile.checkout_options_for_pod_named(pod_name)
198
233
  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]
234
+ %i[tag branch commit].each do |type|
235
+ if checkout_options[type]
236
+ return Source::GitRepository.new(url: url, type: type,
237
+ label: checkout_options[type])
238
+ end
201
239
  end
202
- return Source::GitRepository.new(url: url)
240
+ Source::GitRepository.new(url: url)
203
241
  end
204
242
 
205
-
206
243
  def source_for_pod(podfile, lockfile, pod_name)
207
244
  root_name = pod_name.split('/').first
208
245
  return cocoapods_repository_source(podfile, lockfile, root_name) unless lockfile.spec_repo(root_name).nil?
209
246
  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
247
+ if lockfile.to_hash['EXTERNAL SOURCES'][root_name][:path]
248
+ return Source::LocalPod.new(path: lockfile.to_hash['EXTERNAL SOURCES'][root_name][:path])
249
+ end
250
+ if lockfile.to_hash['EXTERNAL SOURCES'][root_name][:podspec]
251
+ return Source::Podspec.new(url: lockfile.to_hash['EXTERNAL SOURCES'][root_name][:podspec])
252
+ end
214
253
 
254
+ nil
255
+ end
215
256
 
216
257
  def create_source_manager(podfile)
217
- sourceManager = ::Pod::Source::Manager.new(::Pod::Config::instance.repos_dir)
258
+ source_manager = ::Pod::Source::Manager.new(::Pod::Config.instance.repos_dir)
218
259
  @logger.debug "Parsing sources from #{podfile.defined_in_file}"
219
260
  podfile.sources.each do |source|
220
261
  @logger.debug "Ensuring #{source} is available for searches"
221
- sourceManager.find_or_create_source_with_url(source)
262
+ source_manager.find_or_create_source_with_url(source)
222
263
  end
223
- @logger.debug "Source manager successfully created with all needed sources"
224
- return sourceManager
264
+ @logger.debug 'Source manager successfully created with all needed sources'
265
+ source_manager
225
266
  end
226
267
  end
227
268
  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.3.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.3.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-02-08 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.4
144
144
  signing_key:
145
145
  specification_version: 4
146
146
  summary: CycloneDX software bill-of-material (SBOM) generation utility