#!/usr/bin/perl
use
5.014000;
our
$VERSION
=
"0.09"
;
sub
new {
my
$class
=
shift
;
@_
% 2 and croak
"Uneven number of arguments"
;
my
%self
=
@_
;
$self
{deps} //= 1;
$self
{perl} //= 1;
$self
{core} //= 1;
$self
{minimum} //= 0;
$self
{verbose} //= 0;
$self
{width} //=
$ENV
{COLUMNS} // 80;
$self
{want} //= [];
$self
{cpanfile} ||=
"cpanfile"
;
$self
{meta_jsn} ||=
"META.json"
;
$self
{meta_yml} ||=
"META.yml"
;
$self
{make_pl} ||=
"Makefile.PL"
;
$self
{build_pl} ||=
"Build.PL"
;
$self
{CVE} = {};
my
$obj
=
bless
\
%self
=>
$class
;
$obj
->skip (
$self
{skip} //
"CVE.SKIP"
);
return
$obj
;
}
sub
skip {
my
$self
=
shift
;
if
(
@_
) {
if
(
my
$skip
=
shift
) {
if
(
ref
$skip
eq
"HASH"
) {
$self
->{skip} =
$skip
;
}
elsif
(
ref
$skip
eq
"ARRAY"
) {
$self
->{skip} = {
map
{
$_
=> 1 }
@$skip
};
}
elsif
(
$skip
=~ m/^\x20-\xff]+$/ and
open
my
$fh
,
"<"
,
$skip
) {
my
%s
;
while
(<
$fh
>) {
s/[\s\r\n]+\z//;
m/^\s*(\w[-\w]+)(?:\s+(.*))?$/ or
next
;
$s
{$1} = $2 //
""
;
}
close
$fh
;
$self
->{skip} = {
%s
};
}
else
{
$self
->{skip} = {
map
{
$_
=> 1 }
grep
{ m/^\w[-\w]+$/ }
$skip
,
@_
};
}
}
else
{
$self
->{skip} =
undef
;
}
}
$self
->{skip} ||= {};
return
[
sort
keys
%{
$self
->{skip}} ];
}
sub
_read_cpansa {
my
$self
=
shift
;
my
$src
=
$self
->{cpansa} or croak
"No source for CVE database"
;
$self
->{verbose} and
warn
"Reading $src ...\n"
;
if
(-s
$src
) {
open
my
$fh
,
"<"
,
$src
or croak
"$src: $!\n"
;
local
$/;
$self
->{j}{db} = decode_json (<
$fh
>);
close
$fh
;
}
else
{
my
$r
= HTTP::Tiny->new (
verify_SSL
=> 1)->get (
$src
);
$r
->{success} or
die
"$src: $@\n"
;
$self
->{verbose} > 1 and
warn
"Got it. Decoding\n"
;
if
(
my
$c
=
$r
->{content}) {
$c
=~ s/^\s*([^{]+?)[\s\r\n]*\{/{/s and
warn
"$1\n"
;
$self
->{j}{db} = decode_json (
$c
);
}
else
{
$self
->{j}{db} =
undef
;
}
}
$self
->{j}{mod} = [
sort
keys
%{
$self
->{j}{db} // {}} ];
$self
;
}
sub
_read_MakefilePL {
my
(
$self
,
$mf
) =
@_
;
$mf
||=
$self
->{make_pl};
$self
->{verbose} and
warn
"Reading $mf ...\n"
;
open
my
$fh
,
"<"
,
$mf
or
return
$self
;
my
$mfc
=
do
{
local
$/; <
$fh
> };
close
$fh
;
$mfc
or
return
$self
;
my
(
$pv
,
$release
,
$nm
,
$v
,
$vf
) = (
""
);
foreach
my
$mfx
(
grep
{ m/=>/ }
map
{
split
m/\s*[;(){}]\s*/ }
map
{
split
m/\s*,(?!\s*=>)/ }
split
m/[,;]\s*(?:
$mfx
=~ s/[\s\r\n]+/ /g;
$mfx
=~ s/^\s+//;
$mfx
=~ s/^(['"])(.*?)\1/$2/;
my
$a
=
qr{\s* (?:,\s*)? => \s* (?|"([^""]*)"|'([^'']*)'|([-\w.]+))}
x;
$mfx
=~ m/^ VERSION
$a
/ix and
$v
//= $1;
$mfx
=~ m/^ VERSION_FROM
$a
/ix and
$vf
//= $1;
$mfx
=~ m/^ NAME
$a
/ix and
$nm
//= $1;
$mfx
=~ m/^ DISTNAME
$a
/ix and
$release
//= $1;
$mfx
=~ m/^ MIN_PERL_VERSION
$a
/ix and
$pv
||= $1;
}
unless
(
$release
||
$nm
) {
carp
"Cannot get either NAME or DISTNAME, so cowardly giving up\n"
;
return
$self
;
}
unless
(
$pv
) {
$mfc
=~ m/^\s*(?:
use
|
require
)\s+v?(5[.0-9]+)/m and
$pv
= $1;
}
$pv
=~ m/^5\.(\d+)\.(\d+)$/ and
$pv
=
sprintf
"5.%03d%03d"
, $1, $2;
$pv
=~ m/^5\.(\d{1,3})$/ and
$pv
=
sprintf
"5.%03d000"
, $1;
$release
//=
$nm
=~ s{-}{::}gr;
$release
eq
"."
&&
$nm
and
$release
=
$nm
=~ s{::}{-}gr;
if
(!
$v
&&
$vf
and
open
$fh
,
"<"
,
$vf
) {
warn
"Trying to fetch VERSION from $vf ...\n"
if
$self
->{verbose};
while
(<
$fh
>) {
m/\b VERSION \s* = \s* [
"']? ([^;'"
\s]+) /x or
next
;
$v
= $1;
last
;
}
close
$fh
;
}
unless
(
$v
) {
$mfc
=~ m/\$\s
*VERSION
\s*=\s*[
"']?(\S+?)['"
]?\s*;/ and
$v
= $1;
}
unless
(
$v
) {
carp
"Could not derive a VERSION from Makefile.PL\n"
;
carp
"Please tell me where I did wrong\n"
;
carp
"(ideally this should be done by a CORE module)\n"
;
}
$self
->{mf} = {
name
=>
$nm
,
version
=>
$v
,
release
=>
$release
,
mpv
=>
$pv
};
$self
->{verbose} and
warn
"Analysing for $release-"
,
$v
//
"?"
,
$pv
?
" for minimum perl $pv\n"
:
"\n"
;
$self
->{prereq}{
$release
}{v}{
$v
//
"-"
} =
"current"
;
$self
;
}
sub
_read_cpanfile {
my
(
$self
,
$cpf
) =
@_
;
$cpf
||=
$self
->{cpanfile};
-s
$cpf
or
return
;
$self
->{verbose} and
warn
"Reading $cpf ...\n"
;
open
my
$fh
,
"<"
,
$cpf
or croak
"$cpf: $!\n"
;
while
(<
$fh
>) {
my
(
$t
,
$m
,
$v
) = m{ \b
( requires | recommends | suggest ) \s+
[
"'] (\S+) ['"
]
(?: \s*(?:=>|,)\s* [
"'] (\S+) ['"
])?
}x or
next
;
$m
=~ s/::/-/g;
$self
->{prereq}{
$m
}{v}{
$v
//
""
} =
$t
;
$self
->{prereq}{
$m
}{
$t
} =
$v
;
if
(m/
my
$i
= $1;
$self
->{prereq}{
$m
}{i}{
$i
=~ s{[
"''"
]+}{}gr}++;
}
}
push
@{
$self
->{want}} =>
sort
grep
{
$self
->{j}{db}{
$_
} }
keys
%{
$self
->{prereq}};
$self
;
}
sub
_read_META {
my
(
$self
,
$mmf
) =
@_
;
$mmf
||= first {
length
&& -s }
$self
->{meta_jsn},
"META.json"
,
$self
->{meta_yml},
"META.yml"
,
"MYMETA.json"
,
"MYMETA.yml"
;
$mmf
&& -s
$mmf
or
return
;
$self
->{verbose} and
warn
"Reading $mmf ...\n"
;
open
my
$fh
,
"<"
,
$mmf
or croak
"$mmf: $!\n"
;
local
$/;
my
$j
;
if
(
$mmf
=~ m/\.yml$/) {
$self
->{meta_yml} =
$mmf
;
$j
= YAML::PP::Load (<
$fh
>);
$j
->{prereqs} //= {
configure
=> {
requires
=>
$j
->{configure_requires},
recommends
=>
$j
->{configure_recommends},
suggests
=>
$j
->{configure_suggests},
},
build
=> {
requires
=>
$j
->{build_requires},
recommends
=>
$j
->{build_recommends},
suggests
=>
$j
->{build_suggests},
},
test
=> {
requires
=>
$j
->{test_requires},
recommends
=>
$j
->{test_recommends},
suggests
=>
$j
->{test_suggests},
},
runtime
=> {
requires
=>
$j
->{requires},
recommends
=>
$j
->{recommends},
suggests
=>
$j
->{suggests},
},
};
}
else
{
$self
->{meta_jsn} =
$mmf
;
$j
= decode_json (<
$fh
>);
}
close
$fh
;
unless
(
$self
->{mf}) {
my
$rls
=
$self
->{mf}{release} =
$j
->{name} =~ s{::}{-}gr;
my
$vsn
=
$self
->{mf}{version} =
$j
->{version};
my
$nm
=
$self
->{mf}{name} =
$j
->{name} =~ s{-}{::}gr;
$self
->{prereq}{
$rls
}{v}{
$vsn
//
"-"
} =
"current"
;
}
$self
->{mf}{mpv} ||=
$j
->{prereqs}{runtime}{requires}{perl};
my
$pr
=
$j
->{prereqs} or
return
$self
;
foreach
my
$p
(
qw( configure build test runtime )
) {
foreach
my
$t
(
qw( requires recommends suggests )
) {
my
$x
=
$pr
->{
$p
}{
$t
} or
next
;
foreach
my
$m
(
keys
%$x
) {
my
$v
=
$x
->{
$m
};
$m
=~ s/::/-/g;
$self
->{prereq}{
$m
}{v}{
$v
//
""
} =
$t
;
$self
->{prereq}{
$m
}{
$t
} =
$v
;
}
}
}
push
@{
$self
->{want}} =>
sort
grep
{
$self
->{j}{db}{
$_
} }
keys
%{
$self
->{prereq}};
$self
;
}
sub
set_meta {
my
(
$self
,
$m
,
$v
) =
@_
;
$self
->{mf} = {
name
=>
$m
,
release
=>
$m
=~ s{::}{-}gr,
version
=>
$v
//
"-"
,
};
$self
;
}
sub
want {
my
(
$t
,
$self
,
$m
,
$v
) = (
"requires"
,
@_
);
$m
=~ s/::/-/g;
unless
(first {
$_
eq
$m
} @{
$self
->{want}}) {
$self
->{prereq}{
$m
}{v}{
$v
//
""
} =
$t
;
$self
->{prereq}{
$m
}{
$t
} =
$v
;
$self
->{j} or
$self
->_read_cpansa;
$self
->{j}{db}{
$m
} and
push
@{
$self
->{want}} =>
$m
;
}
$self
;
}
sub
test {
my
$self
=
shift
;
my
$meta
= 0;
$self
->{mf} or
$self
->_read_MakefilePL;
$self
->{mf} or
$self
->_read_META &&
$meta
++;
my
$rel
=
$self
->{mf}{release} or
return
$self
;
$self
->{verbose} and
warn
"Processing for $self->{mf}{release} ...\n"
;
$self
->{j}{mod} or
$self
->_read_cpansa;
@{
$self
->{want}} or
$self
->_read_cpanfile
if
$self
->{deps};
@{
$self
->{want}} or
$self
->_read_META
if
$self
->{deps} && !
$meta
;
@{
$self
->{want}} or
$self
->_read_META (
"META.json"
)
if
$self
->{deps};
$self
->{j}{db}{
$rel
} and
unshift
@{
$self
->{want}} =>
$rel
;
my
@w
= @{
$self
->{want}} or
return
$self
;
foreach
my
$m
(
@w
) {
$m
eq
"perl"
&& !
$self
->{perl} and
next
;
my
@mv
=
sort
map
{
$_
|| 0 }
keys
%{
$self
->{prereq}{
$m
}{v} || {}};
if
(
$self
->{core} and
my
$pv
=
$self
->{mf}{mpv}
and
"@mv"
!~ m/[1-9]/) {
my
$pmv
=
$Module::CoreList::version
{
$pv
}{
$m
=~ s/-/::/gr} //
""
;
$pmv
and
@mv
= (
$pmv
=~ s/\d\K_.*//r);
}
$self
->{verbose} and
warn
"$m: "
,
join
(
" / "
=>
grep
{
$_
}
@mv
),
"\n"
;
my
$cv
= (
$self
->{minimum} ?
$mv
[0] :
$mv
[-1]) || 0;
$self
->{CVE}{
$m
} = {
mod
=>
$m
,
vsn
=>
$self
->{prereq}{
$m
}{t},
min
=>
$cv
,
cve
=> [],
};
foreach
my
$c
(@{
$self
->{j}{db}{
$m
}}) {
my
$cid
=
$c
->{cpansa_id};
my
@cve
=
grep
{ !
exists
$self
->{skip}{
$_
} } @{
$c
->{cves} || []};
my
$dte
=
$c
->{reported};
my
$sev
=
$c
->{severity};
my
$dsc
=
$c
->{description};
my
@vsn
= @{
$c
->{affected_versions} || []};
if
(
my
$i
=
$self
->{prereq}{
$m
}{i}) {
my
$p
=
join
"|"
=>
reverse
sort
keys
%$i
;
my
$m
=
join
"#"
=>
sort
@cve
,
$cid
;
"#$m#"
=~ m/
$p
/ and
next
;
}
if
(
@vsn
) {
$self
->{verbose} > 2 and
warn
"CMP<: $m-$cv\n"
;
$self
->{verbose} > 4 and
warn
"VSN : (@vsn)\n"
;
my
$cmp
=
join
" or "
=>
map
{ s/\s*,\s*/") && XV /gr
=~ s/^/XV /r
=~ s/\s+=(?=[^=<>])\s*/ == /r
=~ s/\s*([=<>]+)\s*/$1 version->parse ("/gr
=~ s/$/")/r
=~ s/\bXV\b/version->parse (
"$cv"
)/gr
=~ s/\)\K(?=\S)/ /gr
}
@vsn
;
$self
->{verbose} > 2 and
warn
"CMP>: $cmp\n"
;
eval
"$cmp ? 0 : 1"
and
next
;
$self
->{verbose} > 3 and
warn
"TAKE!\n"
;
}
else
{
warn
"WTF: Geen V of CVE?\n"
;
}
push
@{
$self
->{CVE}{
$m
}{cve}} => {
cid
=>
$cid
,
dte
=>
$dte
,
cve
=> [
@cve
],
sev
=>
$sev
,
av
=> [
@vsn
],
dsc
=>
$dsc
,
};
}
}
$self
;
}
sub
report {
my
$self
=
shift
;
$self
->{j} or
return
;
@_
% 2 and croak
"Uneven number of arguments"
;
my
%args
=
@_
;
local
$Text::Wrap::columns
= (
$args
{width} ||
$self
->{width}) - 4;
my
$n
;
foreach
my
$m
(@{
$self
->{want}}) {
my
$C
=
$self
->{CVE}{
$m
} or
next
;
my
@c
= @{
$C
->{cve}} or
next
;
say
"$m: "
,
$C
->{min} //
"-"
;
foreach
my
$c
(
@c
) {
my
$cve
=
"@{$c->{cve}}"
||
$c
->{cid};
printf
" %-10s %-12s %-12s %s\n"
,
$c
->{dte},
"@{$c->{av}}"
,
$c
->{sev} //
"-"
,
$cve
;
print
s/^/ /gmr
for
wrap (
""
,
""
,
$c
->{dsc});
$n
++;
}
}
$n
or
say
"There heve been no CVE detections in this process"
;
}
sub
cve {
my
$self
=
shift
;
$self
->{j} or
return
;
@_
% 2 and croak
"Uneven number of arguments"
;
my
%args
=
@_
;
local
$Text::Wrap::columns
=
$args
{width} ||
$self
->{width};
my
@cve
;
foreach
my
$m
(@{
$self
->{want}}) {
my
$C
=
$self
->{CVE}{
$m
} or
next
;
my
@c
= @{
$C
->{cve}} or
next
;
push
@cve
=> {
release
=>
$m
,
vsn
=>
$C
->{min},
cve
=> [
@c
] };
}
@cve
;
}
1;