cyclonedx-cocoapods 1.2.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +21 -0
- data/README.md +3 -2
- data/exe/cyclonedx-cocoapods +4 -2
- data/lib/cyclonedx/cocoapods/bom_builder.rb +123 -45
- data/lib/cyclonedx/cocoapods/cli_runner.rb +99 -49
- data/lib/cyclonedx/cocoapods/component.rb +17 -6
- 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 +138 -86
- 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: 4b3c1577d54844759e40218fb3b0876014aa8a2eac1a8ea2be2512cab13d79f9
|
4
|
+
data.tar.gz: dd99bd2aa09a1d6ecd956fdd3118a064df6662026b6816ee86f9a2f553347068
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5e82c25c27de0fbede464d04a06b9b7f06c3fc79550041835395c6fe5aa32a0f1c4bba1d391b988ff6d39107f696960f064730de43ef8c0f0e8000d576cd1010
|
7
|
+
data.tar.gz: '048fa99979dd4e606b4952412dad3675bad2ebe3e45eccd8513089f18908d5594213031cb85130434f002d7a8edbafbb67818e602c648b49137768e8085c445b'
|
data/CHANGELOG.md
CHANGED
@@ -4,6 +4,27 @@ All notable changes to this project will be documented in this file.
|
|
4
4
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
5
5
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
6
6
|
|
7
|
+
## [1.4.0]
|
8
|
+
|
9
|
+
### Added
|
10
|
+
- Added `evidence` element to the component output to indicate that we are doing manifest analysis to generate the bom. ([Issue #69](https://github.com/CycloneDX/cyclonedx-cocoapods/issues/69)) [@macblazer](https://github.com/macblazer).
|
11
|
+
|
12
|
+
### Fixed
|
13
|
+
- Added top level dependencies when the metadata/component is specified (by using the `--name`, `--version`, and `--type` parameters). ([PR #70](https://github.com/CycloneDX/cyclonedx-cocoapods/pull/70)) [@fnxpt](https://github.com/fnxpt)
|
14
|
+
- Properly concatenate paths to Podfile and Podfile.lock (with unit tests!). ([Issue #71](https://github.com/CycloneDX/cyclonedx-cocoapods/issues/71)) [@macblazer](https://github.com/macblazer).
|
15
|
+
|
16
|
+
## [1.3.0]
|
17
|
+
|
18
|
+
### Added
|
19
|
+
- Added optional `--shortened-strings` CLI parameter to limit the author, publisher, and purl lengths. ([Issue #65](https://github.com/CycloneDX/cyclonedx-cocoapods/issues/65)) [@macblazer](https://github.com/macblazer).
|
20
|
+
|
21
|
+
### Changed
|
22
|
+
- Updated to use v1.5 of the CycloneDX specification. ([Issue #57](https://github.com/CycloneDX/cyclonedx-cocoapods/issues/57)) [@macblazer](https://github.com/macblazer)
|
23
|
+
- Code cleanup based on [RuboCop](https://rubocop.org/) analysis. ([Issue #45](https://github.com/CycloneDX/cyclonedx-cocoapods/issues/45)) [@macblazer](https://github.com/macblazer).
|
24
|
+
|
25
|
+
### Fixed
|
26
|
+
- Following the specification to put the `bom-ref` attribute on `component` instead of as a `bomRef` element of `component`. [@macblazer](https://github.com/macblazer).
|
27
|
+
|
7
28
|
## [1.2.0]
|
8
29
|
|
9
30
|
### Added
|
data/README.md
CHANGED
@@ -21,7 +21,7 @@ The CycloneDX CocoaPods Gem creates a valid CycloneDX software bill-of-material
|
|
21
21
|
|
22
22
|
### From Source
|
23
23
|
|
24
|
-
First, clone/copy the source code from GitHub. Then in the source code directory run these
|
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,18 +59,73 @@ module CycloneDX
|
|
59
59
|
CHECKSUM_ALGORITHM = 'SHA-1'
|
60
60
|
HOMEPAGE_REFERENCE_TYPE = 'website'
|
61
61
|
|
62
|
+
def source_qualifier
|
63
|
+
return '' if source.nil? || source.source_qualifier.empty?
|
64
|
+
|
65
|
+
"?#{source.source_qualifier.map do |key, value|
|
66
|
+
"#{key}=#{CGI.escape(value)}"
|
67
|
+
end.join('&')}"
|
68
|
+
end
|
69
|
+
|
70
|
+
def purl_subpath
|
71
|
+
return '' unless name.split('/').length > 1
|
72
|
+
|
73
|
+
"##{name.split('/').drop(1).map do |component|
|
74
|
+
CGI.escape(component)
|
75
|
+
end.join('/')}"
|
76
|
+
end
|
77
|
+
|
62
78
|
def purl
|
63
79
|
purl_name = CGI.escape(name.split('/').first)
|
64
|
-
|
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
|
-
|
73
|
-
xml.
|
85
|
+
def xml_add_author(xml, trim_strings_length)
|
86
|
+
return if author.nil?
|
87
|
+
|
88
|
+
if trim_strings_length.zero?
|
89
|
+
xml.author author
|
90
|
+
xml.publisher author
|
91
|
+
else
|
92
|
+
xml.author author.slice(0, trim_strings_length)
|
93
|
+
xml.publisher author.slice(0, trim_strings_length)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def xml_add_homepage(xml)
|
98
|
+
return if homepage.nil?
|
99
|
+
|
100
|
+
xml.externalReferences do
|
101
|
+
xml.reference(type: HOMEPAGE_REFERENCE_TYPE) do
|
102
|
+
xml.url homepage
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
# Add evidence of the purl identity.
|
108
|
+
# See https://github.com/CycloneDX/guides/blob/main/SBOM/en/0x60-Evidence.md for more info
|
109
|
+
def xml_add_evidence(xml, manifest_path)
|
110
|
+
xml.evidence do
|
111
|
+
xml.identity do
|
112
|
+
xml.field 'purl'
|
113
|
+
xml.confidence '0.6'
|
114
|
+
xml.methods_ do
|
115
|
+
xml.method_ do
|
116
|
+
xml.technique 'manifest-analysis'
|
117
|
+
xml.confidence '0.6'
|
118
|
+
xml.value manifest_path
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def add_to_bom(xml, manifest_path, trim_strings_length = 0)
|
126
|
+
xml.component(type: 'library', 'bom-ref': purl) do
|
127
|
+
xml_add_author(xml, trim_strings_length)
|
128
|
+
xml.name_ name
|
74
129
|
xml.version version.to_s
|
75
130
|
xml.description { xml.cdata description } unless description.nil?
|
76
131
|
unless checksum.nil?
|
@@ -83,15 +138,14 @@ module CycloneDX
|
|
83
138
|
license.add_to_bom(xml)
|
84
139
|
end
|
85
140
|
end
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
xml.
|
90
|
-
xml.reference(type: HOMEPAGE_REFERENCE_TYPE) do
|
91
|
-
xml.url homepage
|
92
|
-
end
|
93
|
-
end
|
141
|
+
if trim_strings_length.zero?
|
142
|
+
xml.purl purl
|
143
|
+
else
|
144
|
+
xml.purl purl.slice(0, trim_strings_length)
|
94
145
|
end
|
146
|
+
xml_add_homepage(xml)
|
147
|
+
|
148
|
+
xml_add_evidence(xml, manifest_path)
|
95
149
|
end
|
96
150
|
end
|
97
151
|
|
@@ -99,7 +153,7 @@ module CycloneDX
|
|
99
153
|
def add_to_bom(xml)
|
100
154
|
xml.license do
|
101
155
|
xml.id identifier if identifier_type == :id
|
102
|
-
xml.
|
156
|
+
xml.name_ identifier if identifier_type == :name
|
103
157
|
xml.text_ text unless text.nil?
|
104
158
|
xml.url url unless url.nil?
|
105
159
|
end
|
@@ -109,51 +163,71 @@ module CycloneDX
|
|
109
163
|
|
110
164
|
class Component
|
111
165
|
def add_to_bom(xml)
|
112
|
-
xml.component(type: type) do
|
166
|
+
xml.component(type: type, 'bom-ref': bomref) do
|
113
167
|
xml.group group unless group.nil?
|
114
|
-
xml.
|
168
|
+
xml.name_ name
|
115
169
|
xml.version version
|
116
170
|
end
|
117
171
|
end
|
118
172
|
end
|
119
173
|
|
174
|
+
# Turns the internal model data into an XML bom.
|
120
175
|
class BOMBuilder
|
121
|
-
NAMESPACE = 'http://cyclonedx.org/schema/bom/1.
|
176
|
+
NAMESPACE = 'http://cyclonedx.org/schema/bom/1.5'
|
122
177
|
|
123
|
-
attr_reader :component, :pods, :dependencies
|
178
|
+
attr_reader :component, :pods, :manifest_path, :dependencies
|
124
179
|
|
125
|
-
def initialize(pods:, component: nil, dependencies: nil)
|
180
|
+
def initialize(pods:, manifest_path:, component: nil, dependencies: nil)
|
126
181
|
@pods = pods.sort_by(&:purl)
|
182
|
+
@manifest_path = manifest_path
|
127
183
|
@component = component
|
128
184
|
@dependencies = dependencies&.sort
|
129
185
|
end
|
130
186
|
|
131
|
-
def bom(version: 1)
|
132
|
-
|
187
|
+
def bom(version: 1, trim_strings_length: 0)
|
188
|
+
unless version.to_i.positive?
|
189
|
+
raise ArgumentError,
|
190
|
+
"Incorrect version: #{version} should be an integer greater than 0"
|
191
|
+
end
|
192
|
+
|
193
|
+
unless trim_strings_length.is_a?(Integer) && (trim_strings_length.positive? || trim_strings_length.zero?)
|
194
|
+
raise ArgumentError,
|
195
|
+
"Incorrect string length: #{trim_strings_length} should be an integer greater than 0"
|
196
|
+
end
|
133
197
|
|
198
|
+
unchecked_bom(version: version, trim_strings_length: trim_strings_length)
|
199
|
+
end
|
200
|
+
|
201
|
+
private
|
202
|
+
|
203
|
+
# does not verify parameters because the public method does that.
|
204
|
+
def unchecked_bom(version: 1, trim_strings_length: 0)
|
134
205
|
Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml|
|
135
|
-
xml.bom(
|
206
|
+
xml.bom(xmlns: NAMESPACE, version: version.to_i.to_s, serialNumber: "urn:uuid:#{SecureRandom.uuid}") do
|
136
207
|
bom_metadata(xml)
|
137
|
-
xml.components do
|
138
|
-
pods.each do |pod|
|
139
|
-
pod.add_to_bom(xml)
|
140
|
-
end
|
141
|
-
end
|
142
208
|
|
143
|
-
xml
|
144
|
-
|
145
|
-
|
209
|
+
bom_components(xml, pods, manifest_path, trim_strings_length)
|
210
|
+
|
211
|
+
bom_dependencies(xml, dependencies)
|
146
212
|
end
|
147
213
|
end.to_xml
|
148
214
|
end
|
149
215
|
|
150
|
-
|
216
|
+
def bom_components(xml, pods, manifest_path, trim_strings_length)
|
217
|
+
xml.components do
|
218
|
+
pods.each do |pod|
|
219
|
+
pod.add_to_bom(xml, manifest_path, trim_strings_length)
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
151
223
|
|
152
224
|
def bom_dependencies(xml, dependencies)
|
153
|
-
dependencies
|
154
|
-
|
155
|
-
|
156
|
-
|
225
|
+
xml.dependencies do
|
226
|
+
dependencies&.each do |key, array|
|
227
|
+
xml.dependency(ref: key) do
|
228
|
+
array.sort.each do |value|
|
229
|
+
xml.dependency(ref: value)
|
230
|
+
end
|
157
231
|
end
|
158
232
|
end
|
159
233
|
end
|
@@ -162,14 +236,18 @@ module CycloneDX
|
|
162
236
|
def bom_metadata(xml)
|
163
237
|
xml.metadata do
|
164
238
|
xml.timestamp Time.now.getutc.strftime('%Y-%m-%dT%H:%M:%SZ')
|
165
|
-
xml
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
239
|
+
bom_tools(xml)
|
240
|
+
component&.add_to_bom(xml)
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
def bom_tools(xml)
|
245
|
+
xml.tools do
|
246
|
+
xml.tool do
|
247
|
+
xml.vendor 'CycloneDX'
|
248
|
+
xml.name_ 'cyclonedx-cocoapods'
|
249
|
+
xml.version VERSION
|
171
250
|
end
|
172
|
-
component.add_to_bom(xml) unless component.nil?
|
173
251
|
end
|
174
252
|
end
|
175
253
|
end
|
@@ -19,6 +19,7 @@
|
|
19
19
|
# Copyright (c) OWASP Foundation. All Rights Reserved.
|
20
20
|
#
|
21
21
|
|
22
|
+
require 'English'
|
22
23
|
require 'logger'
|
23
24
|
require 'optparse'
|
24
25
|
|
@@ -30,34 +31,26 @@ module CycloneDX
|
|
30
31
|
module CocoaPods
|
31
32
|
class BOMOutputError < StandardError; end
|
32
33
|
|
34
|
+
# Interprets CLI parameters and runs the main workflow.
|
33
35
|
class CLIRunner
|
34
36
|
def run
|
35
|
-
|
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
|
+
component, pods, manifest_path, dependencies = analyze(options)
|
55
43
|
|
56
|
-
|
44
|
+
build_and_write_bom(options, component, pods, manifest_path, dependencies)
|
45
|
+
rescue StandardError => e
|
46
|
+
@logger.error ([e.message] + e.backtrace).join($INPUT_RECORD_SEPARATOR)
|
47
|
+
exit 1
|
48
|
+
end
|
57
49
|
|
50
|
+
private
|
58
51
|
|
59
|
-
def
|
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,128 @@ module CycloneDX
|
|
80
73
|
|
81
74
|
options.separator("\n BOM Generation")
|
82
75
|
options.on('-p', '--path path', 'Path to CocoaPods project directory (default: current directory)') do |path|
|
83
|
-
|
76
|
+
parsed_options[:path] = path
|
77
|
+
end
|
78
|
+
options.on('-o', '--output bom_file_path',
|
79
|
+
'Path to output the bom.xml file to (default: "bom.xml")') do |bom_file_path|
|
80
|
+
parsed_options[:bom_file_path] = bom_file_path
|
84
81
|
end
|
85
|
-
options.on('-
|
86
|
-
|
82
|
+
options.on('-b', '--bom-version bom_version', Integer,
|
83
|
+
'Version of the generated BOM (default: "1")') do |version|
|
84
|
+
parsed_options[:bom_version] = version
|
87
85
|
end
|
88
|
-
options.on('-
|
89
|
-
|
86
|
+
options.on('-x', '--exclude-test-targets',
|
87
|
+
'Eliminate Podfile targets whose name contains the word "test"') do |exclude|
|
88
|
+
parsed_options[:exclude_test_targets] = exclude
|
90
89
|
end
|
91
|
-
options.on('-
|
92
|
-
|
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
|
-
|
139
|
+
component = component_from_options(options)
|
140
|
+
|
141
|
+
unless component.nil?
|
142
|
+
# add top level pods to main component
|
143
|
+
top_deps = analyzer.top_level_deps(podfile, lockfile)
|
144
|
+
dependencies[component.bomref] = top_deps
|
145
|
+
end
|
146
|
+
|
147
|
+
manifest_path = lockfile.defined_in_file
|
148
|
+
if manifest_path.absolute?
|
149
|
+
# Use the folder that we are building in, then the path to the manifest file
|
150
|
+
manifest_path = Pathname.pwd.basename + manifest_path.relative_path_from(Pathname.pwd)
|
151
|
+
end
|
152
|
+
|
153
|
+
[component, pods, manifest_path, dependencies]
|
123
154
|
end
|
124
155
|
|
156
|
+
def build_and_write_bom(options, component, pods, manifest_path, dependencies)
|
157
|
+
builder = BOMBuilder.new(pods: pods, manifest_path: manifest_path,
|
158
|
+
component: component, dependencies: dependencies)
|
159
|
+
bom = builder.bom(version: options[:bom_version] || 1,
|
160
|
+
trim_strings_length: options[:trim_strings_length] || 0)
|
161
|
+
write_bom_to_file(bom: bom, options: options)
|
162
|
+
end
|
163
|
+
|
164
|
+
def component_from_options(options)
|
165
|
+
return unless options[:name]
|
166
|
+
|
167
|
+
Component.new(group: options[:group], name: options[:name], version: options[:version],
|
168
|
+
type: options[:type])
|
169
|
+
end
|
125
170
|
|
126
171
|
def setup_logger(verbose: true)
|
127
172
|
@logger ||= Logger.new($stdout)
|
128
173
|
@logger.level = verbose ? Logger::DEBUG : Logger::INFO
|
129
174
|
end
|
130
175
|
|
131
|
-
|
132
176
|
def write_bom_to_file(bom:, options:)
|
177
|
+
bom_file_path = prep_for_bom_write(options)
|
178
|
+
|
179
|
+
begin
|
180
|
+
File.write(bom_file_path, bom)
|
181
|
+
@logger.info "BOM written to #{bom_file_path}"
|
182
|
+
rescue StandardError
|
183
|
+
raise BOMOutputError, "Unable to write the BOM to #{bom_file_path}"
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
def prep_for_bom_write(options)
|
133
188
|
bom_file_path = Pathname.new(options[:bom_file_path] || './bom.xml').expand_path
|
134
189
|
bom_dir = bom_file_path.dirname
|
135
190
|
|
136
191
|
begin
|
137
192
|
FileUtils.mkdir_p(bom_dir) unless bom_dir.directory?
|
138
|
-
rescue
|
193
|
+
rescue StandardError
|
139
194
|
raise BOMOutputError, "Unable to create the BOM output directory at #{bom_dir}"
|
140
195
|
end
|
141
196
|
|
142
|
-
|
143
|
-
File.open(bom_file_path, 'w') { |file| file.write(bom) }
|
144
|
-
@logger.info "BOM written to #{bom_file_path}"
|
145
|
-
rescue
|
146
|
-
raise BOMOutputError, "Unable to write the BOM to #{bom_file_path}"
|
147
|
-
end
|
197
|
+
bom_file_path
|
148
198
|
end
|
149
199
|
end
|
150
200
|
end
|
@@ -24,15 +24,26 @@ module CycloneDX
|
|
24
24
|
class Component
|
25
25
|
VALID_COMPONENT_TYPES = %w[application framework library container operating-system device firmware file].freeze
|
26
26
|
|
27
|
-
attr_reader :group, :name, :version, :type
|
27
|
+
attr_reader :group, :name, :version, :type, :bomref
|
28
|
+
|
29
|
+
def initialize(name:, version:, type:, group: nil)
|
30
|
+
raise ArgumentError, 'Group, if specified, must be non empty' if !group.nil? && group.to_s.strip.empty?
|
31
|
+
raise ArgumentError, 'Name must be non empty' if name.nil? || name.to_s.strip.empty?
|
28
32
|
|
29
|
-
def initialize(group: nil, name:, version:, type:)
|
30
|
-
raise ArgumentError, "Group, if specified, must be non empty" if !group.nil? && group.to_s.strip.empty?
|
31
|
-
raise ArgumentError, "Name must be non empty" if name.nil? || name.to_s.strip.empty?
|
32
33
|
Gem::Version.new(version) # To check that the version string is well formed
|
33
|
-
|
34
|
+
unless VALID_COMPONENT_TYPES.include?(type)
|
35
|
+
raise ArgumentError, "#{type} is not valid component type (#{VALID_COMPONENT_TYPES.join('|')})"
|
36
|
+
end
|
37
|
+
|
38
|
+
@group = group
|
39
|
+
@name = name
|
40
|
+
@version = version
|
41
|
+
@type = type
|
42
|
+
@bomref = "#{name}@#{version}"
|
43
|
+
|
44
|
+
return if group.nil?
|
34
45
|
|
35
|
-
@
|
46
|
+
@bomref = "#{group}/#{@bomref}"
|
36
47
|
end
|
37
48
|
end
|
38
49
|
end
|
@@ -26,15 +26,13 @@ module CycloneDX
|
|
26
26
|
class Pod
|
27
27
|
class License
|
28
28
|
SPDX_LICENSES = JSON.parse(File.read("#{__dir__}/spdx-licenses.json")).freeze
|
29
|
-
IDENTIFIER_TYPES = [
|
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,145 @@ 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))
|
64
|
+
pod_dependencies = parse_dependencies(dependencies, podfile, lockfile)
|
87
65
|
|
88
|
-
|
89
|
-
pod = Pod.new(name: name, version: lockfile.version(name), source: source_for_pod(podfile, lockfile, name), checksum: lockfile.checksum(name))
|
90
|
-
pod.purl
|
91
|
-
end
|
92
|
-
end
|
93
|
-
}
|
94
|
-
|
95
|
-
return pods, pod_dependencies
|
66
|
+
[pods, pod_dependencies]
|
96
67
|
end
|
97
68
|
|
98
|
-
|
99
69
|
def populate_pods_with_additional_info(pods)
|
100
70
|
pods.each do |pod|
|
101
71
|
@logger.debug "Completing information for #{pod.name}"
|
102
72
|
pod.complete_information_from_source
|
103
73
|
end
|
104
|
-
|
74
|
+
pods
|
75
|
+
end
|
76
|
+
|
77
|
+
def top_level_deps(podfile, lockfile)
|
78
|
+
pods_used = top_level_pods(podfile)
|
79
|
+
dependencies_for_pod(pods_used, podfile, lockfile)
|
105
80
|
end
|
106
81
|
|
107
82
|
private
|
108
83
|
|
84
|
+
def load_plugins(podfile_path)
|
85
|
+
podfile_contents = File.read(podfile_path)
|
86
|
+
plugin_syntax = /\s*plugin\s+['"]([^'"]+)['"]/
|
87
|
+
plugin_names = podfile_contents.scan(plugin_syntax).flatten
|
88
|
+
|
89
|
+
plugin_names.each do |plugin_name|
|
90
|
+
load_one_plugin(plugin_name)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def load_one_plugin(plugin_name)
|
95
|
+
@logger.debug("Loading plugin #{plugin_name}")
|
96
|
+
begin
|
97
|
+
plugin_spec = Gem::Specification.find_by_name(plugin_name)
|
98
|
+
plugin_spec&.activate
|
99
|
+
load("#{plugin_spec.gem_dir}/lib/cocoapods_plugin.rb") if plugin_spec
|
100
|
+
rescue Gem::LoadError => e
|
101
|
+
@logger.warn("Failed to load plugin #{plugin_name}. #{e.message}")
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def validate_options(project_dir, options)
|
106
|
+
raise PodfileParsingError, "#{options[:path]} is not a valid directory." unless File.directory?(project_dir)
|
107
|
+
|
108
|
+
options[:podfile_path] = project_dir + 'Podfile'
|
109
|
+
unless File.exist?(options[:podfile_path])
|
110
|
+
raise PodfileParsingError, "Missing Podfile in #{project_dir}. Please use the --path option if " \
|
111
|
+
'not running from the CocoaPods project directory.'
|
112
|
+
end
|
113
|
+
|
114
|
+
options[:podfile_lock_path] = project_dir + 'Podfile.lock'
|
115
|
+
return if File.exist?(options[:podfile_lock_path])
|
116
|
+
|
117
|
+
raise PodfileParsingError, "Missing Podfile.lock, please run 'pod install' before generating BOM"
|
118
|
+
end
|
119
|
+
|
120
|
+
def parse_dependencies(dependencies, podfile, lockfile)
|
121
|
+
pod_dependencies = {}
|
122
|
+
dependencies.each do |key, podname_array|
|
123
|
+
next unless lockfile.pod_names.include? key
|
124
|
+
|
125
|
+
pod = Pod.new(name: key, version: lockfile.version(key), source: source_for_pod(podfile, lockfile, key),
|
126
|
+
checksum: lockfile.checksum(key))
|
127
|
+
|
128
|
+
pod_dependencies[pod.purl] = dependencies_for_pod(podname_array, podfile, lockfile)
|
129
|
+
end
|
130
|
+
|
131
|
+
pod_dependencies
|
132
|
+
end
|
133
|
+
|
134
|
+
def dependencies_for_pod(podname_array, podfile, lockfile)
|
135
|
+
lockfile.pod_names.select { |name| podname_array.include?(name) }.map do |name|
|
136
|
+
pod = Pod.new(name: name,
|
137
|
+
version: lockfile.version(name),
|
138
|
+
source: source_for_pod(podfile, lockfile, name),
|
139
|
+
checksum: lockfile.checksum(name))
|
140
|
+
pod.purl
|
141
|
+
end
|
142
|
+
end
|
109
143
|
|
110
144
|
def initialize_cocoapods_config(project_dir)
|
145
|
+
# First, reset the ::Pod::Config instance in case we need to use this analyzer on multiple pods
|
146
|
+
::Pod::Config.instance = nil
|
111
147
|
::Pod::Config.instance.installation_root = project_dir
|
112
148
|
end
|
113
149
|
|
114
|
-
|
115
150
|
def verify_synced_sandbox(lockfile)
|
116
|
-
|
117
|
-
|
118
|
-
|
151
|
+
manifest_file = ::Pod::Config.instance.sandbox.manifest
|
152
|
+
if manifest_file.nil?
|
153
|
+
raise PodfileParsingError,
|
154
|
+
"Missing Manifest.lock, please run 'pod install' before generating BOM"
|
155
|
+
end
|
156
|
+
return if lockfile == manifest_file
|
157
|
+
|
158
|
+
raise PodfileParsingError,
|
159
|
+
"The sandbox is not in sync with the Podfile.lock. Run 'pod install' " \
|
160
|
+
'or update your CocoaPods installation.'
|
119
161
|
end
|
120
162
|
|
121
163
|
def simple_hash_of_lockfile_pods(lockfile)
|
122
|
-
pods_hash = {
|
164
|
+
pods_hash = {}
|
123
165
|
|
124
166
|
pods_used = lockfile.internal_data['PODS']
|
125
|
-
pods_used&.each
|
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
|
-
}
|
167
|
+
pods_used&.each do |pod|
|
168
|
+
map_single_pod(pod, pods_hash)
|
169
|
+
end
|
138
170
|
pods_hash
|
139
171
|
end
|
140
172
|
|
173
|
+
def map_single_pod(pod, pods_hash)
|
174
|
+
if pod.is_a?(String)
|
175
|
+
# Pods stored as String have no dependencies
|
176
|
+
pod_name = pod.split.first
|
177
|
+
pods_hash[pod_name] = []
|
178
|
+
else
|
179
|
+
# Pods stored as a hash have pod name and dependencies.
|
180
|
+
pod.each do |pod, dependencies|
|
181
|
+
pod_name = pod.split.first
|
182
|
+
pods_hash[pod_name] = dependencies.map { |d| d.split.first }
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
141
186
|
|
142
187
|
def append_all_pod_dependencies(pods_used, pods_cache)
|
143
188
|
result = pods_used
|
144
189
|
original_number = 0
|
145
|
-
dependencies_hash = {
|
190
|
+
dependencies_hash = {}
|
146
191
|
|
147
192
|
# Loop adding pod dependencies until we are not adding any more dependencies to the result
|
148
193
|
# This brings in all the transitive dependencies of every top level pod.
|
@@ -152,76 +197,83 @@ module CycloneDX
|
|
152
197
|
while result.length != original_number
|
153
198
|
original_number = result.length
|
154
199
|
|
155
|
-
pods_used.each
|
200
|
+
pods_used.each do |pod_name|
|
156
201
|
if pods_cache.key?(pod_name)
|
157
202
|
result.push(*pods_cache[pod_name])
|
158
203
|
dependencies_hash[pod_name] = pods_cache[pod_name].empty? ? [] : pods_cache[pod_name]
|
159
204
|
end
|
160
|
-
|
205
|
+
end
|
161
206
|
|
162
207
|
result = result.uniq
|
163
|
-
# maybe additional dependency processing needed here???
|
164
208
|
pods_used = result
|
165
209
|
end
|
166
210
|
|
167
|
-
|
211
|
+
[result, dependencies_hash]
|
212
|
+
end
|
213
|
+
|
214
|
+
def top_level_pods(podfile)
|
215
|
+
included_targets = podfile.target_definition_list.select { |target| include_target_named(target.label) }
|
216
|
+
included_target_names = included_targets.map(&:label)
|
217
|
+
@logger.debug "Including all pods for targets: #{included_target_names}"
|
218
|
+
|
219
|
+
top_level_deps = included_targets.map(&:dependencies).flatten.uniq
|
220
|
+
top_level_deps.map(&:name).uniq
|
168
221
|
end
|
169
222
|
|
170
223
|
def create_list_of_included_pods(podfile, lockfile)
|
171
224
|
pods_cache = simple_hash_of_lockfile_pods(lockfile)
|
172
225
|
|
173
|
-
|
174
|
-
includedTargetNames = includedTargets.map { |target| target.label }
|
175
|
-
@logger.debug "Including all pods for targets: #{includedTargetNames}"
|
176
|
-
|
177
|
-
topLevelDeps = includedTargets.map(&:dependencies).flatten.uniq
|
178
|
-
pods_used = topLevelDeps.map(&:name).uniq
|
226
|
+
pods_used = top_level_pods(podfile)
|
179
227
|
pods_used, dependencies = append_all_pod_dependencies(pods_used, pods_cache)
|
180
228
|
|
181
|
-
|
229
|
+
[pods_used.sort, dependencies]
|
182
230
|
end
|
183
231
|
|
184
|
-
|
185
232
|
def include_target_named(targetname)
|
186
|
-
|
233
|
+
!@exclude_test_targets || !targetname.downcase.include?('test')
|
187
234
|
end
|
188
235
|
|
189
|
-
|
190
236
|
def cocoapods_repository_source(podfile, lockfile, pod_name)
|
191
237
|
@source_manager ||= create_source_manager(podfile)
|
192
|
-
|
238
|
+
Source::CocoaPodsRepository.searchable_source(url: lockfile.spec_repo(pod_name),
|
239
|
+
source_manager: @source_manager)
|
193
240
|
end
|
194
241
|
|
195
|
-
|
196
242
|
def git_source(lockfile, pod_name)
|
197
243
|
checkout_options = lockfile.checkout_options_for_pod_named(pod_name)
|
198
244
|
url = checkout_options[:git]
|
199
|
-
[
|
200
|
-
|
245
|
+
%i[tag branch commit].each do |type|
|
246
|
+
if checkout_options[type]
|
247
|
+
return Source::GitRepository.new(url: url, type: type,
|
248
|
+
label: checkout_options[type])
|
249
|
+
end
|
201
250
|
end
|
202
|
-
|
251
|
+
Source::GitRepository.new(url: url)
|
203
252
|
end
|
204
253
|
|
205
|
-
|
206
254
|
def source_for_pod(podfile, lockfile, pod_name)
|
207
255
|
root_name = pod_name.split('/').first
|
208
256
|
return cocoapods_repository_source(podfile, lockfile, root_name) unless lockfile.spec_repo(root_name).nil?
|
209
257
|
return git_source(lockfile, root_name) unless lockfile.checkout_options_for_pod_named(root_name).nil?
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
258
|
+
if lockfile.to_hash['EXTERNAL SOURCES'][root_name][:path]
|
259
|
+
return Source::LocalPod.new(path: lockfile.to_hash['EXTERNAL SOURCES'][root_name][:path])
|
260
|
+
end
|
261
|
+
if lockfile.to_hash['EXTERNAL SOURCES'][root_name][:podspec]
|
262
|
+
return Source::Podspec.new(url: lockfile.to_hash['EXTERNAL SOURCES'][root_name][:podspec])
|
263
|
+
end
|
214
264
|
|
265
|
+
nil
|
266
|
+
end
|
215
267
|
|
216
268
|
def create_source_manager(podfile)
|
217
|
-
|
269
|
+
source_manager = ::Pod::Source::Manager.new(::Pod::Config.instance.repos_dir)
|
218
270
|
@logger.debug "Parsing sources from #{podfile.defined_in_file}"
|
219
271
|
podfile.sources.each do |source|
|
220
272
|
@logger.debug "Ensuring #{source} is available for searches"
|
221
|
-
|
273
|
+
source_manager.find_or_create_source_with_url(source)
|
222
274
|
end
|
223
|
-
@logger.debug
|
224
|
-
|
275
|
+
@logger.debug 'Source manager successfully created with all needed sources'
|
276
|
+
source_manager
|
225
277
|
end
|
226
278
|
end
|
227
279
|
end
|
@@ -31,13 +31,16 @@ module CycloneDX
|
|
31
31
|
end
|
32
32
|
|
33
33
|
class GitRepository
|
34
|
-
VALID_TYPES = [
|
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.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- José González
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: exe
|
11
11
|
cert_chain: []
|
12
|
-
date: 2024-
|
12
|
+
date: 2024-10-15 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: cocoapods
|
@@ -52,47 +52,47 @@ dependencies:
|
|
52
52
|
- !ruby/object:Gem::Version
|
53
53
|
version: '2.0'
|
54
54
|
- !ruby/object:Gem::Dependency
|
55
|
-
name:
|
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.16
|
144
144
|
signing_key:
|
145
145
|
specification_version: 4
|
146
146
|
summary: CycloneDX software bill-of-material (SBOM) generation utility
|