cyclonedx-cocoapods 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f9377b14e9d7b5f41db0b12693b58dd37367e05e72f8ab208178c6d79f38234b
4
- data.tar.gz: a8402aabb0a9eb157bbbaab4782a3fc6ce731003b48037c54cb1dc7e2d5fb289
3
+ metadata.gz: 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