File Coverage

blib/lib/SBOM/CycloneDX.pm
Criterion Covered Total %
statement 115 146 78.7
branch 23 46 50.0
condition 0 6 0.0
subroutine 25 29 86.2
pod 7 7 100.0
total 170 234 72.6


line stmt bran cond sub pod time code
1             package SBOM::CycloneDX;
2              
3 16     16   455520 use 5.010001;
  16         60  
4 16     16   100 use strict;
  16         30  
  16         458  
5 16     16   66 use warnings;
  16         31  
  16         867  
6 16     16   1169 use utf8;
  16         610  
  16         153  
7              
8 16     16   10525 use Types::Standard qw(Str StrMatch Int Num InstanceOf HashRef);
  16         1912222  
  16         199  
9 16     16   55970 use Types::TypeTiny qw(ArrayLike);
  16         33  
  16         126  
10              
11 16     16   46130 use SBOM::CycloneDX::Declarations;
  16         78  
  16         811  
12 16     16   9049 use SBOM::CycloneDX::Definitions;
  16         72  
  16         733  
13 16     16   8550 use SBOM::CycloneDX::Dependency;
  16         71  
  16         688  
14 16     16   127 use SBOM::CycloneDX::List;
  16         31  
  16         423  
15 16     16   8962 use SBOM::CycloneDX::Metadata;
  16         77  
  16         808  
16 16     16   1520 use SBOM::CycloneDX::Schema;
  16         40  
  16         1040  
17 16     16   2086 use SBOM::CycloneDX::Util qw(urn_uuid);
  16         34  
  16         1233  
18              
19 16     16   104 use List::Util qw(uniq);
  16         30  
  16         985  
20              
21 16     16   131 use Moo;
  16         35  
  16         79  
22 16     16   7473 use namespace::autoclean;
  16         44  
  16         148  
23              
24             extends 'SBOM::CycloneDX::Base';
25              
26 16     16   1614 use constant JSON_SCHEMA_1_2 => 'http://cyclonedx.org/schema/bom-1.2b.schema.json';
  16         32  
  16         1330  
27 16     16   89 use constant JSON_SCHEMA_1_3 => 'http://cyclonedx.org/schema/bom-1.3a.schema.json';
  16         31  
  16         863  
28 16     16   83 use constant JSON_SCHEMA_1_4 => 'http://cyclonedx.org/schema/bom-1.4.schema.json';
  16         39  
  16         1172  
29 16     16   90 use constant JSON_SCHEMA_1_5 => 'http://cyclonedx.org/schema/bom-1.5.schema.json';
  16         31  
  16         754  
30 16     16   92 use constant JSON_SCHEMA_1_6 => 'http://cyclonedx.org/schema/bom-1.6.schema.json';
  16         31  
  16         652  
31 16     16   103 use constant JSON_SCHEMA_1_7 => 'http://cyclonedx.org/schema/bom-1.7.schema.json';
  16         66  
  16         29506  
32              
33             our $VERSION = 1.08;
34              
35             our %JSON_SCHEMA = (
36             '1.2' => JSON_SCHEMA_1_2,
37             '1.3' => JSON_SCHEMA_1_3,
38             '1.4' => JSON_SCHEMA_1_4,
39             '1.5' => JSON_SCHEMA_1_5,
40             '1.6' => JSON_SCHEMA_1_6,
41             '1.7' => JSON_SCHEMA_1_7
42             );
43              
44             has bom_format => (is => 'ro', isa => Str, required => 1, default => 'CycloneDX');
45              
46             has spec_version => (is => 'rw', isa => Num->where(sub { defined $JSON_SCHEMA{$_} }), required => 1, default => 1.7);
47              
48             has serial_number => (
49             is => 'rw',
50             isa => StrMatch [qr{^urn:uuid:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$}],
51             default => sub {urn_uuid}
52             );
53              
54             has version => (is => 'rw', isa => Int, default => 1);
55              
56             has metadata =>
57             (is => 'rw', isa => InstanceOf ['SBOM::CycloneDX::Metadata'], default => sub { SBOM::CycloneDX::Metadata->new });
58              
59             has components => (
60             is => 'rw',
61             isa => ArrayLike [InstanceOf ['SBOM::CycloneDX::Component']],
62             default => sub { SBOM::CycloneDX::List->new }
63             );
64              
65             has services => (
66             is => 'rw',
67             isa => ArrayLike [InstanceOf ['SBOM::CycloneDX::Service']],
68             default => sub { SBOM::CycloneDX::List->new }
69             );
70              
71             has external_references => (
72             is => 'rw',
73             isa => ArrayLike [InstanceOf ['SBOM::CycloneDX::ExternalReference']],
74             default => sub { SBOM::CycloneDX::List->new }
75             );
76              
77             has dependencies => (
78             is => 'rw',
79             isa => ArrayLike [InstanceOf ['SBOM::CycloneDX::Dependency']],
80             default => sub { SBOM::CycloneDX::List->new }
81             );
82              
83             has compositions => (
84             is => 'rw',
85             isa => ArrayLike [InstanceOf ['SBOM::CycloneDX::Composition']],
86             default => sub { SBOM::CycloneDX::List->new }
87             );
88              
89             has vulnerabilities => (
90             is => 'rw',
91             isa => ArrayLike [InstanceOf ['SBOM::CycloneDX::Vulnerability']],
92             default => sub { SBOM::CycloneDX::List->new }
93             );
94              
95             has annotations => (
96             is => 'rw',
97             isa => ArrayLike [InstanceOf ['SBOM::CycloneDX::Annotations']],
98             default => sub { SBOM::CycloneDX::List->new }
99             );
100              
101             has formulation => (
102             is => 'rw',
103             isa => ArrayLike [InstanceOf ['SBOM::CycloneDX::Formulation']],
104             default => sub { SBOM::CycloneDX::List->new }
105             );
106              
107             has declarations => (
108             is => 'rw',
109             isa => InstanceOf ['SBOM::CycloneDX::Declarations'],
110             default => sub { SBOM::CycloneDX::Declarations->new }
111             );
112              
113             has definitions => (
114             is => 'rw',
115             isa => InstanceOf ['SBOM::CycloneDX::Definitions'],
116             default => sub { SBOM::CycloneDX::Definitions->new }
117             );
118              
119             has citations => (
120             is => 'rw',
121             isa => ArrayLike [InstanceOf ['SBOM::CycloneDX::Citation']],
122             default => sub { SBOM::CycloneDX::List->new }
123             );
124              
125             has properties => (
126             is => 'rw',
127             isa => ArrayLike [InstanceOf ['SBOM::CycloneDX::Property']],
128             default => sub { SBOM::CycloneDX::List->new }
129             );
130              
131             # TODO JSF (JSON Signature Format)
132             has signature => (is => 'rw', isa => HashRef, default => sub { {} });
133              
134              
135 53     53 1 115303 sub validate { SBOM::CycloneDX::Schema->new(bom => shift)->validate }
136              
137             sub add_dependency {
138              
139 12     12 1 76 my ($self, $target, $depends_on) = @_;
140              
141 12         314 my $target_ref = $target->bom_ref;
142 12         107 my @depends_on_refs = map { $_->bom_ref } @{$depends_on};
  6         122  
  12         33  
143              
144 12         62 my $exists = 0;
145              
146 12         288 foreach my $dependency ($self->dependencies->each) {
147              
148 6 50       143 next unless $dependency->ref eq $target_ref;
149              
150 0         0 $exists = 1;
151              
152             # TODO
153 0         0 my @old_refs = @{$dependency->depends_on};
  0         0  
154 0         0 push @old_refs, @depends_on_refs;
155 0         0 my @new_refs = uniq(@old_refs);
156              
157 0         0 $dependency->depends_on(\@new_refs);
158              
159             }
160              
161 12 50       41 if (not $exists) {
162 12         262 $self->dependencies->push(
163             SBOM::CycloneDX::Dependency->new(ref => $target_ref, depends_on => \@depends_on_refs));
164             }
165              
166             # Add empty dependency entry if not exists "ref"
167 12         31 $self->add_dependency($_, []) for (@{$depends_on});
  12         81  
168              
169             }
170              
171             sub get_component_by_purl {
172              
173 0     0 1 0 my ($self, $purl) = @_;
174              
175 0         0 foreach my $component (@{$self->components}) {
  0         0  
176 0 0 0     0 return $component if ($component->purl && $component->purl eq $purl);
177             }
178              
179             }
180              
181             sub get_component_by_bom_ref {
182              
183 0     0 1 0 my ($self, $bom_ref) = @_;
184              
185 0         0 foreach my $component (@{$self->components}) {
  0         0  
186 0 0 0     0 return $component if ($component->bom_ref && $component->bom_ref eq $bom_ref);
187             }
188              
189             }
190              
191             sub get_vulnerabilities_for_bom_ref {
192              
193 0     0 1 0 my ($self, $bom_ref) = @_;
194              
195 0         0 my $list = SBOM::CycloneDX::List->new;
196              
197 0         0 foreach my $vulnerability (@{$self->vulnerabilities}) {
  0         0  
198 0         0 foreach my $affect (@{$vulnerability->affects}) {
  0         0  
199 0 0       0 $list->add($vulnerability) if $affect->ref eq $bom_ref;
200             }
201             }
202              
203 0         0 return $list;
204              
205             }
206              
207             sub get_affected_components_by_cve {
208              
209 0     0 1 0 my ($self, $cve_id) = @_;
210              
211 0         0 my $list = SBOM::CycloneDX::List->new;
212              
213 0         0 foreach my $vulnerability (@{$self->vulnerabilities}) {
  0         0  
214 0 0       0 if ($vulnerability->id eq $cve_id) {
215 0         0 foreach my $affect (@{$vulnerability->affects}) {
  0         0  
216 0         0 $list->add($self->get_component_by_bom_ref($affect->ref));
217             }
218             }
219             }
220              
221 0         0 return $list;
222              
223             }
224              
225             sub TO_JSON {
226              
227 356     356 1 1592 my $self = shift;
228              
229 356         10589 my $spec_version = $self->spec_version;
230 356         3332 my $schema = $JSON_SCHEMA{$spec_version};
231              
232 356         2248 my $json = {bomFormat => $self->bom_format, specVersion => "$spec_version"};
233              
234 356 100       2564 if ($spec_version > 1.5) {
235 148         487 $json->{'$schema'} = $schema;
236             }
237              
238 356 50       8637 $json->{serialNumber} = $self->serial_number if $self->serial_number;
239 356 50       16634 $json->{version} = $self->version if $self->version;
240 356 100       10015 $json->{metadata} = $self->metadata if %{$self->metadata->TO_JSON};
  356         7486  
241 356 100       1492 $json->{components} = $self->components if @{$self->components};
  356         7011  
242 356 50       1548 $json->{services} = $self->services if @{$self->services};
  356         7167  
243 356 50       617 $json->{externalReferences} = $self->external_references if @{$self->external_references};
  356         6938  
244 356 100       604 $json->{dependencies} = $self->dependencies if @{$self->dependencies};
  356         6800  
245 356 50       867 $json->{compositions} = $self->compositions if @{$self->compositions};
  356         6931  
246 356 50       574 $json->{vulnerabilities} = $self->vulnerabilities if @{$self->vulnerabilities};
  356         7492  
247 356 50       645 $json->{annotations} = $self->annotations if @{$self->annotations};
  356         7016  
248 356 50       599 $json->{formulation} = $self->formulation if @{$self->formulation};
  356         6883  
249 356 50       628 $json->{declarations} = $self->declarations if %{$self->declarations->TO_JSON};
  356         6906  
250 356 50       805 $json->{definitions} = $self->definitions if %{$self->definitions->TO_JSON};
  356         6946  
251 356 50       675 $json->{citations} = $self->citations if @{$self->citations};
  356         7147  
252 356 50       642 $json->{properties} = $self->properties if @{$self->properties};
  356         6985  
253 356 50       590 $json->{signature} = $self->signature if %{$self->signature};
  356         6649  
254              
255 356         7413 return $json;
256              
257             }
258              
259              
260             1;
261              
262             __END__