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 +4 -4
- data/CHANGELOG.md +12 -0
- data/README.md +3 -2
- data/exe/cyclonedx-cocoapods +4 -2
- data/lib/cyclonedx/cocoapods/bom_builder.rb +95 -39
- data/lib/cyclonedx/cocoapods/cli_runner.rb +84 -49
- data/lib/cyclonedx/cocoapods/component.rb +11 -5
- data/lib/cyclonedx/cocoapods/license.rb +4 -6
- data/lib/cyclonedx/cocoapods/pod.rb +47 -31
- data/lib/cyclonedx/cocoapods/pod_attributes.rb +20 -5
- data/lib/cyclonedx/cocoapods/podfile_analyzer.rb +126 -85
- data/lib/cyclonedx/cocoapods/source.rb +6 -3
- data/lib/cyclonedx/cocoapods/version.rb +1 -1
- metadata +12 -12
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5b02ca712eff5c74b3c7a4e3c59ab7a402a87cf2f3053be388f64a702ca0d3a1
|
4
|
+
data.tar.gz: ca6e3d81e4b255dfcd43add1017e1b5dc939274544b6bade803da0246c544ebc
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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.
|
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
|
data/exe/cyclonedx-cocoapods
CHANGED
@@ -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
|
23
|
+
require 'cyclonedx/cocoapods/cli_runner'
|
22
24
|
|
23
|
-
CycloneDX::CocoaPods::CLIRunner.new
|
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
|
-
|
65
|
-
|
66
|
-
|
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
|
70
|
-
|
71
|
-
|
72
|
-
|
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
|
-
|
87
|
-
|
88
|
-
|
89
|
-
xml.
|
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.
|
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
|
-
|
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(
|
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
|
144
|
-
|
145
|
-
|
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
|
-
|
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
|
154
|
-
|
155
|
-
|
156
|
-
|
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
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
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
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
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
|
-
|
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
|
60
|
-
|
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
|
-
|
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
|
-
|
76
|
+
parsed_options[:path] = path
|
84
77
|
end
|
85
|
-
options.on('-o', '--output bom_file_path',
|
86
|
-
|
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,
|
89
|
-
|
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',
|
92
|
-
|
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',
|
97
|
-
|
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
|
-
|
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',
|
108
|
-
|
109
|
-
|
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
|
-
|
121
|
+
parsed_options[:group] = group
|
113
122
|
end
|
114
123
|
end.parse!
|
115
124
|
|
116
|
-
|
117
|
-
|
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
|
-
|
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
|
-
|
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(
|
30
|
-
raise ArgumentError,
|
31
|
-
raise ArgumentError,
|
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
|
-
|
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
|
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 = [
|
29
|
+
IDENTIFIER_TYPES = %i[id name].freeze
|
30
30
|
|
31
|
-
attr_reader
|
32
|
-
|
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,
|
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
|
-
|
29
|
-
attr_reader :
|
30
|
-
|
31
|
-
attr_reader :
|
32
|
-
|
33
|
-
attr_reader :
|
34
|
-
|
35
|
-
attr_reader :
|
36
|
-
|
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,
|
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,
|
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
|
-
|
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
|
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
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
60
|
-
|
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
|
-
|
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),
|
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
|
-
|
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
|
-
|
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
|
-
|
117
|
-
|
118
|
-
|
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
|
126
|
-
|
127
|
-
|
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
|
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
|
-
|
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
|
-
|
174
|
-
|
175
|
-
@logger.debug "Including all pods for targets: #{
|
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
|
-
|
178
|
-
pods_used =
|
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
|
-
|
218
|
+
[pods_used.sort, dependencies]
|
182
219
|
end
|
183
220
|
|
184
|
-
|
185
221
|
def include_target_named(targetname)
|
186
|
-
|
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
|
-
|
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
|
-
[
|
200
|
-
|
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
|
-
|
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
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
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
|
-
|
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
|
-
|
262
|
+
source_manager.find_or_create_source_with_url(source)
|
222
263
|
end
|
223
|
-
@logger.debug
|
224
|
-
|
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 = [
|
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,
|
40
|
-
|
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
|
|
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.
|
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-
|
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:
|
55
|
+
name: equivalent-xml
|
56
56
|
requirement: !ruby/object:Gem::Requirement
|
57
57
|
requirements:
|
58
58
|
- - "~>"
|
59
59
|
- !ruby/object:Gem::Version
|
60
|
-
version:
|
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:
|
67
|
+
version: 0.6.0
|
68
68
|
- !ruby/object:Gem::Dependency
|
69
|
-
name:
|
69
|
+
name: rake
|
70
70
|
requirement: !ruby/object:Gem::Requirement
|
71
71
|
requirements:
|
72
72
|
- - "~>"
|
73
73
|
- !ruby/object:Gem::Version
|
74
|
-
version: '
|
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: '
|
81
|
+
version: '13.0'
|
82
82
|
- !ruby/object:Gem::Dependency
|
83
|
-
name:
|
83
|
+
name: rspec
|
84
84
|
requirement: !ruby/object:Gem::Requirement
|
85
85
|
requirements:
|
86
86
|
- - "~>"
|
87
87
|
- !ruby/object:Gem::Version
|
88
|
-
version:
|
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:
|
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.
|
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
|