Developer-Dashboard

 view release on metacpan or  search on metacpan

script/cpan-audit-declared-chain  view on Meta::CPAN

#!/usr/bin/env perl

# IMPORTANT: this gate answers a different question from script/cpan-audit-project.
# That gate asks "is the set of distributions installed in this root vulnerable?".
# This gate asks "does the declared dependency chain PERMIT a vulnerable
# resolution?" - which is what an installer actually decides. A resolver always
# takes the newest release, so an installed-only audit stays green even while
# the declared floors allow a vulnerable version, and a floor list derived from
# the modules cpanfile names never sees the transitive requirements that
# libwww-perl, Dancer2 and JSON::MaybeXS pull in on their own.

use strict;
use warnings;

use File::Basename qw(basename dirname);
use File::Spec;
use Getopt::Long qw(GetOptions);
use JSON::XS ();

use constant EXIT_CLEAN   => 0;
use constant EXIT_FINDING => 1;
use constant EXIT_UNUSABLE => 2;

exit main(@ARGV);

# Purpose: run the declared-chain advisory gate end to end.
# Input: the raw @ARGV list (one positional Perl library root, plus the
#        optional --cpanfile and --exclude-file overrides).
# Output: an exit code - 0 clean, 1 at least one permitted vulnerable
#         resolution, 2 the gate could not audit anything and refuses to
#         report a clean result it did not establish.
sub main {
    my @argv = @_;

    my $repo_root = File::Spec->rel2abs( File::Spec->catdir( dirname(__FILE__), File::Spec->updir ) );
    my $cpanfile  = File::Spec->catfile( $repo_root, 'cpanfile' );
    my $exclusions = File::Spec->catfile( $repo_root, 'cpan-audit-exclusions.txt' );

    local @ARGV = @argv;
    GetOptions(
        'cpanfile=s'     => \$cpanfile,
        'exclude-file=s' => \$exclusions,
    ) or return _usage('unrecognized option');
    my @positional = @ARGV;

    return _usage('exactly one Perl library root is required')
        if @positional != 1;
    my $perl5_root = $positional[0];
    return _usage("not a directory: $perl5_root") if !-d $perl5_root;
    return _usage("cpanfile not readable: $cpanfile") if !-f $cpanfile;

    my $version_class = _load_audit_modules();
    return _unusable('CPAN::Audit::DB and CPAN::Audit::Version must be loadable; install CPAN::Audit and put it on PERL5LIB')
        if !$version_class;

    my $declared = _parse_cpanfile($cpanfile);
    return _unusable("no runtime requirements declared in $cpanfile")
        if !%{$declared};

    my $metadata = _index_metadata($perl5_root);
    return _unusable(
        sprintf 'the closure would be incomplete - %d distribution metadata file(s) could not be read: %s',
        scalar @{ $metadata->{unusable} },
        join( '; ', @{ $metadata->{unusable} } )
    ) if @{ $metadata->{unusable} };
    return _unusable("no distribution metadata (.meta) found under $perl5_root; the declared chain cannot be walked")
        if !%{ $metadata->{module_to_dist} };

    my $demand = _closure( $declared, $metadata );
    my $floors = _distribution_floors( $demand, $metadata );
    my $excluded = _read_exclusions($exclusions);

    my @findings = _findings( $floors, $excluded, $version_class );

    printf "declared-chain closure: %d modules across %d distributions under %s\n",
        scalar( keys %{$demand} ), scalar( keys %{$floors} ), $perl5_root;

    if ( !@findings ) {
        print "No distribution in the declared runtime closure permits a version inside an advisory range.\n";
        return EXIT_CLEAN;
    }

    for my $finding ( @findings ) {
        printf "%s permits %s which has advisory %s\n",
            $finding->{distribution}, $finding->{permitted}, $finding->{advisory};
        printf "    affected range: %s\n", $finding->{affected};
        printf "    fixed range:    %s\n", ( $finding->{fixed} eq '' ? '(no fixed release)' : $finding->{fixed} );
        printf "    floor demanded: %s (%s)\n", $finding->{floor}, $finding->{because};
        printf "    remedy:         declare a floor for %s at or above the fixed range in cpanfile, Makefile.PL and dist.ini, or record a reviewed disposition for %s\n",
            $finding->{main_module}, $finding->{advisory};
    }
    printf "%d permitted vulnerable resolution(s) in the declared chain\n", scalar(@findings);

    return EXIT_FINDING;
}

# Purpose: print the usage diagnostic for a caller error.
# Input: a one-line reason string.
# Output: the EXIT_UNUSABLE exit code (the message goes to STDERR).



( run in 1.303 second using v1.01-cache-2.11-cpan-5c0b1e786e0 )