sbom 0.1.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,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sbom
4
+ module Data
5
+ class Document
6
+ DEFAULTS = {
7
+ name: "NOT DEFINED",
8
+ id: "NOT_DEFINED"
9
+ }.freeze
10
+
11
+ def initialize
12
+ @data = {}
13
+ end
14
+
15
+ def reset!
16
+ @data = {}
17
+ end
18
+
19
+ def name
20
+ @data[:name] || DEFAULTS[:name]
21
+ end
22
+
23
+ def name=(value)
24
+ @data[:name] = value
25
+ end
26
+
27
+ def id
28
+ @data[:id] || DEFAULTS[:id]
29
+ end
30
+
31
+ def id=(value)
32
+ @data[:id] = value
33
+ end
34
+
35
+ def version
36
+ @data[:version]
37
+ end
38
+
39
+ def version=(value)
40
+ @data[:version] = value
41
+ end
42
+
43
+ def sbom_type
44
+ @data[:type]
45
+ end
46
+
47
+ def sbom_type=(value)
48
+ @data[:type] = value&.downcase
49
+ end
50
+
51
+ def data_license
52
+ @data[:data_license]
53
+ end
54
+
55
+ def data_license=(value)
56
+ @data[:data_license] = value
57
+ end
58
+
59
+ def license_list_version
60
+ @data[:license_list_version]
61
+ end
62
+
63
+ def license_list_version=(value)
64
+ @data[:license_list_version] = value
65
+ end
66
+
67
+ def created
68
+ @data[:created]
69
+ end
70
+
71
+ def created=(value)
72
+ @data[:created] = value
73
+ end
74
+
75
+ def namespace
76
+ @data[:namespace]
77
+ end
78
+
79
+ def namespace=(value)
80
+ @data[:namespace] = value
81
+ end
82
+
83
+ def add_creator(creator_type, creator_name)
84
+ @data[:creators] ||= []
85
+ @data[:creators] << [creator_type, creator_name]
86
+ end
87
+
88
+ def creators
89
+ @data[:creators] || []
90
+ end
91
+
92
+ def metadata_type
93
+ @data[:metadata_type]
94
+ end
95
+
96
+ def metadata_type=(value)
97
+ @data[:metadata_type] = value
98
+ end
99
+
100
+ def metadata_supplier
101
+ @data[:metadata_supplier]
102
+ end
103
+
104
+ def metadata_supplier=(value)
105
+ @data[:metadata_supplier] = value
106
+ end
107
+
108
+ def metadata_version
109
+ @data[:metadata_version]
110
+ end
111
+
112
+ def metadata_version=(value)
113
+ @data[:metadata_version] = value
114
+ end
115
+
116
+ def lifecycle
117
+ @data[:lifecycle]
118
+ end
119
+
120
+ def lifecycle=(value)
121
+ @data[:lifecycle] = value
122
+ end
123
+
124
+ def [](key)
125
+ @data[key.to_sym]
126
+ end
127
+
128
+ def []=(key, value)
129
+ @data[key.to_sym] = value
130
+ end
131
+
132
+ def to_h
133
+ @data.dup
134
+ end
135
+
136
+ def copy_from(document_hash)
137
+ document_hash.each do |key, value|
138
+ @data[key.to_sym] = value
139
+ end
140
+ end
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,169 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sbom
4
+ module Data
5
+ class SbomFile
6
+ VALID_FILE_TYPES = %w[
7
+ SOURCE BINARY ARCHIVE APPLICATION AUDIO IMAGE
8
+ TEXT VIDEO DOCUMENTATION SPDX OTHER
9
+ ].freeze
10
+
11
+ VALID_ALGORITHMS = %w[
12
+ MD5 SHA1 SHA256 SHA384 SHA512
13
+ SHA3-256 SHA3-384 SHA3-512
14
+ BLAKE2b-256 BLAKE2b-384 BLAKE2b-512 BLAKE3
15
+ ].freeze
16
+
17
+ DEFAULTS = {
18
+ name: "TBD",
19
+ id: "NOT_DEFINED"
20
+ }.freeze
21
+
22
+ def initialize
23
+ @data = {}
24
+ end
25
+
26
+ def reset!
27
+ @data = {}
28
+ end
29
+
30
+ def name
31
+ @data[:name] || DEFAULTS[:name]
32
+ end
33
+
34
+ def name=(value)
35
+ @data[:name] = value
36
+ end
37
+
38
+ def id
39
+ @data[:id] || DEFAULTS[:id]
40
+ end
41
+
42
+ def id=(value)
43
+ @data[:id] = value
44
+ end
45
+
46
+ def add_file_type(file_type)
47
+ type = file_type.to_s.upcase.strip
48
+ return unless VALID_FILE_TYPES.include?(type)
49
+
50
+ @data[:file_types] ||= []
51
+ @data[:file_types] << type unless @data[:file_types].include?(type)
52
+ end
53
+
54
+ def file_types
55
+ @data[:file_types] || []
56
+ end
57
+
58
+ def add_checksum(algorithm, value)
59
+ return unless valid_checksum?(value) && valid_algorithm?(algorithm)
60
+
61
+ @data[:checksums] ||= []
62
+ @data[:checksums] << [algorithm.strip, value.downcase]
63
+ end
64
+
65
+ def checksums
66
+ @data[:checksums] || []
67
+ end
68
+
69
+ def license_concluded
70
+ @data[:license_concluded]
71
+ end
72
+
73
+ def license_concluded=(value)
74
+ @data[:license_concluded] = value
75
+ end
76
+
77
+ def add_license_info_in_file(license)
78
+ @data[:license_info_in_file] ||= []
79
+ @data[:license_info_in_file] << license
80
+ end
81
+
82
+ def license_info_in_file
83
+ @data[:license_info_in_file] || []
84
+ end
85
+
86
+ def license_comment
87
+ @data[:license_comment]
88
+ end
89
+
90
+ def license_comment=(value)
91
+ @data[:license_comment] = clean_text(value)
92
+ end
93
+
94
+ def copyright_text
95
+ @data[:copyright_text]
96
+ end
97
+
98
+ def copyright_text=(value)
99
+ @data[:copyright_text] = clean_text(value)
100
+ end
101
+
102
+ def comment
103
+ @data[:comment]
104
+ end
105
+
106
+ def comment=(value)
107
+ @data[:comment] = clean_text(value)
108
+ end
109
+
110
+ def notice
111
+ @data[:notice]
112
+ end
113
+
114
+ def notice=(value)
115
+ @data[:notice] = clean_text(value)
116
+ end
117
+
118
+ def add_contributor(contributor)
119
+ @data[:contributors] ||= []
120
+ @data[:contributors] << contributor
121
+ end
122
+
123
+ def contributors
124
+ @data[:contributors] || []
125
+ end
126
+
127
+ def attribution
128
+ @data[:attribution]
129
+ end
130
+
131
+ def attribution=(value)
132
+ @data[:attribution] = value
133
+ end
134
+
135
+ def [](key)
136
+ @data[key.to_sym]
137
+ end
138
+
139
+ def []=(key, value)
140
+ @data[key.to_sym] = value
141
+ end
142
+
143
+ def to_h
144
+ @data.dup
145
+ end
146
+
147
+ private
148
+
149
+ def valid_checksum?(value)
150
+ return false unless value.is_a?(String)
151
+
152
+ length = value.length
153
+ return false unless [32, 40, 64, 96, 128].include?(length)
154
+
155
+ value.match?(/\A[0-9a-fA-F]+\z/)
156
+ end
157
+
158
+ def valid_algorithm?(algorithm)
159
+ VALID_ALGORITHMS.include?(algorithm.strip)
160
+ end
161
+
162
+ def clean_text(text)
163
+ return nil if text.nil? || text.empty?
164
+
165
+ text.gsub(/<\/?text>/, "").strip
166
+ end
167
+ end
168
+ end
169
+ end
@@ -0,0 +1,417 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sbom
4
+ module Data
5
+ class Package
6
+ VALID_TYPES = %w[
7
+ APPLICATION FRAMEWORK LIBRARY CONTAINER OPERATING-SYSTEM
8
+ DEVICE FIRMWARE FILE MACHINE-LEARNING-MODEL DATA
9
+ DEVICE-DRIVER PLATFORM CRYPTOGRAPHIC-ASSET
10
+ ].freeze
11
+
12
+ VALID_SUPPLIER_TYPES = %w[Person Organization].freeze
13
+
14
+ VALID_ALGORITHMS = %w[
15
+ MD5 SHA1 SHA256 SHA384 SHA512
16
+ SHA3-256 SHA3-384 SHA3-512
17
+ BLAKE2b-256 BLAKE2b-384 BLAKE2b-512 BLAKE3
18
+ ].freeze
19
+
20
+ VALID_EXTERNAL_REF_CATEGORIES = %w[
21
+ vcs issue-tracker website advisories bom mailing-list
22
+ social chat documentation support source-distribution
23
+ distribution distribution-intake license build-meta
24
+ build-system release-notes security-contact model-card
25
+ log configuration evidence formulation attestation
26
+ threat-model adversary-model risk-assessment
27
+ vulnerability-assertion exploitability-statement
28
+ pentest-report static-analysis-report dynamic-analysis-report
29
+ runtime-analysis-report component-analysis-report
30
+ maturity-report certification-report codified-infrastructure
31
+ quality-metrics poam electronic-signature digital-signature
32
+ rfc-9116 other
33
+ ].freeze
34
+
35
+ URL_PATTERN = %r{
36
+ \A(https?|ssh|git|svn|sftp|ftp)://
37
+ [a-z0-9]+([\-\.]{1}[a-z0-9]+){0,100}\.[a-z]{2,5}
38
+ (:[0-9]{1,5})?(/.*)?
39
+ \z}xi
40
+
41
+ def initialize
42
+ @data = {}
43
+ end
44
+
45
+ def reset!
46
+ @data = {}
47
+ end
48
+
49
+ def name
50
+ @data[:name]
51
+ end
52
+
53
+ def name=(value)
54
+ @data[:name] = value
55
+ end
56
+
57
+ def id
58
+ @data[:id]
59
+ end
60
+
61
+ def id=(value)
62
+ @data[:id] = value
63
+ end
64
+
65
+ def version
66
+ @data[:version]
67
+ end
68
+
69
+ def version=(value)
70
+ @data[:version] = value
71
+ @data[:id] ||= "#{name}_#{value}" if name
72
+ end
73
+
74
+ def package_type
75
+ @data[:type]
76
+ end
77
+
78
+ def package_type=(value)
79
+ normalized = value.to_s.upcase.tr("_", "-").strip
80
+ @data[:type] = VALID_TYPES.include?(normalized) ? normalized : "FILE"
81
+ end
82
+
83
+ def supplier
84
+ @data[:supplier]
85
+ end
86
+
87
+ def supplier_type
88
+ @data[:supplier_type]
89
+ end
90
+
91
+ def set_supplier(type, name)
92
+ return if name.nil? || name.empty?
93
+
94
+ @data[:supplier_type] = normalize_supplier_type(type)
95
+ @data[:supplier] = name
96
+ end
97
+
98
+ def originator
99
+ @data[:originator]
100
+ end
101
+
102
+ def originator_type
103
+ @data[:originator_type]
104
+ end
105
+
106
+ def set_originator(type, name)
107
+ return if name.nil? || name.empty?
108
+
109
+ @data[:originator_type] = normalize_supplier_type(type)
110
+ @data[:originator] = name
111
+ end
112
+
113
+ def download_location
114
+ @data[:download_location]
115
+ end
116
+
117
+ def download_location=(value)
118
+ @data[:download_location] = value if valid_url?(value)
119
+ end
120
+
121
+ def filename
122
+ @data[:filename]
123
+ end
124
+
125
+ def filename=(value)
126
+ @data[:filename] = value
127
+ end
128
+
129
+ def homepage
130
+ @data[:homepage]
131
+ end
132
+
133
+ def homepage=(value)
134
+ @data[:homepage] = value if valid_url?(value)
135
+ end
136
+
137
+ def source_info
138
+ @data[:source_info]
139
+ end
140
+
141
+ def source_info=(value)
142
+ @data[:source_info] = clean_text(value) unless value.nil? || value.empty?
143
+ end
144
+
145
+ def files_analyzed
146
+ @data[:files_analyzed]
147
+ end
148
+
149
+ def files_analyzed=(value)
150
+ @data[:files_analyzed] = value
151
+ end
152
+
153
+ def add_checksum(algorithm, value)
154
+ return unless valid_checksum?(value) && valid_algorithm?(algorithm)
155
+
156
+ @data[:checksums] ||= []
157
+ @data[:checksums] << [algorithm.strip, value.downcase]
158
+ end
159
+
160
+ def checksums
161
+ @data[:checksums] || []
162
+ end
163
+
164
+ def add_property(name, value)
165
+ return if value.nil?
166
+
167
+ @data[:properties] ||= []
168
+ @data[:properties] << [name.strip, value]
169
+ end
170
+
171
+ def properties
172
+ @data[:properties] || []
173
+ end
174
+
175
+ def add_tag(name)
176
+ return if name.nil?
177
+
178
+ @data[:tags] ||= []
179
+ @data[:tags] << name.strip
180
+ end
181
+
182
+ def tags
183
+ @data[:tags] || []
184
+ end
185
+
186
+ def license_concluded
187
+ @data[:license_concluded]
188
+ end
189
+
190
+ def license_concluded=(value)
191
+ @data[:license_concluded] = value
192
+ end
193
+
194
+ def license_declared
195
+ @data[:license_declared]
196
+ end
197
+
198
+ def set_license_declared(license, name = nil)
199
+ @data[:license_declared] = license
200
+ @data[:license_name] = name if name
201
+ end
202
+
203
+ def license_name
204
+ @data[:license_name]
205
+ end
206
+
207
+ def license_list
208
+ @data[:license_list]
209
+ end
210
+
211
+ def license_list=(value)
212
+ @data[:license_list] = value
213
+ end
214
+
215
+ def license_comments
216
+ @data[:license_comments]
217
+ end
218
+
219
+ def license_comments=(value)
220
+ @data[:license_comments] = clean_text(value) unless value.nil? || value.empty?
221
+ end
222
+
223
+ def add_license_info_in_files(license_info)
224
+ @data[:license_info_in_files] ||= []
225
+ @data[:license_info_in_files] << license_info
226
+ end
227
+
228
+ def license_info_in_files
229
+ @data[:license_info_in_files] || []
230
+ end
231
+
232
+ def copyright_text
233
+ @data[:copyright_text]
234
+ end
235
+
236
+ def copyright_text=(value)
237
+ @data[:copyright_text] = clean_text(value) unless value.nil? || value.empty?
238
+ end
239
+
240
+ def comment
241
+ @data[:comment]
242
+ end
243
+
244
+ def comment=(value)
245
+ @data[:comment] = clean_text(value) unless value.nil? || value.empty?
246
+ end
247
+
248
+ def summary
249
+ @data[:summary]
250
+ end
251
+
252
+ def summary=(value)
253
+ @data[:summary] = clean_text(value) unless value.nil? || value.empty?
254
+ end
255
+
256
+ def description
257
+ @data[:description]
258
+ end
259
+
260
+ def description=(value)
261
+ @data[:description] = clean_text(value) unless value.nil? || value.empty?
262
+ end
263
+
264
+ def add_attribution(value)
265
+ @data[:attributions] ||= []
266
+ @data[:attributions] << value
267
+ end
268
+
269
+ def attributions
270
+ @data[:attributions] || []
271
+ end
272
+
273
+ def add_external_reference(category, ref_type, locator)
274
+ if %w[SECURITY PACKAGE-MANAGER PACKAGE_MANAGER].include?(category) &&
275
+ %w[cpe22Type cpe23Type purl].include?(ref_type)
276
+ entry = [category, ref_type.strip, locator]
277
+ else
278
+ normalized_type = VALID_EXTERNAL_REF_CATEGORIES.include?(ref_type.downcase) ? ref_type.downcase : "other"
279
+ entry = [category, normalized_type.strip, locator]
280
+ end
281
+
282
+ @data[:external_references] ||= []
283
+ @data[:external_references] << entry
284
+ end
285
+
286
+ def external_references
287
+ @data[:external_references] || []
288
+ end
289
+
290
+ def purl
291
+ external_references.find { |_, type, _| type == "purl" }&.last
292
+ end
293
+
294
+ def purl=(value)
295
+ return if value.nil? || value.to_s.empty?
296
+
297
+ add_external_reference("PACKAGE_MANAGER", "purl", value.to_s)
298
+ end
299
+
300
+ def parsed_purl
301
+ return nil unless purl
302
+
303
+ Purl.parse(purl)
304
+ rescue Purl::InvalidPackageURL
305
+ nil
306
+ end
307
+
308
+ def purl_type
309
+ parsed_purl&.type
310
+ end
311
+
312
+ def purl_namespace
313
+ parsed_purl&.namespace
314
+ end
315
+
316
+ def purl_name
317
+ parsed_purl&.name
318
+ end
319
+
320
+ def purl_version
321
+ parsed_purl&.version
322
+ end
323
+
324
+ def generate_purl(type:, namespace: nil, qualifiers: nil, subpath: nil)
325
+ purl_obj = Purl::PackageURL.new(
326
+ type: type,
327
+ namespace: namespace,
328
+ name: name,
329
+ version: version,
330
+ qualifiers: qualifiers,
331
+ subpath: subpath
332
+ )
333
+ self.purl = purl_obj.to_s
334
+ purl_obj.to_s
335
+ end
336
+
337
+ def cpe
338
+ external_references.find { |_, type, _| type.start_with?("cpe") }&.last
339
+ end
340
+
341
+ def set_cpe(vector, cpe_type = "cpe23Type")
342
+ return unless %w[cpe22Type cpe23Type].include?(cpe_type)
343
+
344
+ add_external_reference("SECURITY", cpe_type, vector)
345
+ end
346
+
347
+ def add_evidence(evidence)
348
+ @data[:evidence] ||= []
349
+ @data[:evidence] << evidence
350
+ end
351
+
352
+ def evidence
353
+ @data[:evidence] || []
354
+ end
355
+
356
+ def [](key)
357
+ @data[key.to_sym]
358
+ end
359
+
360
+ def []=(key, value)
361
+ @data[key.to_sym] = value
362
+ end
363
+
364
+ def to_h
365
+ @data.dup
366
+ end
367
+
368
+ def copy_from(package_hash)
369
+ package_hash.each do |key, value|
370
+ @data[key.to_sym] = value
371
+ end
372
+ end
373
+
374
+ private
375
+
376
+ def normalize_supplier_type(type)
377
+ normalized = type.to_s.downcase.strip
378
+ case normalized
379
+ when "person", "author"
380
+ "Person"
381
+ when "unknown"
382
+ "UNKNOWN"
383
+ else
384
+ "Organization"
385
+ end
386
+ end
387
+
388
+ def valid_url?(url)
389
+ return false if url.nil? || url.include?(" ")
390
+
391
+ url.match?(URL_PATTERN)
392
+ end
393
+
394
+ def valid_checksum?(value)
395
+ return false unless value.is_a?(String)
396
+
397
+ length = value.length
398
+ return false unless [32, 40, 64, 96, 128].include?(length)
399
+
400
+ value.match?(/\A[0-9a-fA-F]+\z/)
401
+ end
402
+
403
+ def valid_algorithm?(algorithm)
404
+ VALID_ALGORITHMS.include?(algorithm.strip)
405
+ end
406
+
407
+ def clean_text(text)
408
+ return nil if text.nil?
409
+
410
+ text.to_s
411
+ .encode("UTF-8", invalid: :replace, undef: :replace, replace: "")
412
+ .gsub(/<\/?text>/, "")
413
+ .strip
414
+ end
415
+ end
416
+ end
417
+ end