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   491334 use 5.010001;
  16         72  
4 16     16   133 use strict;
  16         33  
  16         723  
5 16     16   93 use warnings;
  16         56  
  16         944  
6 16     16   1443 use utf8;
  16         686  
  16         108  
7              
8 16     16   12943 use Types::Standard qw(Str StrMatch Int Num InstanceOf HashRef);
  16         2265563  
  16         192  
9 16     16   65466 use Types::TypeTiny qw(ArrayLike);
  16         40  
  16         161  
10              
11 16     16   54488 use SBOM::CycloneDX::Declarations;
  16         105  
  16         834  
12 16     16   10164 use SBOM::CycloneDX::Definitions;
  16         87  
  16         865  
13 16     16   10098 use SBOM::CycloneDX::Dependency;
  16         91  
  16         838  
14 16     16   154 use SBOM::CycloneDX::List;
  16         42  
  16         467  
15 16     16   11561 use SBOM::CycloneDX::Metadata;
  16         98  
  16         917  
16 16     16   1433 use SBOM::CycloneDX::Schema;
  16         63  
  16         1364  
17 16     16   2133 use SBOM::CycloneDX::Util qw(urn_uuid);
  16         50  
  16         1668  
18              
19 16     16   134 use List::Util qw(uniq);
  16         34  
  16         1272  
20              
21 16     16   113 use Moo;
  16         35  
  16         2966  
22 16     16   9614 use namespace::autoclean;
  16         49  
  16         425  
23              
24             extends 'SBOM::CycloneDX::Base';
25              
26 16     16   1992 use constant JSON_SCHEMA_1_2 => 'http://cyclonedx.org/schema/bom-1.2b.schema.json';
  16         284  
  16         1824  
27 16     16   429 use constant JSON_SCHEMA_1_3 => 'http://cyclonedx.org/schema/bom-1.3a.schema.json';
  16         41  
  16         1495  
28 16     16   110 use constant JSON_SCHEMA_1_4 => 'http://cyclonedx.org/schema/bom-1.4.schema.json';
  16         57  
  16         1167  
29 16     16   126 use constant JSON_SCHEMA_1_5 => 'http://cyclonedx.org/schema/bom-1.5.schema.json';
  16         33  
  16         1447  
30 16     16   131 use constant JSON_SCHEMA_1_6 => 'http://cyclonedx.org/schema/bom-1.6.schema.json';
  16         81  
  16         1023  
31 16     16   122 use constant JSON_SCHEMA_1_7 => 'http://cyclonedx.org/schema/bom-1.7.schema.json';
  16         64  
  16         37630  
32              
33             our $VERSION = 1.07;
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 109508 sub validate { SBOM::CycloneDX::Schema->new(bom => shift)->validate }
136              
137             sub add_dependency {
138              
139 12     12 1 74 my ($self, $target, $depends_on) = @_;
140              
141 12         324 my $target_ref = $target->bom_ref;
142 12         113 my @depends_on_refs = map { $_->bom_ref } @{$depends_on};
  6         130  
  12         31  
143              
144 12         65 my $exists = 0;
145              
146 12         283 foreach my $dependency ($self->dependencies->each) {
147              
148 6 50       181 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       46 if (not $exists) {
162 12         273 $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         82  
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 1700 my $self = shift;
228              
229 356         11163 my $spec_version = $self->spec_version;
230 356         3750 my $schema = $JSON_SCHEMA{$spec_version};
231              
232 356         2555 my $json = {bomFormat => $self->bom_format, specVersion => "$spec_version"};
233              
234 356 100       2718 if ($spec_version > 1.5) {
235 148         532 $json->{'$schema'} = $schema;
236             }
237              
238 356 50       8870 $json->{serialNumber} = $self->serial_number if $self->serial_number;
239 356 50       19451 $json->{version} = $self->version if $self->version;
240 356 100       10601 $json->{metadata} = $self->metadata if %{$self->metadata->TO_JSON};
  356         8278  
241 356 100       1598 $json->{components} = $self->components if @{$self->components};
  356         7448  
242 356 50       1871 $json->{services} = $self->services if @{$self->services};
  356         7475  
243 356 50       656 $json->{externalReferences} = $self->external_references if @{$self->external_references};
  356         7374  
244 356 100       731 $json->{dependencies} = $self->dependencies if @{$self->dependencies};
  356         7531  
245 356 50       962 $json->{compositions} = $self->compositions if @{$self->compositions};
  356         7239  
246 356 50       679 $json->{vulnerabilities} = $self->vulnerabilities if @{$self->vulnerabilities};
  356         7625  
247 356 50       673 $json->{annotations} = $self->annotations if @{$self->annotations};
  356         7581  
248 356 50       643 $json->{formulation} = $self->formulation if @{$self->formulation};
  356         8565  
249 356 50       670 $json->{declarations} = $self->declarations if %{$self->declarations->TO_JSON};
  356         7430  
250 356 50       833 $json->{definitions} = $self->definitions if %{$self->definitions->TO_JSON};
  356         7557  
251 356 50       780 $json->{citations} = $self->citations if @{$self->citations};
  356         8014  
252 356 50       658 $json->{properties} = $self->properties if @{$self->properties};
  356         7372  
253 356 50       671 $json->{signature} = $self->signature if %{$self->signature};
  356         7299  
254              
255 356         7981 return $json;
256              
257             }
258              
259              
260             1;
261              
262             __END__