File Coverage

lib/CPAN/Audit.pm
Criterion Covered Total %
statement 128 187 68.4
branch 32 70 45.7
condition 18 31 58.0
subroutine 22 25 88.0
pod 0 9 0.0
total 200 322 62.1


line stmt bran cond sub pod time code
1             package CPAN::Audit;
2 13     13   10581450 use v5.10.1;
  13         47  
3 13     13   71 use strict;
  13         19  
  13         396  
4 13     13   56 use warnings;
  13         28  
  13         670  
5 13     13   4724 use version;
  13         21416  
  13         114  
6              
7 13     13   1185 use Carp qw(carp);
  13         22  
  13         748  
8 13     13   60765 use Module::CoreList;
  13         2887821  
  13         168  
9              
10 13     13   37135 use CPAN::Audit::Installed;
  13         38  
  13         554  
11 13     13   6111 use CPAN::Audit::Discover;
  13         49  
  13         608  
12 13     13   5573 use CPAN::Audit::Filter;
  13         70  
  13         509  
13 13     13   5967 use CPAN::Audit::Version;
  13         41  
  13         532  
14 13     13   6424 use CPAN::Audit::Query;
  13         40  
  13         552  
15 13     13   143186 use CPANSA::DB;
  13         55732276  
  13         34340  
16              
17             our $VERSION = '20260308.002';
18              
19             sub new {
20 8     8 0 61 my( $class, %params ) = @_;
21              
22 8         64 my @allowed_keys = qw(ascii db exclude exclude_file include_perl interactive no_corelist quiet verbose version);
23              
24 8         25 my %args = map { $_, $params{$_} } @allowed_keys;
  80         181  
25 8         36 my $self = bless \%args, $class;
26              
27 8 50       66 $self->_handle_exclude_file if $self->{exclude_file};
28              
29 8   33     90 $self->{db} //= $self->_get_db(%args);
30              
31 8         408187 $self->{filter} = CPAN::Audit::Filter->new( exclude => $args{exclude} );
32 8         82 $self->{query} = CPAN::Audit::Query->new( db => $self->{db} );
33 8         86 $self->{discover} = CPAN::Audit::Discover->new( db => $self->{db} );
34              
35 8         85 return $self;
36             }
37              
38             sub _get_db {
39 8     8   43 my( $self, %params ) = @_;
40              
41 8 50       51 if ( $params{'json_db'} ) {
42 0         0 my $data = do {
43 0         0 local $/;
44 0 0       0 open my($fh), '<:raw', $params{'json_db'}
45             or die "could not read file <$params{json_db}>\n";
46 0         0 <$fh>;
47             };
48 0         0 state $rc = require JSON;
49              
50 0         0 my $decoded = eval { JSON::decode_json($data) };
  0         0  
51 0 0       0 die "could not decode JSON from <$params{json_db}>: @_\n" unless defined $decoded;
52 0         0 return $decoded;
53             }
54              
55 8         21 my $rc = eval { require CPANSA::DB };
  8         88  
56 8 50       55 if ( $rc ) {
57 8         81 return CPANSA::DB->db;
58             }
59              
60 0         0 $rc = eval {
61 0         0 warn "CPAN::Audit::DB is deprecated. Use CPANSA::DB instead.\n";
62             require CPAN::Audit::DB
63 0         0 };
64 0 0       0 if ( $rc ) {
65 0         0 return CPAN::Audit::DB->db;
66             }
67              
68 0         0 die "could not find a CPANSA database in CPANSA::DB or CPAN::Audit::DB\n";
69             }
70              
71             sub _handle_exclude_file {
72 0     0   0 my( $self ) = @_;
73              
74 0         0 foreach my $file (@{$self->{exclude_file}}) {
  0         0  
75 0         0 my $fh;
76 0 0       0 unless( open $fh, "<", $file ) {
77 0         0 carp "unable to open exclude_file [$file]: $!\n";
78 0         0 return;
79             }
80             my @excludes =
81 0         0 grep { !/^\s*$/ } # no blank lines
82 0         0 map { s{^\s+|\s+$}{}g; $_ } # strip leading/trailing whitespace
  0         0  
83 0         0 map { s{#.*}{}; $_ } # strip comments
  0         0  
  0         0  
84             <$fh>;
85 0         0 push @{$self->{exclude}}, @excludes;
  0         0  
86             }
87             }
88              
89             sub command_module {
90 5     5 0 16 my ( $self, $dists, $queried, $module, $version_range ) = @_;
91 5 50       17 return "Usage: module [version-range]" unless $module;
92              
93 5         18 my $distname = $self->{db}->{module2dist}->{$module};
94              
95 5 50       13 if ( !$distname ) {
96 0         0 return "Module '$module' is not in database";
97             }
98              
99 5         8 push @{ $queried->{$distname} }, $module;
  5         17  
100 5   100     21 $dists->{$distname} = $version_range // '';
101              
102 5         13 return;
103             }
104              
105             sub command_release {
106 0     0 0 0 my ( $self, $dists, $queried, $distname, $version_range ) = @_;
107 0 0       0 return "Usage: dist|release [version-range]"
108             unless $distname;
109              
110 0 0       0 if ( !$self->{db}->{dists}->{$distname} ) {
111 0         0 return "Distribution '$distname' is not in database";
112             }
113              
114 0   0     0 $dists->{$distname} = $version_range // '';
115              
116 0         0 return;
117             }
118              
119             sub command_show {
120 0     0 0 0 my ( $self, $dists, $queried, $advisory_id ) = @_;
121 0 0       0 return "Usage: show " unless $advisory_id;
122              
123 0         0 my ($release) = $advisory_id =~ m/^CPANSA-(.*?)-(\d+)-(\d+)$/;
124 0 0       0 return "Invalid advisory id" unless $release;
125              
126 0         0 my $dist = $self->{db}->{dists}->{$release};
127 0 0       0 return "Unknown advisory id" unless $dist;
128              
129             my ($advisory) =
130 0         0 grep { $_->{id} eq $advisory_id } @{ $dist->{advisories} };
  0         0  
  0         0  
131 0 0       0 return "Unknown advisory id" unless $advisory;
132              
133 0   0     0 my $distname = $advisory->{distribution} // 'Unknown distribution name';
134 0         0 $dists->{$distname}{advisories} = [ $advisory ];
135 0         0 $dists->{$distname}{version} = 'Any';
136              
137 0         0 return;
138             }
139              
140             sub command_modules {
141 1     1 0 7 my ($self, $dists, $queried, @modules) = @_;
142 1 50       4 return "Usage: modules '[;version-range]' '[;version-range]'" unless @modules;
143              
144 1         3 foreach my $module ( @modules ) {
145 3         11 my ($name, $version) = split /;/, $module;
146              
147 3   50     29 my $failed = $self->command_module( $dists, $queried, $name, $version // '' );
148              
149 3 50       10 if ( $failed ) {
150 0         0 $self->verbose( $failed );
151 0         0 next;
152             }
153             }
154              
155 1         3 return;
156             }
157              
158             sub command_deps {
159 3     3 0 10 my ($self, $dists, $queried, $dir) = @_;
160 3 50       16 $dir = '.' unless defined $dir;
161              
162 3 50       117 return "Usage: deps " unless -d $dir;
163              
164 3         28 my @deps = $self->{discover}->discover($dir);
165              
166 3         28 $self->verbose( sprintf 'Discovered %d dependencies', scalar(@deps) );
167              
168 3         7 foreach my $dep (@deps) {
169             my $dist = $dep->{dist}
170 5   33     33 || $self->{db}->{module2dist}->{ $dep->{module} };
171 5 50       14 next unless $dist;
172              
173 5 50       16 push @{ $queried->{$dist} }, $dep->{module} if !$dep->{dist};
  5         28  
174              
175 5         15 $dists->{$dist} = $dep->{version};
176             }
177              
178 3         47 return;
179             }
180              
181             sub command_installed {
182 2     2 0 4 my ($self, $dists, $queried, @args) = @_;
183              
184 2         10 $self->verbose('Collecting all installed modules. This can take a while...');
185              
186             my $verbose_callback = sub {
187 44     44   127 my ($info) = @_;
188 44         345 $self->verbose( sprintf '%s: %s-%s', $info->{path}, $info->{distname}, $info->{version} );
189 2         10 };
190              
191             my @deps = CPAN::Audit::Installed->new(
192             db => $self->{db},
193             include_perl => $self->{include_perl},
194 2 100       20 ( $self->{verbose} ? ( cb => $verbose_callback ) : () ),
195             )->find(@args);
196              
197 2         30 foreach my $dep (@deps) {
198             my $dist = $dep->{dist}
199 88   33     209 || $self->{db}->{module2dist}->{ $dep->{module} };
200 88 50       121 next unless $dist;
201              
202 88         289 $dists->{ $dep->{dist} } = '==' . $dep->{version};
203             }
204              
205 2         53 return;
206             }
207              
208             sub command {
209 8     8 0 87 state $command_table = {
210             dependencies => 'command_deps',
211             deps => 'command_deps',
212             installed => 'command_installed',
213             module => 'command_module',
214             modules => 'command_modules',
215             release => 'command_release',
216             dist => 'command_release',
217             show => 'command_show',
218             };
219              
220 8         36 my( $self, $command, @args ) = @_;
221              
222 8         131 my %report = (
223             meta => {
224             command => $command,
225             args => [ @args ],
226             cpan_audit => {
227             version => $VERSION,
228             db => $CPANSA::DB::VERSION,
229             },
230             total_advisories => 0,
231             },
232             errors => [],
233             dists => {},
234             );
235 8         25 my $dists = $report{dists};
236 8         16 my $queried = {};
237              
238 8 100 66     125 if (!$self->{no_corelist}
      66        
239             && ( $command eq 'dependencies'
240             || $command eq 'deps'
241             || $command eq 'installed' )
242             )
243             {
244             # Find core modules for this perl version first.
245             # This way explictly installed versions will overwrite.
246 2 50       19 if ( my $core = $Module::CoreList::version{$]} ) {
247 2         14 while ( my ( $mod, $ver ) = each %$core ) {
248 1566 100       4790 my $dist = $self->{db}{module2dist}{$mod} or next;
249 604 100 100     4969 $dists->{$dist} = $ver if( ! defined $dists->{$dist} or version->parse($ver) > $dists->{$dist} );
250             }
251             }
252             }
253              
254 8 50       40 if ( exists $command_table->{$command} ) {
255 8         23 my $method = $command_table->{$command};
256 8         16 push @{ $report{errors} }, $self->$method( $dists, $queried, @args );
  8         85  
257 8 50       53 return \%report if $command eq 'show';
258             }
259             else {
260 0         0 push @{ $report{errors} }, "unknown command: $command. See -h";
  0         0  
261             }
262              
263 8 50       27 if (%$dists) {
264 8         24 my $query = $self->{query};
265              
266 8         48 foreach my $distname ( keys %$dists ) {
267 150         497 my $version_range = $dists->{$distname};
268             my @advisories =
269 150         547 grep { ! $self->{filter}->excludes($_) }
  68         320  
270             $query->advisories_for( $distname, $version_range );
271              
272 150 100 100     951 $version_range = 'Any'
273             if $version_range eq '' || $version_range eq '0';
274              
275 150         461 $report{meta}{total_advisories} += @advisories;
276              
277 150 100       401 if ( @advisories ) {
278             $dists->{$distname} = {
279             advisories => \@advisories,
280             version => $version_range,
281 20   100     423 queried_modules => $queried->{$distname} || [],
282             };
283             }
284             else {
285 130         490 delete $dists->{$distname}
286             }
287             }
288             }
289              
290 8         178 return \%report;
291             }
292              
293             sub verbose {
294 49     49 0 151 my ( $self, $message ) = @_;
295 49 100       185 return if $self->{quiet};
296 46         169 $self->_print( *STDERR, $message );
297             }
298              
299              
300             sub _print {
301 46     46   231 my ( $self, $fh, $message ) = @_;
302              
303 46 50       230 if ( $self->{no_color} ) {
304 0         0 $message =~ s{__BOLD__}{}g;
305 0         0 $message =~ s{__GREEN__}{}g;
306 0         0 $message =~ s{__RED__}{}g;
307 0         0 $message =~ s{__RESET__}{}g;
308             }
309             else {
310 46         148 $message =~ s{__BOLD__}{\e[39;1m}g;
311 46         115 $message =~ s{__GREEN__}{\e[32m}g;
312 46         98 $message =~ s{__RED__}{\e[31m}g;
313 46         94 $message =~ s{__RESET__}{\e[0m}g;
314              
315 46 50       155 $message .= "\e[0m" if length $message;
316             }
317              
318 46         5373 print $fh "$message\n";
319             }
320              
321             1;
322             __END__