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.
- checksums.yaml +7 -0
- data/.gitmodules +7 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/README.md +153 -0
- data/Rakefile +8 -0
- data/exe/sbom +346 -0
- data/lib/sbom/cyclonedx/generator.rb +307 -0
- data/lib/sbom/cyclonedx/parser.rb +275 -0
- data/lib/sbom/data/document.rb +143 -0
- data/lib/sbom/data/file.rb +169 -0
- data/lib/sbom/data/package.rb +417 -0
- data/lib/sbom/data/relationship.rb +43 -0
- data/lib/sbom/data/sbom.rb +124 -0
- data/lib/sbom/error.rb +13 -0
- data/lib/sbom/generator.rb +79 -0
- data/lib/sbom/license/data/spdx_licenses.json +8533 -0
- data/lib/sbom/license/scanner.rb +101 -0
- data/lib/sbom/output.rb +88 -0
- data/lib/sbom/parser.rb +111 -0
- data/lib/sbom/spdx/generator.rb +337 -0
- data/lib/sbom/spdx/parser.rb +426 -0
- data/lib/sbom/validation_result.rb +30 -0
- data/lib/sbom/validator.rb +261 -0
- data/lib/sbom/version.rb +5 -0
- data/lib/sbom.rb +54 -0
- data/sig/sbom.rbs +4 -0
- metadata +114 -0
|
@@ -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
|