cyclonedx-cocoapods 1.4.1 → 2.0.1
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 +22 -0
- data/README.md +31 -13
- data/lib/cyclonedx/cocoapods/bom_builder.rb +253 -22
- data/lib/cyclonedx/cocoapods/cli_runner.rb +122 -16
- data/lib/cyclonedx/cocoapods/component.rb +72 -12
- data/lib/cyclonedx/cocoapods/license.rb +1 -1
- data/lib/cyclonedx/cocoapods/manufacturer.rb +77 -0
- data/lib/cyclonedx/cocoapods/pod.rb +4 -4
- data/lib/cyclonedx/cocoapods/podspec_analyzer.rb +109 -0
- data/lib/cyclonedx/cocoapods/version.rb +1 -1
- metadata +5 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a00a26d3a6f6e7ce71f6c44dbc4eb4dcd046e4c39e5f9a8d8355bf85af85cf16
|
4
|
+
data.tar.gz: d3972e003471c6c71c43c3b83b1373b85efc6b8f78c124d281c23dc9184b83d1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a042823ac822253618165386e3de44e12921f381ea71ae368b75201765ec9d4b2787fbce3bd23cd33cd8bf956c4bacf9929fe0205b5d72991204179223fc0377
|
7
|
+
data.tar.gz: c0665793c4cad2ede693213dbaa866e163af3e44d74e0c96903369a5cc6d5d4706d6cefd900d656553a27f7b451fcabc82e7ef414f7d5b9a4c9538db3d6dff6d
|
data/CHANGELOG.md
CHANGED
@@ -4,6 +4,28 @@ 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
|
+
## [2.0.1]
|
8
|
+
|
9
|
+
### Fixed
|
10
|
+
- Fixed JSON output to use an integer for the bom file version number. ([Issue #89](https://github.com/CycloneDX/cyclonedx-cocoapods/issues/89)) [@macblazer](https://github.com/macblazer).
|
11
|
+
|
12
|
+
## [2.0.0]
|
13
|
+
|
14
|
+
### Added
|
15
|
+
- Added JSON output if the specified `output` has a `.json` suffix. ([Issue #62](https://github.com/CycloneDX/cyclonedx-cocoapods/issues/62)) [@jeremylong](https://github.com/jeremylong).
|
16
|
+
- Added CLI options to set manufacturer metadata about the component being scanned (five separate parameters). ([Issue #72](https://github.com/CycloneDX/cyclonedx-cocoapods/issues/72)) [@jeremylong](https://github.com/jeremylong).
|
17
|
+
- Added CLI options to set the VCS URL and build URL of the component being scanned. ([PR #82](https://github.com/CycloneDX/cyclonedx-cocoapods/pull/82)) [@jeremylong](https://github.com/jeremylong).
|
18
|
+
|
19
|
+
### Changed
|
20
|
+
- Updated to use v1.6 of the CycloneDX specification. ([PR #81](https://github.com/CycloneDX/cyclonedx-cocoapods/pull/81)) [@jeremylong](https://github.com/jeremylong).
|
21
|
+
- Updated to use newer `tools` section elements. ([PR #80](https://github.com/CycloneDX/cyclonedx-cocoapods/pull/80)) [@jeremylong](https://github.com/jeremylong).
|
22
|
+
- Updated to use a purl for the `bom-ref` of the component being scanned. When analyzing an app the purl will start with `pkg:generic`. ([PR #84](https://github.com/CycloneDX/cyclonedx-cocoapods/pull/84)) [@jeremylong](https://github.com/jeremylong).
|
23
|
+
- Changed the short `-b` CLI parameter to specify the build URL instead of the bom file version. Use `--bom-version` to specify the bom file version if needed. ([PR #82](https://github.com/CycloneDX/cyclonedx-cocoapods/pull/82)) [@jeremylong](https://github.com/jeremylong).
|
24
|
+
- Changed the short `-s` CLI parameter to specify the source VCS URL instead of the shortened string lengths. Use `--shortened-strings` to specify the max length of strings if needed. ([PR #82](https://github.com/CycloneDX/cyclonedx-cocoapods/pull/82)) [@jeremylong](https://github.com/jeremylong).
|
25
|
+
|
26
|
+
### Fixed
|
27
|
+
- Fixed XML output when Pod description contains a null byte. ([Issue #85](https://github.com/CycloneDX/cyclonedx-cocoapods/issues/85)) [@fnxpt](https://github.com/fnxpt).
|
28
|
+
|
7
29
|
## [1.4.1]
|
8
30
|
|
9
31
|
### Changed
|
data/README.md
CHANGED
@@ -9,7 +9,9 @@
|
|
9
9
|
|
10
10
|
# CycloneDX CocoaPods (Objective-C/Swift)
|
11
11
|
|
12
|
-
The CycloneDX CocoaPods Gem creates a valid CycloneDX software bill-of-material document from all
|
12
|
+
The CycloneDX CocoaPods Gem creates a valid CycloneDX software bill-of-material document from all
|
13
|
+
[CocoaPods](https://cocoapods.org/) project dependencies. CycloneDX is a lightweight BOM specification
|
14
|
+
that is easily created, human readable, and simple to parse.
|
13
15
|
|
14
16
|
## Installation
|
15
17
|
|
@@ -21,18 +23,19 @@ The CycloneDX CocoaPods Gem creates a valid CycloneDX software bill-of-material
|
|
21
23
|
|
22
24
|
### From Source
|
23
25
|
|
24
|
-
First, clone/copy the source code from GitHub. Then in the source code directory run these
|
26
|
+
First, clone/copy the source code from GitHub. Then in the source code directory run these
|
27
|
+
commands (substituting the actual version number for `x.x.x`):
|
25
28
|
|
26
29
|
```shell
|
27
30
|
gem build cyclonedx-cocoapods.gemspec
|
28
31
|
gem install cyclonedx-cocoapods-x.x.x.gem
|
29
32
|
```
|
30
33
|
|
31
|
-
Building from source requires Ruby 2.
|
34
|
+
Building from source requires Ruby 2.6.3 or newer.
|
32
35
|
|
33
36
|
## Compatibility
|
34
37
|
|
35
|
-
*cyclonedx-cocoapods* aims to produce SBOMs according to the latest CycloneDX specification, which currently is [1.
|
38
|
+
*cyclonedx-cocoapods* aims to produce SBOMs according to the latest CycloneDX specification, which currently is [1.6](https://cyclonedx.org/docs/1.6/xml/).
|
36
39
|
You can use the [CycloneDX CLI](https://github.com/CycloneDX/cyclonedx-cli#convert-command) to convert between multiple BOM formats or specification versions.
|
37
40
|
|
38
41
|
## Usage
|
@@ -49,38 +52,53 @@ OPTIONS
|
|
49
52
|
|
50
53
|
BOM Generation
|
51
54
|
-p, --path path Path to CocoaPods project directory (default: current directory)
|
52
|
-
-o, --output bom_file_path Path to output the bom
|
53
|
-
|
55
|
+
-o, --output bom_file_path Path to output the bom file to (default: "bom.xml"); if a *.json file is specified the output format will be JSON
|
56
|
+
--bom-version bom_version Version of the generated BOM (default: "1")
|
54
57
|
-x, --exclude-test-targets Eliminate Podfile targets whose name contains the word "test"
|
55
|
-
|
58
|
+
--shortened-strings length Trim author, publisher, and purl to <length> characters; this may cause data loss but can improve compatibility with other systems
|
56
59
|
|
57
60
|
Component Metadata
|
61
|
+
If a podspec file is present the name, version, and type do not need to be specified as they will be set automatically.
|
58
62
|
-n, --name name (If specified version and type are also required) Name of the component for which the BOM is generated
|
59
63
|
-v, --version version Version of the component for which the BOM is generated
|
60
64
|
-t, --type type Type of the component for which the BOM is generated (one of application|framework|library|container|operating-system|device|firmware|file)
|
61
65
|
-g, --group group Group of the component for which the BOM is generated
|
66
|
+
-s, --source source_url Optional: The version control system URL of the component for the BOM is generated
|
67
|
+
-b, --build build_url Optional: The build URL of the component for which the BOM is generated
|
68
|
+
|
69
|
+
Manufacturer Metadata
|
70
|
+
--manufacturer-name name Name of the manufacturer
|
71
|
+
--manufacturer-url url URL of the manufacturer
|
72
|
+
--manufacturer-contact-name name
|
73
|
+
Name of the manufacturer contact
|
74
|
+
--manufacturer-email email Email of the manufacturer contact
|
75
|
+
--manufacturer-phone phone Phone number of the manufacturer contact
|
62
76
|
```
|
63
77
|
|
64
|
-
**Output:**
|
78
|
+
**Output:** BOM file at specified location, `./bom.xml` if not specified
|
65
79
|
|
66
80
|
### Example
|
67
81
|
|
68
82
|
```shell
|
69
|
-
% cyclonedx-cocoapods --path /path/to/cocoapods/project --output /path/to/bom.xml --version 6
|
83
|
+
% cyclonedx-cocoapods --path /path/to/cocoapods/project --output /path/to/bom.xml --version 6
|
70
84
|
```
|
71
85
|
|
72
86
|
#### Specific example
|
73
87
|
|
74
|
-
This repo contains
|
88
|
+
This repo contains files named `example_bom.xml` and `example_bom.json` that were generated with this tool.
|
75
89
|
|
76
|
-
|
77
|
-
then these
|
90
|
+
They represent the open source [PodsUpdater application](https://github.com/kizitonwose/PodsUpdater). The PodsUpdater
|
91
|
+
code was checked out, then these three commands were run in the checked out code directory.
|
78
92
|
|
79
93
|
```shell
|
80
94
|
% pod install
|
81
|
-
% cyclonedx-cocoapods -n "kizitonwose
|
95
|
+
% cyclonedx-cocoapods -n "kizitonwose-PodsUpdater" -v 1.0.3 -t application -s https://github.com/kizitonwose/PodsUpdater --output example_bom.xml
|
96
|
+
% cyclonedx-cocoapods -n "kizitonwose-PodsUpdater" -v 1.0.3 -t application -s https://github.com/kizitonwose/PodsUpdater --output example_bom.json
|
82
97
|
```
|
83
98
|
|
99
|
+
The JSON file here has also been run through a JSON formatter for easier reading by humans. The original JSON
|
100
|
+
output is one long line with no extra whitespace - great for computers, but difficult for humans.
|
101
|
+
|
84
102
|
### A Note About CocoaPod Subspecs
|
85
103
|
|
86
104
|
Many CocoaPods make use of [subspec functionality](https://guides.cocoapods.org/syntax/podspec.html#subspec).
|
@@ -127,7 +127,8 @@ module CycloneDX
|
|
127
127
|
xml_add_author(xml, trim_strings_length)
|
128
128
|
xml.name_ name
|
129
129
|
xml.version version.to_s
|
130
|
-
|
130
|
+
# Use `dump` to escape non-printing characters, then remove the starting/trailing double-quotes from `dump`.
|
131
|
+
xml.description { xml.cdata description.dump[1..-2] } unless description.nil?
|
131
132
|
unless checksum.nil?
|
132
133
|
xml.hashes do
|
133
134
|
xml.hash_(checksum, alg: CHECKSUM_ALGORITHM)
|
@@ -149,7 +150,57 @@ module CycloneDX
|
|
149
150
|
end
|
150
151
|
end
|
151
152
|
|
153
|
+
def to_json_component(manifest_path, trim_strings_length = 0)
|
154
|
+
{
|
155
|
+
type: 'library',
|
156
|
+
'bom-ref': purl,
|
157
|
+
author: trim(author, trim_strings_length),
|
158
|
+
publisher: trim(author, trim_strings_length),
|
159
|
+
name: name,
|
160
|
+
version: version.to_s,
|
161
|
+
description: description,
|
162
|
+
hashes: generate_json_hashes,
|
163
|
+
licenses: generate_json_licenses,
|
164
|
+
purl: purl,
|
165
|
+
externalReferences: generate_json_external_references,
|
166
|
+
evidence: generate_json_evidence(manifest_path)
|
167
|
+
}.compact
|
168
|
+
end
|
169
|
+
|
170
|
+
def generate_json_external_references
|
171
|
+
refs = []
|
172
|
+
refs << { type: HOMEPAGE_REFERENCE_TYPE, url: homepage } if homepage
|
173
|
+
refs.empty? ? nil : refs
|
174
|
+
end
|
175
|
+
|
176
|
+
def generate_json_evidence(manifest_path)
|
177
|
+
{
|
178
|
+
identity: {
|
179
|
+
field: 'purl',
|
180
|
+
confidence: 0.6,
|
181
|
+
methods: [
|
182
|
+
{
|
183
|
+
technique: 'manifest-analysis',
|
184
|
+
confidence: 0.6,
|
185
|
+
value: manifest_path
|
186
|
+
}
|
187
|
+
]
|
188
|
+
}
|
189
|
+
}
|
190
|
+
end
|
191
|
+
|
152
192
|
class License
|
193
|
+
def to_json_component
|
194
|
+
{
|
195
|
+
license: {
|
196
|
+
id: identifier_type == :id ? identifier : nil,
|
197
|
+
name: identifier_type == :name ? identifier : nil,
|
198
|
+
text: text,
|
199
|
+
url: url
|
200
|
+
}.compact
|
201
|
+
}
|
202
|
+
end
|
203
|
+
|
153
204
|
def add_to_bom(xml)
|
154
205
|
xml.license do
|
155
206
|
xml.id identifier if identifier_type == :id
|
@@ -159,6 +210,20 @@ module CycloneDX
|
|
159
210
|
end
|
160
211
|
end
|
161
212
|
end
|
213
|
+
|
214
|
+
private
|
215
|
+
|
216
|
+
def generate_json_licenses
|
217
|
+
license ? [license.to_json_component] : nil
|
218
|
+
end
|
219
|
+
|
220
|
+
def generate_json_hashes
|
221
|
+
checksum ? [{ alg: CHECKSUM_ALGORITHM, content: checksum }] : nil
|
222
|
+
end
|
223
|
+
|
224
|
+
def trim(str, trim_strings_length)
|
225
|
+
trim_strings_length.zero? ? str : str&.slice(0, trim_strings_length)
|
226
|
+
end
|
162
227
|
end
|
163
228
|
|
164
229
|
class Component
|
@@ -167,52 +232,215 @@ module CycloneDX
|
|
167
232
|
xml.group group unless group.nil?
|
168
233
|
xml.name_ name
|
169
234
|
xml.version version
|
235
|
+
|
236
|
+
if !build_system.nil? || !vcs.nil?
|
237
|
+
xml.externalReferences do
|
238
|
+
if build_system
|
239
|
+
xml.reference(type: 'build-system') do
|
240
|
+
xml.url build_system
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
if vcs
|
245
|
+
xml.reference(type: 'vcs') do
|
246
|
+
xml.url vcs
|
247
|
+
end
|
248
|
+
end
|
249
|
+
end
|
250
|
+
end
|
251
|
+
xml.purl bomref
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
def to_json_component
|
256
|
+
{
|
257
|
+
type: type,
|
258
|
+
'bom-ref': bomref,
|
259
|
+
group: group,
|
260
|
+
name: name,
|
261
|
+
version: version,
|
262
|
+
purl: bomref,
|
263
|
+
externalReferences: generate_json_external_references
|
264
|
+
}.compact
|
265
|
+
end
|
266
|
+
|
267
|
+
private
|
268
|
+
|
269
|
+
def generate_json_external_references
|
270
|
+
refs = []
|
271
|
+
refs << { type: 'build-system', url: build_system } if build_system
|
272
|
+
refs << { type: 'vcs', url: vcs } if vcs
|
273
|
+
refs.empty? ? nil : refs
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
# Represents manufacturer information in a CycloneDX BOM
|
278
|
+
# Handles generation of manufacturer XML elements including basic info and contact details
|
279
|
+
# Used when generating BOM metadata for CycloneDX specification
|
280
|
+
class Manufacturer
|
281
|
+
def add_to_bom(xml)
|
282
|
+
return if all_attributes_nil?
|
283
|
+
|
284
|
+
xml.manufacturer do
|
285
|
+
add_basic_info(xml)
|
286
|
+
add_contact_info(xml)
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
def to_json_manufacturer
|
291
|
+
return nil if all_attributes_nil?
|
292
|
+
|
293
|
+
{
|
294
|
+
name: name,
|
295
|
+
url: url,
|
296
|
+
contact: generate_json_contact
|
297
|
+
}.compact
|
298
|
+
end
|
299
|
+
|
300
|
+
private
|
301
|
+
|
302
|
+
def generate_json_contact
|
303
|
+
return nil if contact_info_nil?
|
304
|
+
|
305
|
+
[
|
306
|
+
{
|
307
|
+
name: contact_name,
|
308
|
+
email: email,
|
309
|
+
phone: phone
|
310
|
+
}.compact
|
311
|
+
]
|
312
|
+
end
|
313
|
+
|
314
|
+
def all_attributes_nil?
|
315
|
+
[name, url, contact_name, email, phone].all?(&:nil?)
|
316
|
+
end
|
317
|
+
|
318
|
+
def add_basic_info(xml)
|
319
|
+
xml.name_ name unless name.nil?
|
320
|
+
xml.url url unless url.nil?
|
321
|
+
end
|
322
|
+
|
323
|
+
def add_contact_info(xml)
|
324
|
+
return if contact_info_nil?
|
325
|
+
|
326
|
+
xml.contact do
|
327
|
+
xml.name_ contact_name unless contact_name.nil?
|
328
|
+
xml.email email unless email.nil?
|
329
|
+
xml.phone phone unless phone.nil?
|
170
330
|
end
|
171
331
|
end
|
332
|
+
|
333
|
+
def contact_info_nil?
|
334
|
+
contact_name.nil? && email.nil? && phone.nil?
|
335
|
+
end
|
172
336
|
end
|
173
337
|
|
174
338
|
# Turns the internal model data into an XML bom.
|
175
339
|
class BOMBuilder
|
176
|
-
NAMESPACE = 'http://cyclonedx.org/schema/bom/1.
|
340
|
+
NAMESPACE = 'http://cyclonedx.org/schema/bom/1.6'
|
177
341
|
|
178
|
-
attr_reader :component, :pods, :manifest_path, :dependencies
|
342
|
+
attr_reader :component, :pods, :manifest_path, :dependencies, :manufacturer
|
179
343
|
|
180
|
-
def initialize(pods:, manifest_path:, component: nil, dependencies: nil)
|
344
|
+
def initialize(pods:, manifest_path:, component: nil, dependencies: nil, manufacturer: nil)
|
181
345
|
@pods = pods.sort_by(&:purl)
|
182
346
|
@manifest_path = manifest_path
|
183
347
|
@component = component
|
184
348
|
@dependencies = dependencies&.sort
|
349
|
+
@manufacturer = manufacturer
|
185
350
|
end
|
186
351
|
|
187
|
-
def bom(version: 1, trim_strings_length: 0)
|
188
|
-
|
189
|
-
|
190
|
-
|
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
|
352
|
+
def bom(version: 1, trim_strings_length: 0, format: :xml)
|
353
|
+
validate_version(version)
|
354
|
+
validate_trim_length(trim_strings_length)
|
355
|
+
validate_format(format)
|
197
356
|
|
198
|
-
unchecked_bom(version: version, trim_strings_length: trim_strings_length)
|
357
|
+
unchecked_bom(version: version, trim_strings_length: trim_strings_length, format: format)
|
199
358
|
end
|
200
359
|
|
201
360
|
private
|
202
361
|
|
362
|
+
def validate_version(version)
|
363
|
+
return if version.to_i.positive?
|
364
|
+
|
365
|
+
raise ArgumentError, "Incorrect version: #{version} should be an integer greater than 0"
|
366
|
+
end
|
367
|
+
|
368
|
+
def validate_trim_length(trim_strings_length)
|
369
|
+
return if trim_strings_length.is_a?(Integer) && (trim_strings_length.positive? || trim_strings_length.zero?)
|
370
|
+
|
371
|
+
raise ArgumentError, "Incorrect string length: #{trim_strings_length} should be an integer greater than 0"
|
372
|
+
end
|
373
|
+
|
374
|
+
def validate_format(format)
|
375
|
+
return if %i[xml json].include?(format)
|
376
|
+
|
377
|
+
raise ArgumentError, "Incorrect format: #{format} should be either :xml or :json"
|
378
|
+
end
|
379
|
+
|
203
380
|
# does not verify parameters because the public method does that.
|
204
|
-
def unchecked_bom(version
|
381
|
+
def unchecked_bom(version:, trim_strings_length:, format:)
|
382
|
+
case format
|
383
|
+
when :json
|
384
|
+
generate_json(version: version, trim_strings_length: trim_strings_length)
|
385
|
+
when :xml
|
386
|
+
generate_xml(version: version, trim_strings_length: trim_strings_length)
|
387
|
+
end
|
388
|
+
end
|
389
|
+
|
390
|
+
def generate_xml(version:, trim_strings_length:)
|
205
391
|
Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml|
|
206
392
|
xml.bom(xmlns: NAMESPACE, version: version.to_i.to_s, serialNumber: "urn:uuid:#{SecureRandom.uuid}") do
|
207
393
|
bom_metadata(xml)
|
208
|
-
|
209
394
|
bom_components(xml, pods, manifest_path, trim_strings_length)
|
210
|
-
|
211
395
|
bom_dependencies(xml, dependencies)
|
212
396
|
end
|
213
397
|
end.to_xml
|
214
398
|
end
|
215
399
|
|
400
|
+
def generate_json(version:, trim_strings_length:)
|
401
|
+
{
|
402
|
+
'$schema': 'https://cyclonedx.org/schema/bom-1.6.schema.json',
|
403
|
+
bomFormat: 'CycloneDX',
|
404
|
+
specVersion: '1.6',
|
405
|
+
serialNumber: "urn:uuid:#{SecureRandom.uuid}",
|
406
|
+
version: version.to_i,
|
407
|
+
metadata: generate_json_metadata,
|
408
|
+
components: generate_json_components(trim_strings_length),
|
409
|
+
dependencies: generate_json_dependencies
|
410
|
+
}.to_json
|
411
|
+
end
|
412
|
+
|
413
|
+
def generate_json_metadata
|
414
|
+
{
|
415
|
+
timestamp: Time.now.getutc.strftime('%Y-%m-%dT%H:%M:%SZ'),
|
416
|
+
tools: {
|
417
|
+
components: [{
|
418
|
+
type: 'application',
|
419
|
+
group: 'CycloneDX',
|
420
|
+
name: 'cyclonedx-cocoapods',
|
421
|
+
version: VERSION
|
422
|
+
}]
|
423
|
+
},
|
424
|
+
component: component&.to_json_component,
|
425
|
+
manufacturer: manufacturer&.to_json_manufacturer
|
426
|
+
}.compact
|
427
|
+
end
|
428
|
+
|
429
|
+
def generate_json_components(trim_strings_length)
|
430
|
+
pods.map { |pod| pod.to_json_component(manifest_path, trim_strings_length) }
|
431
|
+
end
|
432
|
+
|
433
|
+
def generate_json_dependencies
|
434
|
+
return nil unless dependencies
|
435
|
+
|
436
|
+
dependencies.map do |ref, deps|
|
437
|
+
{
|
438
|
+
ref: ref,
|
439
|
+
dependsOn: deps.sort
|
440
|
+
}
|
441
|
+
end
|
442
|
+
end
|
443
|
+
|
216
444
|
def bom_components(xml, pods, manifest_path, trim_strings_length)
|
217
445
|
xml.components do
|
218
446
|
pods.each do |pod|
|
@@ -238,15 +466,18 @@ module CycloneDX
|
|
238
466
|
xml.timestamp Time.now.getutc.strftime('%Y-%m-%dT%H:%M:%SZ')
|
239
467
|
bom_tools(xml)
|
240
468
|
component&.add_to_bom(xml)
|
469
|
+
manufacturer&.add_to_bom(xml)
|
241
470
|
end
|
242
471
|
end
|
243
472
|
|
244
473
|
def bom_tools(xml)
|
245
474
|
xml.tools do
|
246
|
-
xml.
|
247
|
-
xml.
|
248
|
-
|
249
|
-
|
475
|
+
xml.components do
|
476
|
+
xml.component(type: 'application') do
|
477
|
+
xml.group 'CycloneDX'
|
478
|
+
xml.name_ 'cyclonedx-cocoapods'
|
479
|
+
xml.version VERSION
|
480
|
+
end
|
250
481
|
end
|
251
482
|
end
|
252
483
|
end
|
@@ -25,7 +25,9 @@ require 'optparse'
|
|
25
25
|
|
26
26
|
require_relative 'bom_builder'
|
27
27
|
require_relative 'component'
|
28
|
+
require_relative 'manufacturer'
|
28
29
|
require_relative 'podfile_analyzer'
|
30
|
+
require_relative 'podspec_analyzer'
|
29
31
|
|
30
32
|
module CycloneDX
|
31
33
|
module CocoaPods
|
@@ -36,12 +38,13 @@ module CycloneDX
|
|
36
38
|
def run
|
37
39
|
setup_logger # Needed in case we have errors while processing CLI parameters
|
38
40
|
options = parse_options
|
41
|
+
determine_output_format(options)
|
39
42
|
setup_logger(verbose: options[:verbose])
|
40
43
|
@logger.debug "Running cyclonedx-cocoapods with options: #{options}"
|
41
44
|
|
42
|
-
component, pods, manifest_path, dependencies = analyze(options)
|
45
|
+
component, manufacturer, pods, manifest_path, dependencies = analyze(options)
|
43
46
|
|
44
|
-
build_and_write_bom(options, component, pods, manifest_path, dependencies)
|
47
|
+
build_and_write_bom(options, component, manufacturer, pods, manifest_path, dependencies)
|
45
48
|
rescue StandardError => e
|
46
49
|
@logger.error ([e.message] + e.backtrace).join($INPUT_RECORD_SEPARATOR)
|
47
50
|
exit 1
|
@@ -76,7 +79,8 @@ module CycloneDX
|
|
76
79
|
parsed_options[:path] = path
|
77
80
|
end
|
78
81
|
options.on('-o', '--output bom_file_path',
|
79
|
-
'Path to output the bom
|
82
|
+
'Path to output the bom file to (default: "bom.xml"); ' \
|
83
|
+
'if a *.json file is specified the output format will be JSON') do |bom_file_path|
|
80
84
|
parsed_options[:bom_file_path] = bom_file_path
|
81
85
|
end
|
82
86
|
options.on('-b', '--bom-version bom_version', Integer,
|
@@ -94,6 +98,8 @@ module CycloneDX
|
|
94
98
|
end
|
95
99
|
|
96
100
|
options.separator("\n Component Metadata\n")
|
101
|
+
options.separator(' If a podspec file is present the name, version, and type do not ' \
|
102
|
+
"need to be specified as they will be set automatically.\n")
|
97
103
|
options.on('-n', '--name name',
|
98
104
|
'(If specified version and type are also required) Name of the ' \
|
99
105
|
'component for which the BOM is generated') do |name|
|
@@ -118,6 +124,37 @@ module CycloneDX
|
|
118
124
|
options.on('-g', '--group group', 'Group of the component for which the BOM is generated') do |group|
|
119
125
|
parsed_options[:group] = group
|
120
126
|
end
|
127
|
+
options.on('-s', '--source source_url',
|
128
|
+
'Optional: The version control system URL of the component for the BOM is generated') do |vcs|
|
129
|
+
parsed_options[:vcs] = vcs
|
130
|
+
end
|
131
|
+
options.on('-b', '--build build_url',
|
132
|
+
'Optional: The build URL of the component for which the BOM is generated') do |build|
|
133
|
+
parsed_options[:build] = build
|
134
|
+
end
|
135
|
+
|
136
|
+
# Add this section after the "Component Metadata" options group
|
137
|
+
options.separator("\n Manufacturer Metadata\n")
|
138
|
+
options.on('--manufacturer-name name',
|
139
|
+
'Name of the manufacturer') do |name|
|
140
|
+
parsed_options[:manufacturer_name] = name
|
141
|
+
end
|
142
|
+
options.on('--manufacturer-url url',
|
143
|
+
'URL of the manufacturer') do |url|
|
144
|
+
parsed_options[:manufacturer_url] = url
|
145
|
+
end
|
146
|
+
options.on('--manufacturer-contact-name name',
|
147
|
+
'Name of the manufacturer contact') do |contact_name|
|
148
|
+
parsed_options[:manufacturer_contact_name] = contact_name
|
149
|
+
end
|
150
|
+
options.on('--manufacturer-email email',
|
151
|
+
'Email of the manufacturer contact') do |email|
|
152
|
+
parsed_options[:manufacturer_email] = email
|
153
|
+
end
|
154
|
+
options.on('--manufacturer-phone phone',
|
155
|
+
'Phone number of the manufacturer contact') do |phone|
|
156
|
+
parsed_options[:manufacturer_phone] = phone
|
157
|
+
end
|
121
158
|
end.parse!
|
122
159
|
|
123
160
|
if !parsed_options[:name].nil? && (parsed_options[:version].nil? || parsed_options[:type].nil?)
|
@@ -128,13 +165,15 @@ module CycloneDX
|
|
128
165
|
parsed_options
|
129
166
|
end
|
130
167
|
|
131
|
-
def
|
132
|
-
|
133
|
-
|
134
|
-
pods, dependencies = analyzer.parse_pods(podfile, lockfile)
|
135
|
-
analyzer.populate_pods_with_additional_info(pods)
|
168
|
+
def determine_output_format(options)
|
169
|
+
options[:format] = options[:bom_file_path]&.end_with?('.json') ? :json : :xml
|
170
|
+
end
|
136
171
|
|
137
|
-
|
172
|
+
def analyze(options)
|
173
|
+
analyzer, dependencies, lockfile, podfile, pods = analyze_podfile(options)
|
174
|
+
podspec = analyze_podspec(options)
|
175
|
+
component = component_from_options(options, podspec)
|
176
|
+
manufacturer = manufacturer_from_options(options)
|
138
177
|
|
139
178
|
unless component.nil?
|
140
179
|
# add top level pods to main component
|
@@ -148,22 +187,89 @@ module CycloneDX
|
|
148
187
|
manifest_path = Pathname.pwd.basename + manifest_path.relative_path_from(Pathname.pwd)
|
149
188
|
end
|
150
189
|
|
151
|
-
[component, pods, manifest_path, dependencies]
|
190
|
+
[component, manufacturer, pods, manifest_path, dependencies]
|
191
|
+
end
|
192
|
+
|
193
|
+
def analyze_podspec(options)
|
194
|
+
spec_analyzer = PodspecAnalyzer.new(logger: @logger)
|
195
|
+
podspec = spec_analyzer.ensure_podspec_is_present(options)
|
196
|
+
spec = spec_analyzer.parse_podspec(podspec) unless podspec.nil?
|
197
|
+
spec
|
152
198
|
end
|
153
199
|
|
154
|
-
def
|
200
|
+
def analyze_podfile(options)
|
201
|
+
analyzer = PodfileAnalyzer.new(logger: @logger, exclude_test_targets: options[:exclude_test_targets])
|
202
|
+
podfile, lockfile = analyzer.ensure_podfile_and_lock_are_present(options)
|
203
|
+
pods, dependencies = analyzer.parse_pods(podfile, lockfile)
|
204
|
+
analyzer.populate_pods_with_additional_info(pods)
|
205
|
+
[analyzer, dependencies, lockfile, podfile, pods]
|
206
|
+
end
|
207
|
+
|
208
|
+
def build_and_write_bom(options, component, manufacturer, pods, manifest_path, dependencies)
|
155
209
|
builder = BOMBuilder.new(pods: pods, manifest_path: manifest_path,
|
156
|
-
component: component, dependencies: dependencies)
|
210
|
+
component: component, manufacturer: manufacturer, dependencies: dependencies)
|
157
211
|
bom = builder.bom(version: options[:bom_version] || 1,
|
158
|
-
trim_strings_length: options[:trim_strings_length] || 0
|
212
|
+
trim_strings_length: options[:trim_strings_length] || 0,
|
213
|
+
format: options[:format])
|
159
214
|
write_bom_to_file(bom: bom, options: options)
|
160
215
|
end
|
161
216
|
|
162
|
-
def component_from_options(options)
|
163
|
-
return unless options[:name]
|
217
|
+
def component_from_options(options, podspec)
|
218
|
+
return unless options[:name] || !podspec.nil?
|
219
|
+
|
220
|
+
ensure_options_match(options, podspec)
|
164
221
|
|
165
222
|
Component.new(group: options[:group], name: options[:name], version: options[:version],
|
166
|
-
type: options[:type])
|
223
|
+
type: options[:type], build_system: options[:build], vcs: options[:vcs])
|
224
|
+
end
|
225
|
+
|
226
|
+
def manufacturer_from_options(options)
|
227
|
+
Manufacturer.new(
|
228
|
+
name: options[:manufacturer_name],
|
229
|
+
url: options[:manufacturer_url],
|
230
|
+
contact_name: options[:manufacturer_contact_name],
|
231
|
+
email: options[:manufacturer_email],
|
232
|
+
phone: options[:manufacturer_phone]
|
233
|
+
)
|
234
|
+
end
|
235
|
+
|
236
|
+
def ensure_options_match(options, podspec)
|
237
|
+
validate_name_option(options, podspec)
|
238
|
+
validate_version_option(options, podspec)
|
239
|
+
validate_group_option(options, podspec)
|
240
|
+
validate_type_option(options, podspec)
|
241
|
+
end
|
242
|
+
|
243
|
+
def validate_name_option(options, podspec)
|
244
|
+
if !podspec.nil? && options[:name] && options[:name] != podspec.name
|
245
|
+
raise OptionParser::InvalidArgument,
|
246
|
+
"Component name '#{options[:name]}' does not match podspec name '#{podspec.name}'"
|
247
|
+
end
|
248
|
+
options[:name] ||= podspec&.name
|
249
|
+
end
|
250
|
+
|
251
|
+
def validate_version_option(options, podspec)
|
252
|
+
if !podspec.nil? && options[:version] && options[:version] != podspec.version.to_s
|
253
|
+
raise OptionParser::InvalidArgument,
|
254
|
+
"Component version '#{options[:version]}' does not match podspec version '#{podspec.version}'"
|
255
|
+
end
|
256
|
+
options[:version] ||= podspec&.version&.to_s
|
257
|
+
end
|
258
|
+
|
259
|
+
def validate_type_option(options, podspec)
|
260
|
+
if !podspec.nil? && options[:type] && ptions[:type] != 'library'
|
261
|
+
raise OptionParser::InvalidArgument, "Component type must be 'library' when using a podspec"
|
262
|
+
end
|
263
|
+
|
264
|
+
return if podspec.nil?
|
265
|
+
|
266
|
+
options[:type] = 'cocoapods'
|
267
|
+
end
|
268
|
+
|
269
|
+
def validate_group_option(options, podspec)
|
270
|
+
return if podspec.nil?
|
271
|
+
raise OptionParser::InvalidArgument, 'Component group must not be specified when using a podspec' unless
|
272
|
+
options[:group].nil?
|
167
273
|
end
|
168
274
|
|
169
275
|
def setup_logger(verbose: true)
|
@@ -21,29 +21,89 @@
|
|
21
21
|
|
22
22
|
module CycloneDX
|
23
23
|
module CocoaPods
|
24
|
+
# Represents a software component in the CycloneDX BOM specification
|
25
|
+
#
|
26
|
+
# A component is a self-contained unit of software that can be used as a building block
|
27
|
+
# in the architecture of a software system. Components can be of different types like
|
28
|
+
# libraries, frameworks, or applications.
|
29
|
+
#
|
30
|
+
# @attr_reader [String, nil] group The group/organization identifier of the component
|
31
|
+
# @attr_reader [String] name The name of the component
|
32
|
+
# @attr_reader [String] version The version string of the component
|
33
|
+
# @attr_reader [String] type The type of component (must be one of VALID_COMPONENT_TYPES)
|
34
|
+
# @attr_reader [String] bomref The unique reference ID for this component in the BOM
|
35
|
+
# @attr_reader [String, nil] build_system The build system information
|
36
|
+
# @attr_reader [String, nil] vcs The version control system information
|
37
|
+
#
|
38
|
+
# @example Creating a new component
|
39
|
+
# component = Component.new(
|
40
|
+
# name: "AFNetworking",
|
41
|
+
# version: "4.0.1",
|
42
|
+
# type: "library"
|
43
|
+
# )
|
24
44
|
class Component
|
25
45
|
VALID_COMPONENT_TYPES = %w[application framework library container operating-system device firmware file].freeze
|
26
46
|
|
27
|
-
attr_reader :group, :name, :version, :type, :bomref
|
47
|
+
attr_reader :group, :name, :version, :type, :bomref, :build_system, :vcs
|
28
48
|
|
29
|
-
def initialize(name:, version:, type:, group: nil)
|
30
|
-
|
31
|
-
|
49
|
+
def initialize(name:, version:, type:, group: nil, build_system: nil, vcs: nil)
|
50
|
+
# cocoapods is a special case to correctly build a purl
|
51
|
+
package_type = type == 'cocoapods' ? 'cocoapods' : 'generic'
|
52
|
+
@type = type == 'cocoapods' ? 'library' : type
|
32
53
|
|
33
|
-
|
34
|
-
unless VALID_COMPONENT_TYPES.include?(type)
|
35
|
-
raise ArgumentError, "#{type} is not valid component type (#{VALID_COMPONENT_TYPES.join('|')})"
|
36
|
-
end
|
54
|
+
validate_attributes(name, version, @type, group)
|
37
55
|
|
38
56
|
@group = group
|
39
57
|
@name = name
|
40
58
|
@version = version
|
41
|
-
@
|
42
|
-
@
|
59
|
+
@build_system = build_system
|
60
|
+
@vcs = vcs
|
61
|
+
@bomref = build_purl(package_type, name, group, version)
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def build_purl(package_type, name, group, version)
|
67
|
+
if group.nil?
|
68
|
+
purl_name, subpath = parse_name(name)
|
69
|
+
else
|
70
|
+
purl_name = "#{CGI.escape(group)}/#{CGI.escape(name)}"
|
71
|
+
subpath = ''
|
72
|
+
end
|
73
|
+
"pkg:#{package_type}/#{purl_name}@#{CGI.escape(version.to_s)}#{subpath}"
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
43
77
|
|
44
|
-
|
78
|
+
def validate_attributes(name, version, type, group)
|
79
|
+
raise ArgumentError, 'Group, if specified, must be non-empty' if exists_and_blank(group)
|
80
|
+
raise ArgumentError, 'Name must be non-empty' if missing(name)
|
81
|
+
|
82
|
+
Gem::Version.new(version) # To check that the version string is well-formed
|
83
|
+
return if VALID_COMPONENT_TYPES.include?(type)
|
84
|
+
|
85
|
+
raise ArgumentError, "#{type} is not valid component type (#{VALID_COMPONENT_TYPES.join('|')})"
|
86
|
+
end
|
87
|
+
|
88
|
+
def parse_name(name)
|
89
|
+
purls = name.split('/')
|
90
|
+
purl_name = CGI.escape(purls[0])
|
91
|
+
subpath = if purls.length > 1
|
92
|
+
"##{name.split('/').drop(1).map do |component|
|
93
|
+
CGI.escape(component)
|
94
|
+
end.join('/')}"
|
95
|
+
else
|
96
|
+
''
|
97
|
+
end
|
98
|
+
[purl_name, subpath]
|
99
|
+
end
|
100
|
+
|
101
|
+
def missing(str)
|
102
|
+
str.nil? || str.to_s.strip.empty?
|
103
|
+
end
|
45
104
|
|
46
|
-
|
105
|
+
def exists_and_blank(str)
|
106
|
+
!str.nil? && str.to_s.strip.empty?
|
47
107
|
end
|
48
108
|
end
|
49
109
|
end
|
@@ -32,7 +32,7 @@ module CycloneDX
|
|
32
32
|
attr_accessor :text, :url
|
33
33
|
|
34
34
|
def initialize(identifier:)
|
35
|
-
raise ArgumentError, 'License identifier must be non
|
35
|
+
raise ArgumentError, 'License identifier must be non-empty' if identifier.nil? || identifier.to_s.strip.empty?
|
36
36
|
|
37
37
|
@identifier = SPDX_LICENSES.find { |license_id| license_id.downcase == identifier.to_s.downcase }
|
38
38
|
@identifier_type = @identifier.nil? ? :name : :id
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
# This file is part of CycloneDX CocoaPods
|
5
|
+
#
|
6
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
7
|
+
# you may not use this file except in compliance with the License.
|
8
|
+
# You may obtain a copy of the License at
|
9
|
+
#
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
11
|
+
#
|
12
|
+
# Unless required by applicable law or agreed to in writing, software
|
13
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
14
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
15
|
+
# See the License for the specific language governing permissions and
|
16
|
+
# limitations under the License.
|
17
|
+
#
|
18
|
+
# SPDX-License-Identifier: Apache-2.0
|
19
|
+
# Copyright (c) OWASP Foundation. All Rights Reserved.
|
20
|
+
#
|
21
|
+
|
22
|
+
module CycloneDX
|
23
|
+
module CocoaPods
|
24
|
+
# Represents manufacturer information in a CycloneDX BOM
|
25
|
+
#
|
26
|
+
# The Manufacturer class holds details about the manufacturer of a component,
|
27
|
+
# including company information and contact details.
|
28
|
+
#
|
29
|
+
# @attr_reader [String] name The name of the manufacturing organization
|
30
|
+
# @attr_reader [String] url The URL of the manufacturer's website
|
31
|
+
# @attr_reader [String] contact_name Name of the manufacturer contact person
|
32
|
+
# @attr_reader [String] email Email address of the manufacturer contact
|
33
|
+
# @attr_reader [String] phone Phone number of the manufacturer contact
|
34
|
+
#
|
35
|
+
# @example Creating a manufacturer with basic info
|
36
|
+
# manufacturer = Manufacturer.new(
|
37
|
+
# name: "ACME Corp",
|
38
|
+
# url: "https://acme.example"
|
39
|
+
# )
|
40
|
+
#
|
41
|
+
# @example Creating a manufacturer with full contact details
|
42
|
+
# manufacturer = Manufacturer.new(
|
43
|
+
# name: "ACME Corp",
|
44
|
+
# url: "https://acme.example",
|
45
|
+
# contact_name: "John Doe",
|
46
|
+
# email: "john@acme.example",
|
47
|
+
# phone: "+1-555-123-4567"
|
48
|
+
# )
|
49
|
+
class Manufacturer
|
50
|
+
attr_reader :name, :url, :contact_name, :email, :phone
|
51
|
+
|
52
|
+
def initialize(name: nil, url: nil, contact_name: nil, email: nil, phone: nil)
|
53
|
+
validate_parameters(name, url, contact_name, email, phone)
|
54
|
+
|
55
|
+
@name = name
|
56
|
+
@url = url
|
57
|
+
@contact_name = contact_name
|
58
|
+
@email = email
|
59
|
+
@phone = phone
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def validate_parameters(name, url, contact_name, email, phone)
|
65
|
+
raise ArgumentError, 'name, if specified, must be non-empty' if blank(name)
|
66
|
+
raise ArgumentError, 'URL, if specified, must be non-empty' if blank(url)
|
67
|
+
raise ArgumentError, 'Contact name, if specified, must be non-empty' if blank(contact_name)
|
68
|
+
raise ArgumentError, 'Email, if specified, must be non-empty' if blank(email)
|
69
|
+
raise ArgumentError, 'Phone, if specified, must be non-empty' if blank(phone)
|
70
|
+
end
|
71
|
+
|
72
|
+
def blank(str)
|
73
|
+
!str.nil? && str.to_s.strip.empty?
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -31,20 +31,20 @@ module CycloneDX
|
|
31
31
|
attr_reader :version
|
32
32
|
# Anything responding to :source_qualifier
|
33
33
|
attr_reader :source
|
34
|
-
# xs:anyURI - https://cyclonedx.org/docs/1.
|
34
|
+
# xs:anyURI - https://cyclonedx.org/docs/1.6/xml/#type_externalReference
|
35
35
|
attr_reader :homepage
|
36
|
-
# https://cyclonedx.org/docs/1.
|
36
|
+
# https://cyclonedx.org/docs/1.6/xml/#type_hashValue (We only use SHA-1 hashes - length == 40)
|
37
37
|
attr_reader :checksum
|
38
38
|
# xs:normalizedString
|
39
39
|
attr_reader :author
|
40
40
|
# xs:normalizedString
|
41
41
|
attr_reader :description
|
42
|
-
# https://cyclonedx.org/docs/1.
|
42
|
+
# https://cyclonedx.org/docs/1.6/xml/#type_licenseType
|
43
43
|
# We don't currently support several licenses or license expressions https://spdx.github.io/spdx-spec/appendix-IV-SPDX-license-expressions/
|
44
44
|
attr_reader :license
|
45
45
|
|
46
46
|
def initialize(name:, version:, source: nil, checksum: nil)
|
47
|
-
raise ArgumentError, 'Name must be non
|
47
|
+
raise ArgumentError, 'Name must be non-empty' if name.nil? || name.to_s.empty?
|
48
48
|
raise ArgumentError, "Name shouldn't contain spaces" if name.to_s.include?(' ')
|
49
49
|
raise ArgumentError, "Name shouldn't start with a dot" if name.to_s.start_with?('.')
|
50
50
|
|
@@ -0,0 +1,109 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
# This file is part of CycloneDX CocoaPods
|
5
|
+
#
|
6
|
+
# Licensed under the Apache License, Version 2.0 (the “License”);
|
7
|
+
# you may not use this file except in compliance with the License.
|
8
|
+
# You may obtain a copy of the License at
|
9
|
+
#
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
11
|
+
#
|
12
|
+
# Unless required by applicable law or agreed to in writing, software
|
13
|
+
# distributed under the License is distributed on an “AS IS” BASIS,
|
14
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
15
|
+
# See the License for the specific language governing permissions and
|
16
|
+
# limitations under the License.
|
17
|
+
#
|
18
|
+
# SPDX-License-Identifier: Apache-2.0
|
19
|
+
# Copyright (c) OWASP Foundation. All Rights Reserved.
|
20
|
+
#
|
21
|
+
|
22
|
+
require 'cocoapods'
|
23
|
+
require 'cocoapods-core'
|
24
|
+
require 'logger'
|
25
|
+
|
26
|
+
require_relative 'pod'
|
27
|
+
require_relative 'pod_attributes'
|
28
|
+
require_relative 'source'
|
29
|
+
|
30
|
+
module CycloneDX
|
31
|
+
module CocoaPods
|
32
|
+
class PodspecParsingError < StandardError; end
|
33
|
+
|
34
|
+
# Analyzes CocoaPods podspec files to extract component information for CycloneDX BOM generation
|
35
|
+
#
|
36
|
+
# The PodspecAnalyzer is responsible for:
|
37
|
+
# - Validating and loading podspec files from a given path
|
38
|
+
# - Parsing podspec contents to extract pod metadata
|
39
|
+
# - Converting podspec source information into standardized Source objects
|
40
|
+
#
|
41
|
+
# @example
|
42
|
+
# analyzer = PodspecAnalyzer.new(logger: Logger.new(STDOUT))
|
43
|
+
# podspec = analyzer.ensure_podspec_is_present(path: '/path/to/project')
|
44
|
+
# pod = analyzer.parse_podspec(podspec)
|
45
|
+
#
|
46
|
+
class PodspecAnalyzer
|
47
|
+
def initialize(logger:)
|
48
|
+
@logger = logger
|
49
|
+
end
|
50
|
+
|
51
|
+
def ensure_podspec_is_present(options)
|
52
|
+
project_dir = Pathname.new(options[:path] || Dir.pwd)
|
53
|
+
validate_options(project_dir, options)
|
54
|
+
initialize_cocoapods_config(project_dir)
|
55
|
+
|
56
|
+
options[:podspec_path].nil? ? nil : ::Pod::Specification.from_file(options[:podspec_path])
|
57
|
+
end
|
58
|
+
|
59
|
+
def parse_podspec(podspec)
|
60
|
+
return nil if podspec.nil?
|
61
|
+
|
62
|
+
@logger.debug "Parsing podspec from #{podspec.defined_in_file}"
|
63
|
+
|
64
|
+
Pod.new(
|
65
|
+
name: podspec.name,
|
66
|
+
version: podspec.version.to_s,
|
67
|
+
source: source_from_podspec(podspec),
|
68
|
+
checksum: nil
|
69
|
+
)
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
def validate_options(project_dir, options)
|
75
|
+
raise PodspecParsingError, "#{options[:path]} is not a valid directory." unless File.directory?(project_dir)
|
76
|
+
|
77
|
+
podspec_files = Dir.glob("#{project_dir}/*.podspec{.json,}")
|
78
|
+
options[:podspec_path] = podspec_files.first unless podspec_files.empty?
|
79
|
+
end
|
80
|
+
|
81
|
+
def initialize_cocoapods_config(project_dir)
|
82
|
+
::Pod::Config.instance = nil
|
83
|
+
::Pod::Config.instance.installation_root = project_dir
|
84
|
+
end
|
85
|
+
|
86
|
+
def source_from_podspec(podspec)
|
87
|
+
return unless podspec.source[:git]
|
88
|
+
|
89
|
+
Source::GitRepository.new(
|
90
|
+
url: podspec.source[:git],
|
91
|
+
type: determine_git_ref_type(podspec.source),
|
92
|
+
label: determine_git_ref_label(podspec.source)
|
93
|
+
)
|
94
|
+
end
|
95
|
+
|
96
|
+
def determine_git_ref_type(source)
|
97
|
+
return :tag if source[:tag]
|
98
|
+
return :commit if source[:commit]
|
99
|
+
return :branch if source[:branch]
|
100
|
+
|
101
|
+
nil
|
102
|
+
end
|
103
|
+
|
104
|
+
def determine_git_ref_label(source)
|
105
|
+
source[:tag] || source[:commit] || source[:branch]
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
metadata
CHANGED
@@ -1,15 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: cyclonedx-cocoapods
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.0.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- José González
|
8
8
|
- Kyle Hammond
|
9
|
-
autorequire:
|
10
9
|
bindir: exe
|
11
10
|
cert_chain: []
|
12
|
-
date:
|
11
|
+
date: 2025-02-09 00:00:00.000000000 Z
|
13
12
|
dependencies:
|
14
13
|
- !ruby/object:Gem::Dependency
|
15
14
|
name: cocoapods
|
@@ -71,9 +70,11 @@ files:
|
|
71
70
|
- lib/cyclonedx/cocoapods/cli_runner.rb
|
72
71
|
- lib/cyclonedx/cocoapods/component.rb
|
73
72
|
- lib/cyclonedx/cocoapods/license.rb
|
73
|
+
- lib/cyclonedx/cocoapods/manufacturer.rb
|
74
74
|
- lib/cyclonedx/cocoapods/pod.rb
|
75
75
|
- lib/cyclonedx/cocoapods/pod_attributes.rb
|
76
76
|
- lib/cyclonedx/cocoapods/podfile_analyzer.rb
|
77
|
+
- lib/cyclonedx/cocoapods/podspec_analyzer.rb
|
77
78
|
- lib/cyclonedx/cocoapods/source.rb
|
78
79
|
- lib/cyclonedx/cocoapods/spdx-licenses.json
|
79
80
|
- lib/cyclonedx/cocoapods/version.rb
|
@@ -83,7 +84,6 @@ licenses:
|
|
83
84
|
metadata:
|
84
85
|
homepage_uri: https://github.com/CycloneDX/cyclonedx-cocoapods
|
85
86
|
source_code_uri: https://github.com/CycloneDX/cyclonedx-cocoapods.git
|
86
|
-
post_install_message:
|
87
87
|
rdoc_options: []
|
88
88
|
require_paths:
|
89
89
|
- lib
|
@@ -98,8 +98,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
98
98
|
- !ruby/object:Gem::Version
|
99
99
|
version: '0'
|
100
100
|
requirements: []
|
101
|
-
rubygems_version: 3.
|
102
|
-
signing_key:
|
101
|
+
rubygems_version: 3.6.2
|
103
102
|
specification_version: 4
|
104
103
|
summary: CycloneDX software bill-of-material (SBOM) generation utility
|
105
104
|
test_files: []
|