App-CPAN-SBOM

 view release on metacpan or  search on metacpan

lib/App/CPAN/SBOM.pm  view on Meta::CPAN

use SBOM::CycloneDX;

our $VERSION = '1.04';


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 {

    my (@args) = @_;

    my %options = ();

    GetOptionsFromArray(

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;

lib/App/CPAN/SBOM.pm  view on Meta::CPAN


    if (defined $options{'project-directory'} || defined $options{'project-meta'}) {
        make_sbom_from_project(bom => $bom, options => \%options);
    }

    $bom->metadata->timestamp(time);
    $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);
    }

    if (defined $options{'server-url'} && defined $options{'api-key'}) {
        submit_bom(bom => $bom, options => \%options);
    }

}

sub show_version {

    (my $progname = $0) =~ s/.*\///;

    say <<"VERSION";
$progname version $VERSION

Copyright 2025-2026, 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

lib/App/CPAN/SBOM.pm  view on Meta::CPAN


    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


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

    my $maxdepth         = $params{maxdepth}  || 1;
    my $add_vulns        = $params{add_vulns} || 0;

    my $mcpan = MetaCPAN::Client->new;

    if ($module) {

        $module = 'perl' if ($module eq 'Perl');

        DEBUG
            and say STDERR sprintf '-- %s[%d] Collect module %s@%s info (parent component %s)',
            ("    " x ($depth - 1)), $depth, $module, $version, $parent_component->bom_ref;

        my $module_data = $mcpan->module($module);

        unless ($module_data) {
            Carp::carp("Unable to find module ($module) in Meta::CPAN");
            return;
        }

        $author //= $module_data->author;

lib/App/CPAN/SBOM.pm  view on Meta::CPAN

    my $release_data = $mcpan->release({
        either => [
            {all => [{distribution => $distribution}, {version => $version}]},
            {all => [{distribution => $distribution}, {version => "v$version"}]},
        ]
    });

    my $dist_data = $release_data->next;

    DEBUG
        and say STDERR sprintf '-- %s[%d] Collect distribution %s@%s info (parent component %s)',
        ("    " x ($depth - 1)), $depth, $distribution, $version, $parent_component->bom_ref;

    unless ($dist_data) {
        Carp::carp("Unable to find release ($distribution\@$version) in Meta::CPAN");
        return;
    }

    my $metadata = $dist_data->metadata;

    $author //= $dist_data->author;

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}
        ));
    }

}



( run in 1.418 second using v1.01-cache-2.11-cpan-d7f47b0818f )