cyclonedx-cocoapods 1.0.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.
@@ -0,0 +1,224 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ # This file is part of CycloneDX CocoaPods
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the “License”);
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an “AS IS” BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+ # SPDX-License-Identifier: Apache-2.0
18
+ # Copyright (c) OWASP Foundation. All Rights Reserved.
19
+ #
20
+
21
+ require 'optparse'
22
+ require 'logger'
23
+ require 'cocoapods'
24
+
25
+ require_relative 'component'
26
+ require_relative 'pod'
27
+ require_relative 'pod_attributes'
28
+ require_relative 'source'
29
+ require_relative 'bom_builder'
30
+
31
+ module CycloneDX
32
+ module CocoaPods
33
+ class PodfileParsingError < StandardError; end
34
+ class BOMOutputError < StandardError; end
35
+
36
+ class CLIRunner
37
+ def run
38
+ begin
39
+ setup_logger # Needed in case we have errors while processing CLI parameters
40
+ options = parseOptions
41
+ setup_logger(verbose: options[:verbose])
42
+ @logger.debug "Running cyclonedx-cocoapods with options: #{options}"
43
+
44
+ podfile, lockfile = ensure_podfile_and_lock_are_present(options)
45
+ pods = parse_pods(podfile, lockfile)
46
+
47
+ populate_pods_with_additional_info(pods)
48
+
49
+ bom = BOMBuilder.new(component: component_from_options(options), pods: pods).bom(version: options[:bom_version] || 1)
50
+ write_bom_to_file(bom: bom, options: options)
51
+ rescue StandardError => e
52
+ @logger.error ([e.message] + e.backtrace).join($/)
53
+ exit 1
54
+ end
55
+ end
56
+
57
+
58
+ private
59
+
60
+ def parseOptions
61
+ parsedOptions = {}
62
+ component_types = Component::VALID_COMPONENT_TYPES
63
+ OptionParser.new do |options|
64
+ options.banner = <<~BANNER
65
+ Usage: cyclonedx-cocoapods [options]
66
+ Generates a BOM with the given parameters. BOM component metadata is only generated if the component's name and version are provided using the --name and --version parameters.
67
+ BANNER
68
+
69
+ options.on('--[no-]verbose', 'Run verbosely') do |v|
70
+ parsedOptions[:verbose] = v
71
+ end
72
+ options.on('-p', '--path path', '(Optional) Path to CocoaPods project directory, current directory if missing') do |path|
73
+ parsedOptions[:path] = path
74
+ end
75
+ options.on('-o', '--output bom_file_path', '(Optional) Path to output the bom.xml file to') do |bom_file_path|
76
+ parsedOptions[:bom_file_path] = bom_file_path
77
+ end
78
+ options.on('-b', '--bom-version bom_version', Integer, '(Optional) Version of the generated BOM, 1 if not provided') do |version|
79
+ parsedOptions[:bom_version] = version
80
+ end
81
+ options.on('-g', '--group group', '(Optional) Group of the component for which the BOM is generated') do |group|
82
+ parsedOptions[:group] = group
83
+ end
84
+ options.on('-n', '--name name', '(Optional, if specified version and type are also required) Name of the component for which the BOM is generated') do |name|
85
+ parsedOptions[:name] = name
86
+ end
87
+ options.on('-v', '--version version', '(Optional) Version of the component for which the BOM is generated') do |version|
88
+ begin
89
+ Gem::Version.new(version)
90
+ parsedOptions[:version] = version
91
+ rescue StandardError => e
92
+ raise OptionParser::InvalidArgument, e.message
93
+ end
94
+ end
95
+ options.on('-t', '--type type', "(Optional) Type of the component for which the BOM is generated (one of #{component_types.join('|')})") do |type|
96
+ raise OptionParser::InvalidArgument, "Invalid value for component's type (#{type}). It must be one of #{component_types.join('|')}" unless component_types.include?(type)
97
+ parsedOptions[:type] = type
98
+ end
99
+ options.on_tail('-h', '--help', 'Show help message') do
100
+ puts options
101
+ exit
102
+ end
103
+ end.parse!
104
+
105
+ raise OptionParser::InvalidArgument, 'You must also specify --version and --type if --name is provided' if !parsedOptions[:name].nil? && (parsedOptions[:version].nil? || parsedOptions[:type].nil?)
106
+ return parsedOptions
107
+ end
108
+
109
+
110
+ def component_from_options(options)
111
+ Component.new(group: options[:group], name: options[:name], version: options[:version], type: options[:type]) if options[:name]
112
+ end
113
+
114
+
115
+ def setup_logger(verbose: true)
116
+ @logger ||= Logger.new($stdout)
117
+ @logger.level = verbose ? Logger::DEBUG : Logger::INFO
118
+ end
119
+
120
+
121
+ def ensure_podfile_and_lock_are_present(options)
122
+ project_dir = Pathname.new(options[:path] || Dir.pwd)
123
+ raise PodfileParsingError, "#{options[:path]} is not a valid directory." unless File.directory?(project_dir)
124
+ options[:podfile_path] = project_dir + 'Podfile'
125
+ 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])
126
+ options[:podfile_lock_path] = project_dir + 'Podfile.lock'
127
+ raise PodfileParsingError, "Missing Podfile.lock, please run 'pod install' before generating BOM" unless File.exist?(options[:podfile_lock_path])
128
+
129
+ initialize_cocoapods_config(project_dir)
130
+
131
+ lockfile = ::Pod::Lockfile.from_file(options[:podfile_lock_path])
132
+ verify_synced_sandbox(lockfile)
133
+
134
+ return ::Pod::Podfile.from_file(options[:podfile_path]), lockfile
135
+ end
136
+
137
+
138
+ def initialize_cocoapods_config(project_dir)
139
+ ::Pod::Config.instance.installation_root = project_dir
140
+ end
141
+
142
+
143
+ def verify_synced_sandbox(lockfile)
144
+ manifestFile = ::Pod::Config.instance.sandbox.manifest
145
+ raise PodfileParsingError, "Missing Manifest.lock, please run 'pod install' before generating BOM" if manifestFile.nil?
146
+ raise PodfileParsingError, "The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation." unless lockfile == manifestFile
147
+ end
148
+
149
+
150
+ def cocoapods_repository_source(podfile, lockfile, pod_name)
151
+ @source_manager ||= create_source_manager(podfile)
152
+ return Source::CocoaPodsRepository.searchable_source(url: lockfile.spec_repo(pod_name), source_manager: @source_manager)
153
+ end
154
+
155
+
156
+ def git_source(lockfile, pod_name)
157
+ checkout_options = lockfile.checkout_options_for_pod_named(pod_name)
158
+ url = checkout_options[:git]
159
+ [:tag, :branch, :commit].each do |type|
160
+ return Source::GitRepository.new(url: url, type: type, label: checkout_options[type]) if checkout_options[type]
161
+ end
162
+ return Source::GitRepository.new(url: url)
163
+ end
164
+
165
+
166
+ def source_for_pod(podfile, lockfile, pod_name)
167
+ root_name = pod_name.split('/').first
168
+ return cocoapods_repository_source(podfile, lockfile, root_name) unless lockfile.spec_repo(root_name).nil?
169
+ return git_source(lockfile, root_name) unless lockfile.checkout_options_for_pod_named(root_name).nil?
170
+ return Source::LocalPod.new(path: lockfile.to_hash['EXTERNAL SOURCES'][root_name][:path]) if lockfile.to_hash['EXTERNAL SOURCES'][root_name][:path]
171
+ return Source::Podspec.new(url: lockfile.to_hash['EXTERNAL SOURCES'][root_name][:podspec]) if lockfile.to_hash['EXTERNAL SOURCES'][root_name][:podspec]
172
+ return nil
173
+ end
174
+
175
+
176
+ def parse_pods(podfile, lockfile)
177
+ @logger.debug "Parsing pods from #{podfile.defined_in_file}"
178
+ return lockfile.pod_names.map do |name|
179
+ Pod.new(name: name, version: lockfile.version(name), source: source_for_pod(podfile, lockfile, name), checksum: lockfile.checksum(name))
180
+ end
181
+ end
182
+
183
+
184
+ def create_source_manager(podfile)
185
+ sourceManager = ::Pod::Source::Manager.new(::Pod::Config::instance.repos_dir)
186
+ @logger.debug "Parsing sources from #{podfile.defined_in_file}"
187
+ podfile.sources.each do |source|
188
+ @logger.debug "Ensuring #{source} is available for searches"
189
+ sourceManager.find_or_create_source_with_url(source)
190
+ end
191
+ @logger.debug "Source manager successfully created with all needed sources"
192
+ return sourceManager
193
+ end
194
+
195
+
196
+ def populate_pods_with_additional_info(pods)
197
+ pods.each do |pod|
198
+ @logger.debug "Completing information for #{pod.name}"
199
+ pod.complete_information_from_source
200
+ end
201
+ return pods
202
+ end
203
+
204
+
205
+ def write_bom_to_file(bom:, options:)
206
+ bom_file_path = Pathname.new(options[:bom_file_path] || './bom.xml').expand_path
207
+ bom_dir = bom_file_path.dirname
208
+
209
+ begin
210
+ FileUtils.mkdir_p(bom_dir) unless bom_dir.directory?
211
+ rescue
212
+ raise BOMOutputError, "Unable to create the BOM output directory at #{bom_dir}"
213
+ end
214
+
215
+ begin
216
+ File.open(bom_file_path, 'w') { |file| file.write(bom) }
217
+ @logger.info "BOM written to #{bom_file_path}"
218
+ rescue
219
+ raise BOMOutputError, "Unable to write the BOM to #{bom_file_path}"
220
+ end
221
+ end
222
+ end
223
+ end
224
+ end
@@ -0,0 +1,37 @@
1
+ #
2
+ # This file is part of CycloneDX CocoaPods
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the “License”);
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an “AS IS” BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+ # SPDX-License-Identifier: Apache-2.0
17
+ # Copyright (c) OWASP Foundation. All Rights Reserved.
18
+ #
19
+
20
+ module CycloneDX
21
+ module CocoaPods
22
+ class Component
23
+ VALID_COMPONENT_TYPES = %w[application framework library container operating-system device firmware file].freeze
24
+
25
+ attr_reader :group, :name, :version, :type
26
+
27
+ def initialize(group: nil, name:, version:, type:)
28
+ raise ArgumentError, "Group, if specified, must be non empty" if !group.nil? && group.to_s.strip.empty?
29
+ raise ArgumentError, "Name must be non empty" if name.nil? || name.to_s.strip.empty?
30
+ Gem::Version.new(version) # To check that the version string is well formed
31
+ raise ArgumentError, "#{type} is not valid component type (#{VALID_COMPONENT_TYPES.join('|')})" unless VALID_COMPONENT_TYPES.include?(type)
32
+
33
+ @group, @name, @version, @type = group, name, version, type
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,44 @@
1
+ #
2
+ # This file is part of CycloneDX CocoaPods
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the “License”);
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an “AS IS” BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+ # SPDX-License-Identifier: Apache-2.0
17
+ # Copyright (c) OWASP Foundation. All Rights Reserved.
18
+ #
19
+
20
+ require 'json'
21
+
22
+ module CycloneDX
23
+ module CocoaPods
24
+ class Pod
25
+ class License
26
+ SPDX_LICENSES = JSON.parse(File.read("#{__dir__}/spdx-licenses.json")).freeze
27
+ IDENTIFIER_TYPES = [:id, :name].freeze
28
+
29
+ attr_reader :identifier
30
+ attr_reader :identifier_type
31
+ attr_accessor :text
32
+ attr_accessor :url
33
+
34
+ def initialize(identifier:)
35
+ raise ArgumentError, "License identifier must be non empty" if identifier.nil? || identifier.to_s.strip.empty?
36
+
37
+ @identifier = SPDX_LICENSES.find { |license_id| license_id.downcase == identifier.to_s.downcase }
38
+ @identifier_type = @identifier.nil? ? :name : :id
39
+ @identifier ||= identifier
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,108 @@
1
+ #
2
+ # This file is part of CycloneDX CocoaPods
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the “License”);
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an “AS IS” BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+ # SPDX-License-Identifier: Apache-2.0
17
+ # Copyright (c) OWASP Foundation. All Rights Reserved.
18
+ #
19
+
20
+ require 'rubygems/version'
21
+ require_relative 'license'
22
+
23
+ module CycloneDX
24
+ module CocoaPods
25
+ class Pod
26
+ attr_reader :name # xs:normalizedString
27
+ attr_reader :version # xs:normalizedString
28
+ attr_reader :source # Anything responding to :source_qualifier
29
+ attr_reader :homepage # xs:anyURI - https://cyclonedx.org/docs/1.4/#type_externalReference
30
+ attr_reader :checksum # https://cyclonedx.org/docs/1.4/#type_hashValue (We only use SHA-1 hashes - length == 40)
31
+ attr_reader :author # xs:normalizedString
32
+ attr_reader :description # xs:normalizedString
33
+ attr_reader :license # https://cyclonedx.org/docs/1.4/#type_licenseType
34
+ # We don't currently support several licenses or license expressions https://spdx.github.io/spdx-spec/appendix-IV-SPDX-license-expressions/
35
+ def initialize(name:, version:, source: nil, checksum: nil)
36
+ raise ArgumentError, "Name must be non empty" if name.nil? || name.to_s.empty?
37
+ raise ArgumentError, "Name shouldn't contain spaces" if name.to_s.include?(' ')
38
+ raise ArgumentError, "Name shouldn't start with a dot" if name.to_s.start_with?('.')
39
+ # `pod create` also enforces no plus sign, but more than 500 public pods have a plus in the root name.
40
+ # https://github.com/CocoaPods/CocoaPods/blob/9461b346aeb8cba6df71fd4e71661688138ec21b/lib/cocoapods/command/lib/create.rb#L35
41
+
42
+ Gem::Version.new(version) # To check that the version string is well formed
43
+ raise ArgumentError, "Invalid pod source" unless source.nil? || source.respond_to?(:source_qualifier)
44
+ raise ArgumentError, "#{checksum} is not valid SHA-1 hash" unless checksum.nil? || checksum =~ /[a-fA-F0-9]{40}/
45
+ @name, @version, @source, @checksum = name.to_s, version, source, checksum
46
+ end
47
+
48
+ def root_name
49
+ @name.split('/').first
50
+ end
51
+
52
+ def populate(attributes)
53
+ attributes.transform_keys!(&:to_sym)
54
+ populate_author(attributes)
55
+ populate_description(attributes)
56
+ populate_license(attributes)
57
+ populate_homepage(attributes)
58
+ self
59
+ end
60
+
61
+ def to_s
62
+ "Pod<#{name}, #{version.to_s}>"
63
+ end
64
+
65
+ private
66
+
67
+ def populate_author(attributes)
68
+ authors = attributes[:author] || attributes[:authors]
69
+ case authors
70
+ when String
71
+ @author = authors
72
+ when Array
73
+ @author = authors.join(', ')
74
+ when Hash
75
+ @author = authors.map { |name, email| "#{name} <#{email}>" }.join(', ')
76
+ else
77
+ @author = nil
78
+ end
79
+ end
80
+
81
+ def populate_description(attributes)
82
+ @description = attributes[:description] || attributes[:summary]
83
+ end
84
+
85
+ def populate_license(attributes)
86
+ case attributes[:license]
87
+ when String
88
+ @license = License.new(identifier: attributes[:license])
89
+ when Hash
90
+ attributes[:license].transform_keys!(&:to_sym)
91
+ identifier = attributes[:license][:type]
92
+ unless identifier.nil? || identifier.to_s.strip.empty?
93
+ @license = License.new(identifier: identifier)
94
+ @license.text = attributes[:license][:text]
95
+ else
96
+ @license = nil
97
+ end
98
+ else
99
+ @license = nil
100
+ end
101
+ end
102
+
103
+ def populate_homepage(attributes)
104
+ @homepage = attributes[:homepage]
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,72 @@
1
+ #
2
+ # This file is part of CycloneDX CocoaPods
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the “License”);
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an “AS IS” BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+ # SPDX-License-Identifier: Apache-2.0
17
+ # Copyright (c) OWASP Foundation. All Rights Reserved.
18
+ #
19
+
20
+ module CycloneDX
21
+ module CocoaPods
22
+ class SearchError < StandardError; end
23
+
24
+ module Source
25
+ class CocoaPodsRepository
26
+ attr_accessor :source_manager
27
+
28
+ def self.searchable_source(url:, source_manager:)
29
+ source = CocoaPodsRepository.new(url: url)
30
+ source.source_manager = source_manager
31
+ return source
32
+ end
33
+
34
+ def attributes_for(pod:)
35
+ specification_sets = @source_manager.search_by_name("^#{Regexp.escape(pod.root_name)}$")
36
+ raise SearchError, "No pod found named #{pod.name}" if specification_sets.length == 0
37
+ raise SearchError, "More than one pod found named #{pod.name}" if specification_sets.length > 1
38
+
39
+ paths = specification_sets[0].specification_paths_for_version(pod.version)
40
+ raise SearchError, "Version #{pod.version} not found for pod #{pod.name}" if paths.length == 0
41
+
42
+ ::Pod::Specification.from_file(paths[0]).attributes_hash
43
+ end
44
+ end
45
+
46
+ class GitRepository
47
+ def attributes_for(pod:)
48
+ ::Pod::Config.instance.sandbox.specification(pod.name).attributes_hash
49
+ end
50
+ end
51
+
52
+ class LocalPod
53
+ def attributes_for(pod:)
54
+ ::Pod::Config.instance.sandbox.specification(pod.name).attributes_hash
55
+ end
56
+ end
57
+
58
+ class Podspec
59
+ def attributes_for(pod:)
60
+ ::Pod::Config.instance.sandbox.specification(pod.name).attributes_hash
61
+ end
62
+ end
63
+ end
64
+
65
+
66
+ class Pod
67
+ def complete_information_from_source
68
+ populate(source.attributes_for(pod: self))
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,59 @@
1
+ #
2
+ # This file is part of CycloneDX CocoaPods
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the “License”);
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an “AS IS” BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+ # SPDX-License-Identifier: Apache-2.0
17
+ # Copyright (c) OWASP Foundation. All Rights Reserved.
18
+ #
19
+
20
+ module CycloneDX
21
+ module CocoaPods
22
+ module Source
23
+ class CocoaPodsRepository
24
+ attr_reader :url
25
+
26
+ def initialize(url:)
27
+ @url = url
28
+ end
29
+ end
30
+
31
+ class GitRepository
32
+ VALID_TYPES = [:branch, :tag, :commit].freeze
33
+
34
+ attr_reader :url, :type, :label
35
+
36
+ def initialize(url:, type: nil, label: nil)
37
+ raise ArgumentError, "Invalid checkout information" if !type.nil? && !VALID_TYPES.include?(type)
38
+ @url, @type, @label = url, type, label
39
+ end
40
+ end
41
+
42
+ class LocalPod
43
+ attr_reader :path
44
+
45
+ def initialize(path:)
46
+ @path = path
47
+ end
48
+ end
49
+
50
+ class Podspec
51
+ attr_reader :url
52
+
53
+ def initialize(url:)
54
+ @url = url
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end