File Coverage

script/cpan-audit
Criterion Covered Total %
statement 132 154 85.7
branch 30 52 57.6
condition 8 16 50.0
subroutine 21 23 91.3
pod n/a
total 191 245 77.9


line stmt bran cond sub pod time code
1             #!/usr/bin/env perl
2 11     11   57037 use v5.10;
  11         41  
3 11     11   58 use strict;
  11         15  
  11         307  
4 11     11   78 use warnings;
  11         15  
  11         620  
5 11     11   5865 use open qw(:std :encoding(UTF-8));
  11         16781  
  11         67  
6              
7 11     11   264701 use IO::Interactive qw(is_interactive);
  11         96047  
  11         68  
8              
9 11     11   6197 use CPAN::Audit;
  11         55  
  11         1012  
10              
11 11         1655463 our $VERSION = "1.503";
12              
13 11 50       223 __PACKAGE__->run( @ARGV ) unless caller;
14              
15             # The exit code indicates the number of advisories, up to this max
16             # since we have a limited number of exit codes.
17 11     11   148 use constant ADVISORY_COUNT_MAX => 62;
  11         18  
  11         1137  
18              
19 11     11   64 use constant EXIT_NORMAL => 0;
  11         20  
  11         554  
20 11     11   60 use constant EXIT_ZERO => 0;
  11         22  
  11         487  
21 11     11   61 use constant EXIT_USAGE => 2;
  11         17  
  11         438  
22 11     11   47 use constant EXIT_BASE => 64;
  11         18  
  11         981  
23              
24 0         0 my $output_table;
25             BEGIN {
26 11     11   5301 $output_table = {
27             text => \&format_text,
28             dumper => \&format_dump,
29             json => \&format_json,
30             default => \&format_text,
31             };
32             }
33              
34             sub format_advisory {
35 8     8   17 my ($advisory) = @_;
36 8         31 my $s = " __BOLD__* $advisory->{id}__RESET__\n";
37 8         25 $s .= " $advisory->{description}\n";
38              
39 8 50       27 if ( $advisory->{affected_versions} ) {
40 8 50       24 my @v = ref $advisory->{affected_versions} ? @{$advisory->{affected_versions}} : $advisory->{affected_versions};
  8         27  
41 8         16 my $first = shift @v;
42 8         46 $s .= " Affected range: $first\n";
43 8         25 $s .= " $_\n" for @v;
44             }
45              
46 8 50       25 if ( $advisory->{fixed_versions} ) {
47 8 50       25 my @v = ref $advisory->{fixed_versions} ? @{$advisory->{fixed_versions}} : $advisory->{fixed_versions};
  8         22  
48 8         17 my $first = shift @v;
49 8   100     41 $first //= '';
50 8         13 $s .= " Fixed range: $first\n";
51 8         19 $s .= " $_\n" for @v;
52             }
53              
54 8 50       55 if ( $advisory->{cves} ) {
55 8         18 $s .= "\n CVEs: ";
56 8         13 $s .= join ', ', @{ $advisory->{cves} };
  8         25  
57 8         14 $s .= "\n";
58             }
59              
60 8 50       26 if ( $advisory->{references} ) {
61 8         12 $s .= "\n References:\n";
62 8 50       16 foreach my $reference ( @{ $advisory->{references} || [] } ) {
  8         31  
63 26         55 $s .= " $reference\n";
64             }
65             }
66              
67 8         14 $s .= "\n";
68 8         39 return $s;
69             }
70              
71 11     11   10090 use Data::Dumper;
  11         93401  
  11         18297  
72 0     0   0 sub dumper { Data::Dumper->new([@_])->Indent(1)->Sortkeys(1)->Terse(1)->Useqq(1)->Dump }
73             sub format_dump {
74 0     0   0 my( $result ) = @_;
75 0         0 return dumper($result);
76             }
77              
78             sub format_json {
79 6     6   4940 state $rc = require JSON;
80 6         63587 my( $result ) = @_;
81 6         655 return JSON::encode_json($result);
82             }
83              
84             sub format_text {
85 2     2   7 my( $result, $opts ) = @_;
86 2         6 my $s = '';
87              
88 2         5 foreach my $distname ( keys %{ $result->{dists} } ) {
  2         16  
89 8         29 my $advisories = $result->{dists}{$distname}{advisories};
90             $s .= sprintf("__RED__%s (%s %s) has %d advisor%s__RESET__\n",
91             $distname,
92             ($result->{meta}{command} eq 'installed' ? 'have' : 'requires'),
93             $result->{dists}{$distname}{version},
94 8 50       110 scalar(@$advisories),
    50          
95             (scalar(@$advisories) == 1 ? 'y' : 'ies'),
96             );
97              
98 8         19 foreach my $advisory ( @$advisories ) {
99 8         26 $s .= format_advisory( $advisory );
100             }
101             }
102              
103 2 50       14 $s .= "\n" if length $s;
104              
105 2 50 33     29 if ( $opts->{'no-color'} or $opts->{'ascii'} ) {
106 0         0 $s =~ s{__BOLD__}{}g;
107 0         0 $s =~ s{__GREEN__}{}g;
108 0         0 $s =~ s{__RED__}{}g;
109 0         0 $s =~ s{__RESET__}{}g;
110             }
111             else {
112 2         33 $s =~ s{__BOLD__}{\e[39;1m}g;
113 2         10 $s =~ s{__GREEN__}{\e[32m}g;
114 2         22 $s =~ s{__RED__}{\e[31m}g;
115 2         28 $s =~ s{__RESET__}{\e[0m}g;
116              
117 2 50       12 $s .= "\e[0m" if length $s;
118             }
119              
120 2         11 return $s;
121             }
122              
123             sub output_version {
124 1     1   2 my( $class, $exit_code ) = @_;
125              
126 1         1 print <<"HERE";
127             $0 version $VERSION using:
128 1         15 \tCPAN::Audit @{[ CPAN::Audit->VERSION ]}
129 1   50     12 \tCPAN::Audit::DB @{[ CPAN::Audit::DB->VERSION // '' ]} (deprecated)
130 1   33     2 \tCPANSA::DB @{[ ( eval { require CPANSA::DB } && CPANSA::DB->VERSION) // '' ]}
      50        
131              
132             HERE
133              
134 1         105 exit($exit_code);
135             }
136              
137             sub run {
138 11     11   177 my( $class, @args ) = @_;
139              
140 11         74 my( $opts ) = $class->process_options( \@args );
141 11 50       51 unless( ! $opts->{interactive} ) {
142 0         0 $opts->{ascii} = 1;
143 0   0     0 $opts->{no_color} //= 1;
144             }
145              
146 11 100       52 $class->usage(EXIT_NORMAL) if $opts->{help};
147 10 100       48 $class->output_version(EXIT_NORMAL) if $opts->{version};
148              
149 9 50       43 if( $opts->{fresh_check} ) {
150 0         0 require CPAN::Audit::FreshnessCheck;
151 0         0 CPAN::Audit::FreshnessCheck->import
152             }
153              
154 9         24 my $command = shift @args;
155 9 100       36 $class->usage(EXIT_USAGE) unless defined $command;
156              
157 8         67 my %extra = (
158             interactive => is_interactive(),
159             );
160              
161 8         279 my $audit = CPAN::Audit->new( %$opts, %extra );
162              
163 8         86 my $result = $audit->command( $command, @args );
164              
165 8 50       24 if( @{ $result->{errors} } > 0 ) {
  8         53  
166 0         0 my $message = join "\n", map "Error: $_", @{ $result->{errors} };
  0         0  
167 0 0       0 unless( $opts->{'no-color'} ) {
168 0         0 $message = "\e[31m" . $message . "\e[0m"
169             }
170 0         0 print STDERR $message;
171 0         0 exit 255;
172             }
173              
174 8         24 my( $output_type ) = grep { $opts->{$_} } qw(json);
  8         50  
175 8   100     169 my $sub = $output_table->{$output_type // 'default'};
176              
177 8         59 my $output = $sub->( $result, $opts );
178 8 50       58 if( $command eq 'show' ) {
179 0         0 $output =~ s/\A.*\n//;
180             }
181              
182 8         1314 print $output;
183              
184 8         44 my $advisory_count = $result->{meta}{total_advisories};
185 8 50       71 $advisory_count = ADVISORY_COUNT_MAX if $advisory_count > ADVISORY_COUNT_MAX;
186              
187 8         15 my $exit_code = do {
188 8 100       64 if( $opts->{exit_zero} ) { EXIT_ZERO }
  1 50       4  
189 0         0 elsif( $advisory_count == 0 ) { EXIT_NORMAL }
190 7         22 else { EXIT_BASE + $advisory_count }
191             };
192              
193 8         141825 exit( $exit_code );
194             }
195              
196             sub process_options {
197 11     11   59 my( $class, $args ) = @_;
198 11         10410 require Getopt::Long;
199              
200 11         161034 my $options = {};
201              
202 11         33 my %params = ();
203             my $params = {
204             'ascii' => \$params{ascii},
205             'f|fresh' => \$params{fresh_check},
206             'help|h' => \$params{help},
207             'json' => \$params{json},
208             'no-color' => \$params{no_color},
209             'no-corelist' => \$params{no_corelist},
210             'perl' => \$params{include_perl},
211             'quiet|q' => \$params{quiet},
212             'verbose|v' => \$params{verbose},
213             'version' => \$params{version},
214             'exclude=s@' => \$params{exclude},
215             'exclude-file=s@' => \$params{exclude_file},
216             'modules=s@' => \$params{modules},
217             'exit-zero' => \$params{exit_zero},
218 11         351 };
219              
220 11 50       85 my $ret = Getopt::Long::GetOptionsFromArray( $args, $options, %$params )
221             or $class->usage(EXIT_USAGE);
222              
223 11 100       18278 $params{quiet} = 1 if $params{json};
224              
225 11         90 \%params;
226             }
227              
228             sub usage {
229 2     2   1457 require Pod::Usage;
230 2         132612 require FindBin;
231              
232 2         3216 my( $class, $exit_code ) = @_;
233 11     11   119 no warnings qw(once);
  11         18  
  11         23878  
234 2         17 Pod::Usage::pod2usage( -input => $FindBin::Bin . "/" . $FindBin::Script );
235 0           print <<'HERE';
236             NAME
237             cpan-audit - Audit CPAN modules
238              
239             SYNOPSIS
240             cpan-audit [command] [options]
241              
242             Commands:
243              
244             module [version range] audit module with optional version range (all by default)
245             modules [version range] audit module list with optional version range (all by default)
246             dist|release [version range] audit distribution with optional version range (all by default)
247             deps [directory] audit dependencies from the directory (. by default)
248             installed audit all installed modules
249             show [advisory id] show information about specific advisory
250              
251             Options:
252              
253             --ascii use ascii output
254             --fresh|f check the database for freshness (CPAN::Audit::FreshnessCheck)
255             --help|h show the help message and exit
256             --no-color switch off colors
257             --no-corelist ignore modules bundled with perl version
258             --perl include perl advisories
259             --quiet be quiet (overrules --verbose)
260             --verbose be verbose (off if --quiet in effect)
261             --version show the version and exit
262             --exit-zero always exit with 0 even if advisories are reported
263             --exclude exclude/ignore the specified advisory/cve (multiple)
264             --exclude-file read exclude/ignore patterns from file
265             --json output JSON
266              
267             Examples:
268              
269             cpan-audit dist Catalyst-Runtime
270             cpan-audit dist Catalyst-Runtime 7.0
271             cpan-audit dist Catalyst-Runtime '>5.48'
272              
273             cpan-audit module Catalyst 7.0
274              
275             cpan-audit modules "Catalyst;7.0" "Mojolicious;>8.40,<9.20"
276              
277             cpan-audit deps .
278             cpan-audit deps /path/to/distribution
279              
280             cpan-audit installed
281             cpan-audit installed local/
282             cpan-audit installed local/ --exclude CVE-2011-4116
283             cpan-audit installed local/ --exclude CVE-2011-4116 --exclude CVE-2011-123
284             cpan-audit installed local/ --exclude-file ignored-cves.txt
285              
286             cpan-audit installed --json
287              
288             cpan-audit installed --json --exit-zero
289              
290             cpan-audit show CPANSA-Mojolicious-2018-03
291              
292             DESCRIPTION
293             "cpan-audit" is a command line application that checks the modules or
294             distributions for known vulnerabilities. It is using its internal
295             database that is automatically generated from a hand-picked database
296             .
297              
298             "cpan-audit" does not connect to anything, that is why it is important
299             to keep it up to date. Every update of the internal database is released
300             as a new version. Ensure that you have the latest database by updating
301             CPAN::Audit frequently; the database can change daily. You can use
302             enable a warning for a possibly out-of-date database by adding
303             "--fresh", which warns if the database version is older than a month:
304              
305             % cpan-audit --fresh ...
306             % cpan-audit -f ...
307              
308             % env CPAN_AUDIT_FRESH_DAYS=7 cpan-audit -f ...
309              
310             Finding dependencies
311             "cpan-audit" can automatically detect dependencies from the following
312             sources:
313              
314             "Carton"
315             Parses cpanfile.snapshot file and checks the distribution versions.
316              
317             cpanfile
318             Parses cpanfile taking into account the required versions.
319              
320             It is assumed that if the required version of the module is less than a
321             version of a release with a known vulnerability fix, then the module is
322             considered affected.
323              
324             JSON data
325             If you request JSON output, the data looks like
326              
327             {
328             "meta" : {
329             ... meta information ...
330             "dists": {
331             "": {
332             ... distribution info ...
333             }
334             }
335             "errors" : [
336             ... list of errors - if any ...
337             ]
338             }
339              
340             Meta information
341             The meta data contains information about the run of "cpan-audit".
342              
343             {
344             "args": [
345             "Mojo::File",
346             "Mojo::UserAgent",
347             "LWP::UserAgent"
348             ],
349             "cpan_audit": {
350             "version": "20230601.002"
351             },
352             "total_advisories": 19,
353             "command": "modules"
354             }
355              
356             These information are shown
357              
358             * cpan_audit
359              
360             The version of "cpan_audit" that is used for the audit
361              
362             * command
363              
364             The command of "cpan_audit" that was run
365              
366             * args
367              
368             Arguments for the command
369              
370             * total_advisories
371              
372             Number of found advisories
373              
374             Distribution information
375             For each distribution where at least one advisory was found, the JSON
376             looks like:
377              
378             "Dist-Name": {
379             "queried_modules": [
380             "Queried::Namespace"
381             ],
382             "version": "Any",
383             "advisories": [
384             {
385             ... advisory data as in the audit database ...
386             },
387             ... more advisories ...
388             ]
389             },
390              
391             The advisory data is basically the data from the database. So this
392             depends on what is known for the given advisory.
393              
394             The distribution information contains:
395              
396             * version
397              
398             The version (range) that is checked for advisories. If there's no
399             version specified, all versions are checked and the version is
400             report as "Any".
401              
402             * queried_modules
403              
404             The actual namespaces queried, either from the command line or
405             another source, such as a cpanfile.
406              
407             * advisories
408              
409             A list of all vulnerabilities found for the version range
410              
411             Exit values
412             In prior versions, "cpan-audit" exited with the number of advisories it
413             found. Starting with 1.001, if there are advisories found, "cpan-audit"
414             exits with 64 added to that number. The maximum number of reported
415             advisories is 62, since values over 126 are spoken for.
416              
417             If the option "--exit-zero" is set "cpan-audit" exits always with a
418             normal exit code (0). This allows to use "cpan-audit" in build
419             environments together with bash exit mode activated ("set -e").
420              
421             * 0 - no advisories found
422              
423             * 2 - problem with program invocation, such as bad switches or values
424              
425             * 64+n - advisories found. Subtract 64 to get the advisory count, up
426             to 62 advisories
427              
428             * 255 - unspecified program error
429              
430             LICENSE
431             Copyright (C) Viacheslav Tykhanovskyi.
432              
433             This library is free software; you can redistribute it and/or modify it
434             under the same terms as Perl itself.HERE
435             HERE
436              
437 0           exit( $exit_code );
438             }
439              
440             __END__