Dist-Zilla-Plugin-CycloneDX
view release on metacpan or search on metacpan
lib/Dist/Zilla/Plugin/CycloneDX.pm view on Meta::CPAN
package Dist::Zilla::Plugin::CycloneDX;
$Dist::Zilla::Plugin::CycloneDX::VERSION = '0.001';
use 5.020;
use Moose;
with qw/Dist::Zilla::Role::FileGatherer/;
use MooseX::Types::Moose 'Str';
use namespace::autoclean;
use experimental qw/signatures postderef lexical_subs/;
no autovivification;
use Carp;
use Dist::Zilla::File::InMemory;
use Time::Piece;
use URI::PackageURL;
use SBOM::CycloneDX 1.08;
use SBOM::CycloneDX::Component;
use SBOM::CycloneDX::ExternalReference;
use SBOM::CycloneDX::License;
use SBOM::CycloneDX::Metadata;
use SBOM::CycloneDX::Metadata::Lifecycle;
use SBOM::CycloneDX::OrganizationalContact;
use SBOM::CycloneDX::Tools;
use SBOM::CycloneDX::Util qw/cpan_meta_to_spdx_license cyclonedx_component/;
has filename => (
is => 'ro',
isa => Str,
lazy => 1,
default => sub($self) {
my $name = $self->zilla->name;
my $version = $self->zilla->version;
return "CPAN-META/$name-$version.cdx.json";
}
);
my %simple_resource = (
homepage => 'website',
x_mailing_list => 'mailing-list',
x_MailingList => 'mailing-list',
x_mailinglist => 'mailing-list',
x_mastodon => 'social',
x_twitter => 'social',
x_wiki => 'documentation',
);
my sub make_externs($module, $resources) {
my @externs = SBOM::CycloneDX::ExternalReference->new(type => 'documentation', url => "https://metacpan.org/pod/$module");
if (my $tracker = $resources->{bugtracker}{web}) {
push @externs, SBOM::CycloneDX::ExternalReference->new(type => 'issue-tracker', url => $tracker);
}
if (my $repo = $resources->{repository}{web}) {
push @externs, SBOM::CycloneDX::ExternalReference->new(type => 'vcs', url => $repo);
}
if (my $irc = $resources->{x_IRC}) {
my $url = ref $irc ? $irc->{url} : $irc;
push @externs, SBOM::CycloneDX::ExternalReference->new(type => 'chat', url => $url);
}
for my $key (keys %simple_resource) {
if (my $url = $resources->{$key}) {
push @externs, SBOM::CycloneDX::ExternalReference->new(type => $simple_resource{$key}, url => $url);
}
}
return sort { $a->type cmp $b->type } @externs;
}
sub make_purl($name, $version) {
return URI::PackageURL->new(
type => 'cpan',
namespace => '',
name => $name,
version => $version,
);
}
my sub make_root_component($meta) {
my $purl = make_purl($meta->{name}, $meta->{version});
my $bom_ref = sprintf '%s@%s', $meta->{name}, $meta->{version};
my $module = $meta->{name} =~ s/-/::/gr;
my @externs = make_externs($module, $meta->{resources});
return SBOM::CycloneDX::Component->new(
type => 'library',
group => 'CPAN',
name => $meta->{name},
version => $meta->{version},
description => $meta->{abstract},
bom_ref => $bom_ref,
purl => $purl,
external_references => \@externs,
);
}
my sub to_organization($author) {
my ($name, $email) = $author =~ /([^<]+?)\s+<([^>]+)>/ or croak "Could not parse author '$author'";
return SBOM::CycloneDX::OrganizationalContact->new(name => $name, email => $email);
}
my sub make_licenses($meta) {
if (my $spdx_expr = $meta->{x_spdx_expression}) {
return SBOM::CycloneDX::License->new($spdx_expr);
} else {
my @raw = map { cpan_meta_to_spdx_license($_) } $meta->{license}->@*;
return map { SBOM::CycloneDX::License->new($_) } @raw;
}
}
my %self_resources = (
bugtracker => {
web => 'https://github.com/Leont/dist-zilla-plugin-cyclonedx/issues',
},
repository => {
web => 'https://github.com/Leont/dist-zilla-plugin-cyclonedx',
},
x_IRC => 'irc://irc.perl.org/#distzilla',
);
my sub make_tool_component($module, $dist, $resources = {}) {
my @externs = make_externs($module, $resources);
my $version = $module->VERSION;
my %args = (
type => 'library',
group => 'CPAN',
name => $dist,
external_references => \@externs,
);
if ($version) {
$args{version} = $version;
$args{purl} = make_purl($dist, $version);
}
return SBOM::CycloneDX::Component->new(%args);
}
my sub make_tools($plugins) {
my $distname = __PACKAGE__ =~ s/::/-/gr;
my @tools = make_tool_component(__PACKAGE__, $distname, \%self_resources);
push @tools, map { make_tool_component(ref($_), $_->cyclonedx_dist, $_->cyclonedx_resources) } $plugins->@*;
push @tools, cyclonedx_component;
return SBOM::CycloneDX::Tools->new(components => \@tools);
}
my sub make_metadata($meta, $plugins) {
my $component = make_root_component($meta);
my @authors = map { to_organization($_) } $meta->{author}->@*;
my $timestamp = SBOM::CycloneDX::Timestamp->new($ENV{TIMESTAMP} // scalar gmtime);
my @licenses = make_licenses($meta);
my @lifecycle = SBOM::CycloneDX::Metadata::Lifecycle->new(phase => 'pre-build');
my $tools = make_tools($plugins);
return SBOM::CycloneDX::Metadata->new(
component => $component,
authors => \@authors,
timestamp => $timestamp,
licenses => \@licenses,
lifecycles => \@lifecycle,
tools => $tools,
);
}
sub gather_files($self) {
my @plugins = $self->zilla->plugins_with(-CycloneDXSource)->@*;
my $metadata = make_metadata($self->zilla->distmeta, \@plugins);
my $bom = SBOM::CycloneDX->new(metadata => $metadata);
for my $plugin (@plugins) {
$plugin->add_to_bom($bom);
}
local $SIG{__WARN__} = sub { warn $_[0] unless $_[0] =~ /Net::IDN::Encode/ };
if (my @errors = $bom->validate) {
croak "Errors found in BOM: @errors";
}
$self->add_file(Dist::Zilla::File::InMemory->new(
name => $self->filename,
encoded_content => $bom->to_string,
));
return;
}
__PACKAGE__->meta->make_immutable;
1;
# ABSTRACT: CycloneDX files in dzil generated dists
__END__
=pod
=encoding UTF-8
=head1 NAME
Dist::Zilla::Plugin::CycloneDX - CycloneDX files in dzil generated dists
=head1 VERSION
version 0.001
=head1 DESCRIPTION
This module adds a CycloneDX SBOM file to your distribution. By default this will only contain information that's already contained in the META files, but this can be extended by other plugins that implement the L<CycloneDXSource|Dist::Zilla::Role::C...
=head1 AUTHOR
Leon Timmermans <fawaka@gmail.com>
=head1 COPYRIGHT AND LICENSE
This software is copyright (c) 2026 by Leon Timmermans.
This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.
=cut
( run in 0.749 second using v1.01-cache-2.11-cpan-9581c071862 )