File Coverage

blib/lib/SBOM/CycloneDX.pm
Criterion Covered Total %
statement 114 146 78.0
branch 23 46 50.0
condition 0 6 0.0
subroutine 24 29 82.7
pod 7 7 100.0
total 168 234 71.7


line stmt bran cond sub pod time code
1             package SBOM::CycloneDX;
2              
3 16     16   373015 use 5.010001;
  16         45  
4 16     16   61 use strict;
  16         25  
  16         352  
5 16     16   52 use warnings;
  16         21  
  16         706  
6 16     16   913 use utf8;
  16         484  
  16         81  
7              
8 16     16   8514 use Types::Standard qw(Str StrMatch Int Num InstanceOf HashRef);
  16         1538745  
  16         172  
9 16     16   46391 use Types::TypeTiny qw(ArrayLike);
  16         28  
  16         127  
10              
11 16     16   37892 use SBOM::CycloneDX::Declarations;
  16         86  
  16         592  
12 16     16   7539 use SBOM::CycloneDX::Definitions;
  16         59  
  16         586  
13 16     16   6819 use SBOM::CycloneDX::Dependency;
  16         57  
  16         534  
14 16     16   96 use SBOM::CycloneDX::List;
  16         24  
  16         267  
15 16     16   7575 use SBOM::CycloneDX::Metadata;
  16         71  
  16         628  
16 16     16   1178 use SBOM::CycloneDX::Schema;
  16         30  
  16         1001  
17 16     16   1605 use SBOM::CycloneDX::Util qw(urn_uuid);
  16         32  
  16         1041  
18              
19 16     16   118 use List::Util qw(uniq);
  16         28  
  16         917  
20              
21 16     16   78 use Moo;
  16         26  
  16         76  
22 16     16   6404 use namespace::autoclean;
  16         29  
  16         141  
23              
24             extends 'SBOM::CycloneDX::Base';
25              
26 16     16   1277 use constant JSON_SCHEMA_1_2 => 'http://cyclonedx.org/schema/bom-1.2b.schema.json';
  16         27  
  16         1123  
27 16     16   63 use constant JSON_SCHEMA_1_3 => 'http://cyclonedx.org/schema/bom-1.3a.schema.json';
  16         41  
  16         746  
28 16     16   59 use constant JSON_SCHEMA_1_4 => 'http://cyclonedx.org/schema/bom-1.4.schema.json';
  16         33  
  16         852  
29 16     16   66 use constant JSON_SCHEMA_1_5 => 'http://cyclonedx.org/schema/bom-1.5.schema.json';
  16         25  
  16         546  
30 16     16   67 use constant JSON_SCHEMA_1_6 => 'http://cyclonedx.org/schema/bom-1.6.schema.json';
  16         24  
  16         495  
31 16     16   54 use constant JSON_SCHEMA_1_7 => 'http://cyclonedx.org/schema/bom-1.7.schema.json';
  16         27  
  16         28297  
32              
33             our $VERSION = 1.09;
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 0     0 1 0 sub validate { SBOM::CycloneDX::Schema->new(bom => shift)->validate }
136              
137             sub add_dependency {
138              
139 12     12 1 48 my ($self, $target, $depends_on) = @_;
140              
141 12         149 my $target_ref = $target->bom_ref;
142 12         55 my @depends_on_refs = map { $_->bom_ref } @{$depends_on};
  6         57  
  12         19  
143              
144 12         37 my $exists = 0;
145              
146 12         125 foreach my $dependency ($self->dependencies->each) {
147              
148 6 50       73 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       23 if (not $exists) {
162 12         125 $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         20 $self->add_dependency($_, []) for (@{$depends_on});
  12         42  
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 138     138 1 201 my $self = shift;
228              
229 138         3249 my $spec_version = $self->spec_version;
230 138         1001 my $schema = $JSON_SCHEMA{$spec_version};
231              
232 138         679 my $json = {bomFormat => $self->bom_format, specVersion => "$spec_version"};
233              
234 138 100       846 if ($spec_version > 1.5) {
235 58         132 $json->{'$schema'} = $schema;
236             }
237              
238 138 50       2320 $json->{serialNumber} = $self->serial_number if $self->serial_number;
239 138 50       4288 $json->{version} = $self->version if $self->version;
240 138 100       2447 $json->{metadata} = $self->metadata if %{$self->metadata->TO_JSON};
  138         1729  
241 138 100       418 $json->{components} = $self->components if @{$self->components};
  138         1712  
242 138 50       433 $json->{services} = $self->services if @{$self->services};
  138         1655  
243 138 50       212 $json->{externalReferences} = $self->external_references if @{$self->external_references};
  138         1673  
244 138 100       197 $json->{dependencies} = $self->dependencies if @{$self->dependencies};
  138         1613  
245 138 50       209 $json->{compositions} = $self->compositions if @{$self->compositions};
  138         1658  
246 138 50       166 $json->{vulnerabilities} = $self->vulnerabilities if @{$self->vulnerabilities};
  138         1618  
247 138 50       176 $json->{annotations} = $self->annotations if @{$self->annotations};
  138         1668  
248 138 50       169 $json->{formulation} = $self->formulation if @{$self->formulation};
  138         1649  
249 138 50       173 $json->{declarations} = $self->declarations if %{$self->declarations->TO_JSON};
  138         1583  
250 138 50       242 $json->{definitions} = $self->definitions if %{$self->definitions->TO_JSON};
  138         1638  
251 138 50       194 $json->{citations} = $self->citations if @{$self->citations};
  138         1629  
252 138 50       167 $json->{properties} = $self->properties if @{$self->properties};
  138         1603  
253 138 50       172 $json->{signature} = $self->signature if %{$self->signature};
  138         1578  
254              
255 138         2084 return $json;
256              
257             }
258              
259             1;
260              
261             __END__