#! /usr/bin/env perl
use DBI;
use LWP::UserAgent;
use Getopt::Long;
use Sys::Hostname;
use JSON qw(to_json encode_json decode_json);
use Text::Wrap;
use strict;
use warnings;
my $hostname = hostname();
my %args;
binmode STDOUT, ":encoding(utf8)";

sub usage {
    my ($msg) = @_;
    print $msg, "\n" if defined($msg);
    die <<'USAGE';
Usage: vulnreport [options]

OPTIONS
    -u, --user=USER             # report USER's vulnerabilities
    -p, --plugin=SLUG           # get vulnerabilities about plugin
    -p, --plugin=SLUG:VERSION   # for a particular version of the plugin
    --overview                  # print vulnerable plugins per site

    -q, --quiet                 # exit with status code 2 if vulns found
    -b, --brief                 # print only one line per vulnerable version

    --csv                       # print tab-separated report
    --soc                       # ... with SOC Dashboard URLs
    --json                      # print JSON

    --socuser                   # print a URL with a user site report
    --ifvuln                    # ... if there are any vulnerabilities to see

    -h, --help                  # print this information
    --notes                     # print about information sources

EXAMPLES
    vulnreport -u someuser
    vulnreport -u someuser --ifvuln      # no output if no vulns
    vulnreport -u someuser -p elementor  # warn about this user's version
    vulnreport -p woocommerce
    vulnreport -p elementor:3.15.0
USAGE
}
sub notes {
    die <<'NOTES';
NOTE WELL
    Local plugin information is pulled from /var/lib/eig/dc/plugins , which is
    populated by /etc/cron.daily/wpscan .  The wpscan script should only take a
    few minutes, so if current information is desired, just run that.

    Vulnerability reports are pulled from the SOC Dashboard, and ultimately
    from Wordfence's vulnerability database:
    https://www.wordfence.com/threat-intel/vulnerabilities

    --socuser links can be slightly stale, as the database is rebuilt every
    four hours from server reports.
NOTES
}

sub main() {
    GetOptions (
        'user|u=s' => \$args{user},
        'plugin|p=s' => \$args{plugin},
        'overview' => \$args{overview},

        'quiet|q' => \$args{quiet},
        'brief|b' => \$args{brief},

        'csv' => \$args{csv},
        'soc' => \$args{soc},

        'socuser' => \$args{socuser},
        'ifvuln' => \$args{ifvuln},
        'json' => \$args{json},

        'help|h' => \$args{help},
        'notes' => \$args{notes},
    ) or usage();
    usage() if defined($args{help});
    notes() if defined($args{notes});
    usage("--socuser requires --user")
        if defined($args{socuser}) && !defined($args{user});
    usage("--soc requires --csv or --brief")
        if defined($args{soc}) && !(defined($args{csv}) || defined($args{brief}));
    usage("--quiet requires --plugin or --user")
        if defined($args{quiet}) && !(defined($args{plugin}) || defined($args{user}));
    usage("only one of --quiet, --brief, or --csv may be provided")
        if 1 < (defined($args{quiet}) +
            defined($args{brief}) +
            defined($args{csv}));
    usage() unless defined($args{user}) || defined($args{plugin});

    my $vulns; # hashref or, if no vulns or --quiet passed, a boolean
    if (defined($args{socuser})) {
        my $user = $args{user};
        $vulns = user_vulns($user);
        exit if defined($args{ifvuln}) && !$vulns;
        print "https://reports.soc.newfold.com/dash/plugins/hostvuln?user=$user&host=$hostname\n";
        exit;
    } elsif (defined($args{user}) && defined($args{plugin})) {
        my ($user, $plugin) = ($args{user}, $args{plugin});
        $plugin =~ s/:.*//; # strip any explicit version
        my $version = user_plugin_version($user, $plugin);
        $vulns = plugin_vulns($plugin, $version);
    } elsif (defined($args{user}) && defined($args{overview})) {
        my ($sites, @plugins) = user_site_plugins($args{user});
        my $vulns = plugin_vulns(@plugins);
        overview_report($sites, $vulns) if $vulns;
        exit if defined($args{ifvuln}) && !$vulns;
        print "No plugin vulnerabilities found.\n" if !$vulns;
        exit;
    } elsif (defined($args{user})) {
        $vulns = user_vulns($args{user});
    } else {
        my ($slug, $version) = split /:/, $args{plugin}, 2;
        $version = "0" unless defined($version);
        $vulns = plugin_vulns($slug, $version);
    }

    if (defined($args{brief})) {
        exit if defined($args{ifvuln}) && !$vulns;
        brief_report($vulns) if $vulns;
        print "No plugin vulnerabilities found.\n" if !$vulns;
    } elsif (defined($args{csv})) {
        exit if !$vulns;
        csv_report($vulns, $args{soc});
    } elsif (defined($args{quiet})) {
        exit($vulns ? 2 : 0);
    } elsif (defined($args{json})) {
        exit if !$vulns;
        print to_json($vulns, {pretty => 1});
    } else {
        exit if defined($args{ifvuln}) && !$vulns;
        full_report($vulns) if $vulns;
        print "No plugin vulnerabilities found.\n" if !$vulns;
    }
}

sub brief_report {
    my ($vulns) = @_;
    for my $pluginver (keys %{$vulns}) {
        for my $vuln (@{$vulns->{$pluginver}}) {
            print $vuln->{title}, "\n";
        }
    }
}

sub csv_report {
    my ($vulns) = @_;
    for my $pluginver (keys %{$vulns}) {
        for my $vuln (@{$vulns->{$pluginver}}) {
            my @urls = @{$vuln->{references}};
            if (defined($args{soc})) {
                my ($plugin, $vsn) = split / /, $pluginver, 2;
                @urls = ("https://reports.soc.newfold.com/dash/plugins/vuln?plugin=$plugin&vsn=$vsn");
            }
            print join(",", $pluginver, $vuln->{title}, @urls), "\n";
        }
    }
}

sub overview_report {
    no warnings 'uninitialized';
    my ($sites, $vulns) = @_;
    for my $site (values %$sites) {
        my @vuln = grep { exists $vulns->{$_} } @{$site->{plugins}};
        next unless @vuln;
        print "VULNERABLE SITE: ROOT:$site->{root} URL:$site->{siteurl} WPVER:$site->{wpver} THEME:$site->{theme}\n";
        for my $pluginver (sort { @{$vulns->{$b}} <=> @{$vulns->{$a}} || $a cmp $b } @vuln) {
            my $vulncount = @{$vulns->{$pluginver}};
            printf "%3d   %s\n", $vulncount, $pluginver;
        }
        print "\n";
    }
    for my $pluginver (keys %{$vulns}) {
        my ($plugin, $version) = split / /, $pluginver, 2;
        for my $vuln (@{$vulns->{$pluginver}}) {
        }
    }
}

sub full_report {
    no warnings 'uninitialized';
    my ($vulns) = @_;
    for my $pluginver (keys %{$vulns}) {
        my ($plugin, $version) = split / /, $pluginver, 2;
        for my $vuln (@{$vulns->{$pluginver}}) {
            my $ref = join ' ', @{$vuln->{references}};
            print <<VULN;
*** Known Vulnerability in $plugin @ $version
  Title        @{[wrap '', ' 'x15, $vuln->{title}]}
  Desc         @{[wrap '', ' 'x15, $vuln->{description}]}
  CWE Name     @{[wrap '', ' 'x15, $vuln->{cwe}{name}]}
  CWE Desc     @{[wrap '', ' 'x15, $vuln->{cwe}{description}]}
  CVSS Vector  $vuln->{cvss}{vector}
  CVSS Score   $vuln->{cvss}{score}
  CVSS Rating  $vuln->{cvss}{rating}
  CVE          $vuln->{cve_link}
  Published    $vuln->{published}
  Updated      $vuln->{updated}
  References   $ref
VULN
            my $spaced;
            for my $soft (@{$vuln->{software}}) {
                $spaced = 1;
                print <<SOFT;
    slug               $soft->{slug}
    patched?           @{[$soft->{patched} ? "yes" : "no"]}
    remediation        $soft->{remediation}

SOFT
            }
            print "\n" unless $spaced;
        }
    }
}

sub user_plugin_version {
    my ($user, $slug) = @_;
    my $dbh = DBI->connect("dbi:SQLite:dbname=/var/lib/eig/dc/plugins", undef, undef, { RaiseError => 1 });
    for my $res (@{$dbh->selectall_arrayref("SELECT pver FROM plugins WHERE user=? AND plugin=? LIMIT 1", undef, $user, $slug)}) {
        my ($pver) = @$res;
        return $pver;
    }
    die "[!] not found in database: ${user}'s $slug\n";
}

sub user_plugins {
    my ($user) = @_;
    my (%seen, @result);
    my $dbh = DBI->connect("dbi:SQLite:dbname=/var/lib/eig/dc/plugins", undef, undef, { RaiseError => 1 });
    for my $res (@{$dbh->selectall_arrayref("SELECT plugin,pver FROM plugins WHERE user=?", undef, $user)}) {
        my ($plugin, $pver) = @$res;
        push @result, $plugin, $pver unless $seen{"$plugin $pver"}++;
    }
    die "No plugins found in database for user ``$user''\n" if !@result && !defined($args{quiet});
    return @result;
}

sub user_site_plugins {
    my ($user) = @_;
    my (%sites, %seen, @plugins);
    my $dbh = DBI->connect("dbi:SQLite:dbname=/var/lib/eig/dc/plugins", undef, undef, { RaiseError => 1 });
    for my $res (@{$dbh->selectall_arrayref("SELECT db,tbl,root,siteurl,wpver,theme,plugin,pver FROM plugins WHERE user=?", undef, $user)}) {
        my ($db, $tbl, $root, $siteurl, $wpver, $theme, $plugin, $pver) = @$res;
        $sites{"$db $tbl"} = {
            root => $root,
            siteurl => $siteurl, 
            wpver => $wpver,
            theme => $theme,
            plugin => $plugin,
            pver => $pver,
        } unless exists $sites{"$db $tbl"};
        push @{$sites{"$db $tbl"}{plugins}}, "$plugin $pver";
        push @plugins, $plugin, $pver unless $seen{"$plugin $pver"}++;
    }
    die "No plugins found in database for user ``$user''\n" if !@plugins && !defined($args{quiet});
    return (\%sites, @plugins);
}

sub user_vulns {
    my ($user) = @_;
    return plugin_vulns(user_plugins($user));
}

sub plugin_vulns {
    my (@pairs) = @_; # flat list of (plugin, version) pairs
    my $ua = LWP::UserAgent->new;
    $ua->agent('vulnreport/1.0');
    $ua->ssl_opts(verify_hostname => 0);
    my $quiet = defined($args{quiet}) ? "?quiet=1" : "";
    my $res = $ua->post('https://reports.soc.newfold.com/dash/plugins/bulkvuln'.$quiet, Content => encode_json(\@pairs));
    $res->is_success or die "Failed to get vuln info: ".$res->content;
    return $res->content eq "1" if defined($args{quiet});
    my $obj = decode_json($res->content);
    keys(%$obj) or return 0;
    return $obj;
}


# to reuse this file as a library, require() it:
#   perl -e 'require "/root/bin/vulnreport"; main()'
main() unless caller;
1;
