view release on metacpan or search on metacpan
Change history for App-CPAN-SBOM
1.03 2025-08-11
- Added "DTRACK_*" env variables for CI/CD
- Improved BOM generation
1.02 2025-06-05
- Added support for projects that use "cpanfile.snapshot" or "cpanfile"
- Added support for uploading BOM files to OWASP Dependency Track
1.01 2025-03-14
- Added CPAN::Audit for populate vulnerabilities
1.00 2025-03-10
- First release of App::CPAN::SBOM
# App::CPAN::SBOM - Utility for generate SBOM file
The INSTALL is used to introduce the module and provide instructions on
how to install the module, any machine dependencies it may have (for
example C compilers and installed libraries) and any other information
that should be provided before the module is installed.
## INSTALLATION
Using Makefile.PL:
To install this module, run the following commands.
perl Makefile.PL
make
make test
make install
Using App::cpanminus:
cpanm App::CPAN::SBOM
## SUPPORT AND DOCUMENTATION
After installing, you can find documentation for this module with the
perldoc command.
perldoc App::CPAN::SBOM
You can also look for information at:
* GitHub issues (report bugs here) https://github.com/giterlizzi/perl-App-CPAN-SBOM/issues
## LICENSE AND COPYRIGHT
Copyright (C) 2025 Giuseppe Di Terlizzi
This program is free software; you can redistribute it and/or modify it
under the terms of the the Artistic License (2.0). You may obtain a
copy of the full license at:
bin/cpan-sbom
Changes
INSTALL.md
lib/App/CPAN/SBOM.pm
LICENSE
Makefile.PL
MANIFEST
README.md
t/00-load.t
xt/kwalitee.t
xt/manifest.t
xt/pod-coverage.t
xt/pod.t
META.yml Module YAML meta-data (added by MakeMaker)
],
"dynamic_config" : 1,
"generated_by" : "ExtUtils::MakeMaker version 7.76, CPAN::Meta::Converter version 2.150010",
"license" : [
"artistic_2"
],
"meta-spec" : {
"url" : "http://search.cpan.org/perldoc?CPAN::Meta::Spec",
"version" : 2
},
"name" : "App-CPAN-SBOM",
"no_index" : {
"directory" : [
"t",
"inc"
]
},
"prereqs" : {
"build" : {
"requires" : {
"ExtUtils::MakeMaker" : "0"
"ExtUtils::MakeMaker" : "0"
}
},
"runtime" : {
"requires" : {
"CPAN::Audit" : "0",
"Cpanel::JSON::XS" : "0",
"HTTP::Tiny" : "0",
"MIME::Base64" : "0",
"MetaCPAN::Client" : "0",
"SBOM::CycloneDX" : "0",
"URI::PackageURL" : "2.22",
"perl" : "5.016"
}
},
"test" : {
"requires" : {
"Test::More" : "0"
}
}
},
"release_status" : "stable",
"resources" : {
"bugtracker" : {
"web" : "https://github.com/giterlizzi/perl-App-CPAN-SBOM/issues"
},
"repository" : {
"type" : "git",
"url" : "git://github.com/giterlizzi/perl-App-CPAN-SBOM",
"web" : "https://github.com/giterlizzi/perl-App-CPAN-SBOM"
}
},
"version" : "1.03",
"x_purl" : "pkg:cpan/GDT/App-CPAN-SBOM",
"x_serialization_backend" : "JSON::PP version 4.16"
}
ExtUtils::MakeMaker: '0'
Test::More: '0'
configure_requires:
ExtUtils::MakeMaker: '0'
dynamic_config: 1
generated_by: 'ExtUtils::MakeMaker version 7.76, CPAN::Meta::Converter version 2.150010'
license: artistic_2
meta-spec:
url: http://module-build.sourceforge.net/META-spec-v1.4.html
version: '1.4'
name: App-CPAN-SBOM
no_index:
directory:
- t
- inc
requires:
CPAN::Audit: '0'
Cpanel::JSON::XS: '0'
HTTP::Tiny: '0'
MIME::Base64: '0'
MetaCPAN::Client: '0'
SBOM::CycloneDX: '0'
URI::PackageURL: '2.22'
perl: '5.016'
resources:
bugtracker: https://github.com/giterlizzi/perl-App-CPAN-SBOM/issues
repository: git://github.com/giterlizzi/perl-App-CPAN-SBOM
version: '1.03'
x_purl: pkg:cpan/GDT/App-CPAN-SBOM
x_serialization_backend: 'CPAN::Meta::YAML version 0.020'
Makefile.PL view on Meta::CPAN
#!perl
use strict;
use warnings;
use ExtUtils::MakeMaker;
WriteMakefile(
NAME => 'App-CPAN-SBOM',
AUTHOR => q{Giuseppe Di Terlizzi <gdt@cpan.org>},
VERSION_FROM => 'lib/App/CPAN/SBOM.pm',
LICENSE => 'artistic_2',
MIN_PERL_VERSION => 5.016,
PL_FILES => {},
EXE_FILES => ['bin/cpan-sbom'],
CONFIGURE_REQUIRES => {'ExtUtils::MakeMaker' => '0'},
TEST_REQUIRES => {'Test::More' => '0'},
PREREQ_PM => {
'SBOM::CycloneDX' => 0,
'CPAN::Audit' => 0,
'MetaCPAN::Client' => 0,
'URI::PackageURL' => '2.22',
'MIME::Base64' => 0,
'HTTP::Tiny' => 0,
'Cpanel::JSON::XS' => 0,
},
META_MERGE => {
'meta-spec' => {version => 2},
'resources' => {
bugtracker => {web => 'https://github.com/giterlizzi/perl-App-CPAN-SBOM/issues'},
repository => {
type => 'git',
url => 'git://github.com/giterlizzi/perl-App-CPAN-SBOM',
web => 'https://github.com/giterlizzi/perl-App-CPAN-SBOM'
},
},
x_purl => 'pkg:cpan/GDT/App-CPAN-SBOM'
},
dist => {COMPRESS => 'gzip -9f', SUFFIX => 'gz'},
clean => {FILES => 'App-CPAN-SBOM-*'},
);
[](https://github.com/giterlizzi/perl-App-CPAN-SBOM/releases) [](https://g...
# App-CPAN-SBOM - CPAN SBOM (Software Bill of Materials) generator
## Synopsis
```.bash
cpan-sbom --distribution NAME@VERSION
cpan-sbom --meta (META|MYMETA).(json|yml)
cpan-sbom --project-directory DIRECTORY [ --project-name NAME --project-version VERSION --project-description TEXT
--project-license SPDX-LICENSE --project-type BOM-TYPE
--project-author STRING [--project-author STRING] ]
cpan-sbom [--help|--man|-v]
Options:
-o, --output Output file. Default bom.json
--distribution NAME@VERSION Distribution name and version
--meta META or MYMETA file
--project-directory NAME Project directory
--project-meta Project META or MYMETA file (alias of --meta)
--project-type BOM-TYPE Project type (default: library)
--project-name NAME Project name (default: project directory name)
--project-version VERSION Project version
--project-author STRING Project author(s)
--project-license SPDX-LICENSE Project SPDX license
--project-description TEXT Project description
--maxdepth=NUM Max depth (default: 1)
--vulnerabilities Include Module/Distribution vulnerabilities
--no-vulnerabilities
--validate Validate the generated SBOM using JSON Schema (default: true)
--no-validate
--list-spdx-licenses List SPDX licenses
--debug Enable debug messages
--help Brief help message
--man Full documentation
-v, --version Print version
--skip-tls-check Disable SSL/TLS check (Env: $DTRACK_SKIP_TLS_CHECK)
--project-id STRING Project ID (Env: $DTRACK_PROJECT_ID)
--project-name NAME Project name (Env: DTRACK_PROJECT_NAME)
--project-version VERSION Project version (Env: $DTRACK_PROJECT_VERSION)
--parent-project-id STRING Parent project ID (Env: $DTRACK_PARENT_PROJECT_ID)
```
## Examples
```.bash
Create SBOM of specific distribution:
$ cpan-sbom --distribution libwww-perl@6.78
Create SBOM from META file:
$ cpan-sbom --meta META.json
Create SBOM from your project directory:
$ cpan-sbom \
--project-directory . \
--project-name "My Cool Application" \
--project-type application \
--project-version 1.337 \
--project-license Artistic-2.0
--project-author "Larry Wall <larry@wall.org>"
Create SBOM file and upload to OWASP Dependency Track:
$ cpan-sbom \
--meta META.json \
--server-url https://dtrack.example.com \
--api-key DTRAC-API-KEY \
--project-id DTRACK-PROJECT-ID
```
## Install
Using Makefile.PL:
To install `App-CPAN-SBOM` distribution, run the following commands.
perl Makefile.PL
make
make test
make install
Using `App::cpanminus`:
cpanm App::CPAN::SBOM
## Documentation
- `perldoc App::CPAN::SBOM`
- https://metacpan.org/release/App-CPAN-SBOM
## Copyright
- Copyright 2025 © Giuseppe Di Terlizzi
bin/cpan-sbom view on Meta::CPAN
#!/usr/bin/perl
use strict;
use warnings;
use utf8;
use App::CPAN::SBOM;
exit App::CPAN::SBOM->run(@ARGV) unless caller();
__END__
=encoding utf-8
=head1 NAME
cpan-sbom - CPAN SBOM (Software Bill of Materials) generator
=head1 SYNOPSIS
cpan-sbom --distribution NAME@VERSION
cpan-sbom --meta (META|MYMETA).(json|yml)
cpan-sbom --project-directory DIRECTORY [ --project-name NAME --project-version VERSION --project-description TEXT
--project-license SPDX-LICENSE --project-type BOM-TYPE
--project-author STRING [--project-author STRING] ]
cpan-sbom [--help|--man|-v]
Options:
-o, --output Output file. Default bom.json
--distribution NAME@VERSION Distribution name and version
--meta META or MYMETA file
--project-directory NAME Project directory
--project-meta Project META or MYMETA file (alias of --meta)
--project-type BOM-TYPE Project type (default: library)
--project-name NAME Project name (default: project directory name)
--project-version VERSION Project version
--project-author STRING Project author(s)
--project-license SPDX-LICENSE Project SPDX license
--project-description TEXT Project description
--maxdepth=NUM Max depth (default: 1)
--vulnerabilities Include Module/Distribution vulnerabilities
--no-vulnerabilities
--validate Validate the generated SBOM using JSON Schema (default: true)
--no-validate
--list-spdx-licenses List SPDX licenses
--debug Enable debug messages
--help Brief help message
--man Full documentation
-v, --version Print version
bin/cpan-sbom view on Meta::CPAN
--server-url URL Dependency Track URL (Env: $DTRACK_URL)
--api-key STRING API-Key (Env: $DTRACK_API_KEY)
--skip-tls-check Disable SSL/TLS check (Env: $DTRACK_SKIP_TLS_CHECK)
--project-id STRING Project ID (Env: $DTRACK_PROJECT_ID)
--project-name NAME Project name (Env: DTRACK_PROJECT_NAME)
--project-version VERSION Project version (Env: $DTRACK_PROJECT_VERSION)
--parent-project-id STRING Parent project ID (Env: $DTRACK_PARENT_PROJECT_ID)
=head1 DESCRIPTION
C<cpan-sbom> CPAN SBOM (Software Bill of Materials) generator
=head1 EXAMPLES
Create SBOM of specific distribution:
$ cpan-sbom --distribution libwww-perl@6.78
Create SBOM from META file:
$ cpan-sbom --meta META.json
Create SBOM from your project directory:
$ cpan-sbom \
--project-directory . \
--project-name "My Cool Application" \
--project-type application \
--project-version 1.337 \
--project-license Artistic-2.0
--project-author "Larry Wall <larry@wall.org>"
Create SBOM file and upload to OWASP Dependency Track:
$ cpan-sbom \
--meta META.json \
--server-url https://dtrack.example.com \
--api-key DTRAC-API-KEY \
--project-id DTRACK-PROJECT-ID
=head1 SEE ALSO
L<SBOM::CycloneDX>
=head1 AUTHOR
L<Giuseppe Di Terlizzi|https://metacpan.org/author/gdt>
=head1 COPYRIGHT AND LICENSE
Copyright © 2025 L<Giuseppe Di Terlizzi|https://metacpan.org/author/gdt>
You may use and distribute this module according to the same terms
lib/App/CPAN/SBOM.pm view on Meta::CPAN
package App::CPAN::SBOM;
use 5.010001;
use strict;
use warnings;
use utf8;
use CPAN::Audit;
use CPAN::Meta;
use Cpanel::JSON::XS qw(encode_json);
use Data::Dumper;
use File::Basename;
use File::Spec;
use Getopt::Long qw(GetOptionsFromArray :config gnu_compat);
use HTTP::Tiny;
use MetaCPAN::Client;
use MIME::Base64;
use Pod::Usage qw(pod2usage);
use URI::PackageURL;
use SBOM::CycloneDX::Component;
use SBOM::CycloneDX::ExternalReference;
use SBOM::CycloneDX::Hash;
use SBOM::CycloneDX::License;
use SBOM::CycloneDX::Metadata;
use SBOM::CycloneDX::OrganizationalContact;
use SBOM::CycloneDX::Util qw(cpan_meta_to_spdx_license cyclonedx_tool cyclonedx_component);
use SBOM::CycloneDX::Vulnerability::Affect;
use SBOM::CycloneDX::Vulnerability::Rating;
use SBOM::CycloneDX::Vulnerability::Source;
use SBOM::CycloneDX::Vulnerability;
use SBOM::CycloneDX;
our $VERSION = '1.03';
sub DEBUG { $ENV{SBOM_DEBUG} || 0 }
sub cli_error {
my ($error, $code) = @_;
$error =~ s/ at .* line \d+.*//;
say STDERR "ERROR: $error";
return $code || 1;
}
sub run {
lib/App/CPAN/SBOM.pm view on Meta::CPAN
pod2usage(-exitstatus => 0, -verbose => 2) if defined $options{man};
pod2usage(-exitstatus => 0, -verbose => 0) if defined $options{help};
$options{'project-meta'} //= $options{meta};
if (defined $options{v}) {
return show_version();
}
if ($options{'list-spdx-licenses'}) {
say $_ for (sort @{SBOM::CycloneDX::Enum->SPDX_LICENSES});
return 0;
}
unless ($options{distribution} || $options{'project-meta'} || $options{'project-directory'}) {
pod2usage(-exitstatus => 0, -verbose => 0);
}
$options{maxdepth} //= 1;
$options{validate} //= 1;
if (defined $options{debug}) {
$ENV{SBOM_DEBUG} = 1;
}
my $bom = SBOM::CycloneDX->new;
if (defined $options{distribution}) {
my ($distribution, $version) = split '@', $options{distribution};
return cli_error('Missing distribution version') unless $version;
make_sbom_from_dist(bom => $bom, distribution => $distribution, version => $version, options => \%options);
}
if (defined $options{'project-directory'} || defined $options{'project-meta'}) {
make_sbom_from_project(bom => $bom, options => \%options);
}
$bom->metadata->tools->push(cyclonedx_tool());
my $output_file = $options{output} // 'bom.json';
say STDERR "Save SBOM to $output_file";
open my $fh, '>', $output_file or Carp::croak "Failed to open file: $!";
say $fh $bom->to_string;
close $fh;
if ($options{validate}) {
my @errors = $bom->validate;
say STDERR $_ foreach (@errors);
}
lib/App/CPAN/SBOM.pm view on Meta::CPAN
sub show_version {
(my $progname = $0) =~ s/.*\///;
say <<"VERSION";
$progname version $VERSION
Copyright 2025, Giuseppe Di Terlizzi <gdt\@cpan.org>
This program is part of the "App-CPAN-SBOM" distribution and is free software;
you can redistribute it and/or modify it under the same terms as Perl itself.
Complete documentation for $progname can be found using 'man $progname'
or on the internet at <https://metacpan.org/dist/App-CPAN-SBOM>.
VERSION
return 0;
}
sub make_sbom_from_project {
my (%params) = @_;
my $audit_discover = CPAN::Audit::Discover->new;
my $bom = $params{bom};
my $options = $params{options} || {};
my @META_FILES = (qw[META.json META.yml MYMETA.json MYMETA.yml]);
say STDERR 'Generate SBOM';
my $project_type = $options->{'project-type'} || 'library';
my $project_directory = File::Spec->rel2abs($options->{'project-directory'});
my $project_meta = $options->{'project-meta'} || $options->{'meta'};
my $project_name = $options->{'project-name'} || basename($project_directory);
my $project_version = $options->{'project-version'} || 0;
my $project_description = $options->{'project-description'};
my $project_license = $options->{'project-license'};
my $project_author = $options->{'project-author'} || [];
lib/App/CPAN/SBOM.pm view on Meta::CPAN
if ($project_meta) {
my $meta = CPAN::Meta->load_file($project_meta);
$project_name = $meta->name;
$project_version = $meta->version;
@authors = make_authors([$meta->author]);
@external_references = make_external_references($meta->{resources});
@licenses = (SBOM::CycloneDX::License->new(id => cpan_meta_to_spdx_license($meta->license)));
# Detect distribution author dependencies
# TODO get the author-defined dependency version
my $prereqs = $meta->effective_prereqs;
my $reqs = $prereqs->requirements_for("runtime", "requires");
for my $module (sort $reqs->required_modules) {
next if $module eq 'perl';
push @dependencies, {module => $module};
}
}
if ($project_license) {
@licenses = (SBOM::CycloneDX::License->new(id => $project_license));
}
if (@{$project_author}) {
@authors = make_authors($project_author);
}
my $bom_ref = "$project_name\@$project_version";
$bom_ref =~ s/\s+/-/g;
# Build root BOM component
my $root_component = SBOM::CycloneDX::Component->new(
type => $project_type,
name => $project_name,
version => $project_version,
bom_ref => $bom_ref,
licenses => \@licenses,
authors => \@authors,
external_references => \@external_references,
);
if ($project_description) {
$root_component->description($project_description);
}
# Add root BOM component in metadata
$bom->metadata->component($root_component);
# Find dependencies from "cpanfile.snapshot" or "cpanfile"
if (my @audit_deps = $audit_discover->discover($project_directory)) {
@dependencies = @audit_deps;
}
foreach my $dependency (@dependencies) {
make_dep_compoment(
lib/App/CPAN/SBOM.pm view on Meta::CPAN
sub make_sbom_from_dist {
my (%params) = @_;
my $distribution = $params{distribution};
my $version = $params{version};
my $bom = $params{bom};
my $options = $params{options} || {};
say STDERR "Generate SBOM for $distribution\@$version";
my $mcpan = MetaCPAN::Client->new;
my $release_data = $mcpan->release({all => [{distribution => $distribution}, {version => $version}]});
my $dist_data = $release_data->next;
unless ($dist_data) {
Carp::carp("Unable to find release ($distribution\@$version) in Meta::CPAN");
return;
}
lib/App/CPAN/SBOM.pm view on Meta::CPAN
namespace => $dist_data->author,
name => $dist_data->distribution,
version => $dist_data->version
);
my @external_references = make_external_references($dist_data->metadata->{resources});
my $license = join ' AND ', @{$metadata->{license}};
my $spdx_license = cpan_meta_to_spdx_license($license);
my $bom_license = SBOM::CycloneDX::License->new(($spdx_license) ? {id => $spdx_license} : {name => $license});
my $root_component = SBOM::CycloneDX::Component->new(
type => 'library',
name => $dist_data->name,
version => $dist_data->version,
licenses => [$bom_license],
authors => \@authors,
bom_ref => $purl->to_string,
purl => $purl,
external_references => \@external_references
);
lib/App/CPAN/SBOM.pm view on Meta::CPAN
}
sub make_external_references {
my $resources = shift;
my @external_references = ();
if (defined $resources->{repository} && $resources->{repository}->{url}) {
my $external_reference
= SBOM::CycloneDX::ExternalReference->new(type => 'vcs', url => $resources->{repository}->{url});
push @external_references, $external_reference;
}
if (defined $resources->{bugtracker} && $resources->{bugtracker}->{web}) {
my $external_reference
= SBOM::CycloneDX::ExternalReference->new(type => 'issue-tracker', url => $resources->{bugtracker}->{web});
push @external_references, $external_reference;
}
return @external_references;
}
sub make_authors {
my $metadata_authors = shift;
my @authors = ();
foreach my $metadata_author (@{$metadata_authors}) {
if ($metadata_author =~ /(.*) <(.*)>/) {
my ($name, $email) = $metadata_author =~ /(.*) <(.*)>/;
push @authors, SBOM::CycloneDX::OrganizationalContact->new(name => $name, email => _clean_email($email));
}
elsif ($metadata_author =~ /(.*), (.*)/) {
my ($name, $email) = $metadata_author =~ /(.*), (.*)/;
push @authors, SBOM::CycloneDX::OrganizationalContact->new(name => $name, email => _clean_email($email));
}
else {
push @authors, SBOM::CycloneDX::OrganizationalContact->new(name => $metadata_author);
}
}
return @authors;
}
sub _clean_email {
my $email = shift;
lib/App/CPAN/SBOM.pm view on Meta::CPAN
my $metadata = $dist_data->metadata;
$author //= $dist_data->author;
my @authors = make_authors($metadata->{author});
my $license = join ' AND ', @{$dist_data->metadata->{license}};
my $spdx_license = cpan_meta_to_spdx_license($license);
my $bom_license = SBOM::CycloneDX::License->new(($spdx_license) ? {id => $spdx_license} : {name => $license});
my $purl = URI::PackageURL->new(type => 'cpan', namespace => $author, name => $distribution, version => $version);
my @ext_refs = make_external_references($dist_data->metadata->{resources});
my $hashes = SBOM::CycloneDX::List->new;
if (my $checksum = $dist_data->checksum_sha256) {
$hashes->add(SBOM::CycloneDX::Hash->new(alg => 'sha-256', content => $checksum));
}
if (my $checksum = $dist_data->checksum_md5) {
$hashes->add(SBOM::CycloneDX::Hash->new(alg => 'md5', content => $checksum));
}
my $component = SBOM::CycloneDX::Component->new(
type => 'library',
name => $distribution,
version => $version,
licenses => [$bom_license],
authors => \@authors,
bom_ref => $purl->to_string,
purl => $purl,
hashes => $hashes,
external_references => \@ext_refs,
);
lib/App/CPAN/SBOM.pm view on Meta::CPAN
foreach my $advisory (@{$result->{dists}->{$distribution}->{advisories}}) {
my $description = $advisory->{description};
my $severity = $advisory->{severity} || 'unknown';
my @cves = @{$advisory->{cves}};
my $cpansa = $advisory->{id};
my @references = @{$advisory->{references}};
foreach my $cve (@cves) {
my $vulnerability = SBOM::CycloneDX::Vulnerability->new(
id => $cve,
description => $description,
source => SBOM::CycloneDX::Vulnerability::Source->new(
name => 'NVD',
url => "https://nvd.nist.gov/vuln/detail/$cve"
),
affects => [SBOM::CycloneDX::Vulnerability::Affect->new(ref => $bom_ref)],
ratings => [SBOM::CycloneDX::Vulnerability::Rating->new(severity => $severity)]
);
$bom->vulnerabilities->add($vulnerability);
}
}
}
sub submit_bom {
lib/App/CPAN/SBOM.pm view on Meta::CPAN
$bom_payload->{parentUUID} = $options->{'parent-project-id'};
}
my $verify_ssl = (defined $options->{'skip-tls-check'}) ? 0 : 1;
my $ua = HTTP::Tiny->new(
verify_SSL => $verify_ssl,
default_headers => {'Content-Type' => 'application/json', 'X-Api-Key' => $options->{'api-key'}}
);
say STDERR "Upload BOM in OSWASP Dependency Track ($server_url)";
my $response = $ua->put($server_url, {content => encode_json($bom_payload)});
DEBUG and say STDERR "-- Response <-- " . Dumper($response);
unless ($response->{success}) {
return cli_error(sprintf(
'Failed to upload BOM file to OWASP Dependency Track: (%s) %s - %s',
$response->{status}, $response->{reason}, $response->{content}
));
}
}
1;
__END__
=encoding utf-8
=head1 NAME
App::CPAN::SBOM - CPAN SBOM (Software Bill of Materials) generator
=head1 SYNOPSIS
use App::CPAN::SBOM qw(run);
run(\@ARGV);
=head1 DESCRIPTION
L<App::CPAN::SBOM> is a "Command Line Interface" helper module for C<cpan-sbom(1)> command.
=head2 METHODS
=over
=item App::CPAN::SBOM->run(@args)
=back
Execute the command
=head1 SUPPORT
=head2 Bugs / Feature Requests
Please report any bugs or feature requests through the issue tracker
at L<https://github.com/giterlizzi/perl-App-CPAN-SBOM/issues>.
You will be notified automatically of any progress on your issue.
=head2 Source Code
This is open source software. The code repository is available for
public review and contribution under the terms of the license.
L<https://github.com/giterlizzi/perl-App-CPAN-SBOM>
git clone https://github.com/giterlizzi/perl-App-CPAN-SBOM.git
=head1 AUTHOR
=over 4
=item * Giuseppe Di Terlizzi <gdt@cpan.org>
=back
t/00-load.t view on Meta::CPAN
#!perl -T
use strict;
use warnings;
use Test::More;
use_ok('App::CPAN::SBOM');
done_testing();
diag("App::CPAN::SBOM $App::CPAN::SBOM::VERSION, Perl $], $^X");