cyclonedx-cocoapods 1.4.1 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +17 -0
- data/README.md +31 -13
- data/lib/cyclonedx/cocoapods/bom_builder.rb +252 -23
- 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: a7dce97ad0ab74a34fbe0e768172779ccbb53189ab786742275b1c5b5f00b7d2
|
4
|
+
data.tar.gz: bfb1e7e6a22778c85995bd7ee7c43cc6aa21896362d4b2cb4ad5fa4b137f4d61
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2b96c4c4d2225b5aa8f70b4fad6bab563ecfe863b3ec3a2018cfa9d0a5c12e99b8569ff43a52a8ce1b3ec31587af8c705b7a9c9fcba1f13598392a2be10c2dff
|
7
|
+
data.tar.gz: 0be60a77ebd3c442bd25481731eb52fdb53338e8b15d6996396d1b49f17ef778db0e686a9a14b12bd5e0c1902a68087b2926c19bd22852cf1fad16974270aec2
|
data/CHANGELOG.md
CHANGED
@@ -4,6 +4,23 @@ 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.0]
|
8
|
+
|
9
|
+
### Added
|
10
|
+
- 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).
|
11
|
+
- 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).
|
12
|
+
- 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).
|
13
|
+
|
14
|
+
### Changed
|
15
|
+
- Updated to use v1.6 of the CycloneDX specification. ([PR #81](https://github.com/CycloneDX/cyclonedx-cocoapods/pull/81)) [@jeremylong](https://github.com/jeremylong).
|
16
|
+
- Updated to use newer `tools` section elements. ([PR #80](https://github.com/CycloneDX/cyclonedx-cocoapods/pull/80)) [@jeremylong](https://github.com/jeremylong).
|
17
|
+
- 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).
|
18
|
+
- 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).
|
19
|
+
- 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).
|
20
|
+
|
21
|
+
### Fixed
|
22
|
+
- 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).
|
23
|
+
|
7
24
|
## [1.4.1]
|
8
25
|
|
9
26
|
### 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,214 @@ 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_s,
|
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
|
216
443
|
def bom_components(xml, pods, manifest_path, trim_strings_length)
|
217
444
|
xml.components do
|
218
445
|
pods.each do |pod|
|
@@ -238,15 +465,17 @@ module CycloneDX
|
|
238
465
|
xml.timestamp Time.now.getutc.strftime('%Y-%m-%dT%H:%M:%SZ')
|
239
466
|
bom_tools(xml)
|
240
467
|
component&.add_to_bom(xml)
|
468
|
+
manufacturer&.add_to_bom(xml)
|
241
469
|
end
|
242
470
|
end
|
243
|
-
|
244
471
|
def bom_tools(xml)
|
245
472
|
xml.tools do
|
246
|
-
xml.
|
247
|
-
xml.
|
248
|
-
|
249
|
-
|
473
|
+
xml.components do
|
474
|
+
xml.component(type: 'application') do
|
475
|
+
xml.group 'CycloneDX'
|
476
|
+
xml.name_ 'cyclonedx-cocoapods'
|
477
|
+
xml.version VERSION
|
478
|
+
end
|
250
479
|
end
|
251
480
|
end
|
252
481
|
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.0
|
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-01-07 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: []
|