180 lines
6.5 KiB
Perl
180 lines
6.5 KiB
Perl
![]() |
#!/usr/bin/perl
|
||
|
#
|
||
|
# Copyright 2020, Mischa Peters <mischa AT netskope DOT com>, Netskope.
|
||
|
# Netskope_ZScalerImporter.pl - Version 3.0 - 20200615
|
||
|
#
|
||
|
# Permission to use, copy, modify, and distribute this software for any
|
||
|
# purpose with or without fee is hereby granted, provided that the above
|
||
|
# copyright notice and this permission notice appear in all copies.
|
||
|
#
|
||
|
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||
|
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||
|
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||
|
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||
|
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||
|
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||
|
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||
|
#
|
||
|
# ZScaler integration with Netskope
|
||
|
#
|
||
|
use 5.024;
|
||
|
use strict;
|
||
|
use warnings;
|
||
|
use autodie;
|
||
|
use Config::Tiny;
|
||
|
use Time::HiRes qw(gettimeofday);
|
||
|
use POSIX qw(strftime);
|
||
|
use HTTP::Tiny;
|
||
|
use HTTP::CookieJar;
|
||
|
use JSON::PP;
|
||
|
use Text::CSV;
|
||
|
use MIME::Lite;
|
||
|
|
||
|
my $VERBOSE = 1;
|
||
|
my $DEBUG = 0;
|
||
|
my $CONFIG_FILE = "/home/mischa/netskope/netskope.cnf";
|
||
|
my $config = Config::Tiny->read($CONFIG_FILE, 'utf8');
|
||
|
my $NTSKP_TENANT = $config->{netskope}{NTSKP_TENANT};
|
||
|
my $NTSKP_TOKEN = $config->{netskope}{NTSKP_TOKEN};
|
||
|
my $NTSKP_REPORTID = $config->{netskope}{NTSKP_REPORTID};
|
||
|
my $ZS_MAX_DOMAINS = $config->{zscaler}{ZS_MAX_DOMAINS};
|
||
|
my $ZS_BASE_URI = $config->{zscaler}{ZS_BASE_URI};
|
||
|
my $ZS_API_KEY = $config->{zscaler}{ZS_API_KEY};
|
||
|
my $ZS_API_USERNAME = $config->{zscaler}{ZS_API_USERNAME};
|
||
|
my $ZS_API_PASSWORD = $config->{zscaler}{ZS_API_PASSWORD};
|
||
|
my $ZS_CATEGORY_NAME = $config->{zscaler}{ZS_CATEGORY_NAME};
|
||
|
my $ZS_CATEGORY_DESC = $config->{zscaler}{ZS_CATEGORY_DESC};
|
||
|
my $PROXY = $config->{general}{PROXY};
|
||
|
my $SMTP = $config->{general}{SMTP};
|
||
|
my $FROM = $config->{general}{FROM};
|
||
|
my $TO = $config->{general}{TO};
|
||
|
my $SUBJECT = $config->{general}{SUBJECT};
|
||
|
my $TEXT = $config->{general}{TEXT} . "\n\n";
|
||
|
my %HEADERS = ("Content-Type" => "application/json", "Cache-Control" => "no-cache");
|
||
|
my $EMAIL_CSV;
|
||
|
|
||
|
### Netskope ###
|
||
|
sub mail_csv {
|
||
|
my $msg = MIME::Lite->new(From=> $FROM, To => $TO, Subject => $SUBJECT, Type => 'TEXT', Data => $TEXT);
|
||
|
$msg->attach(Type => 'text/csv', Data => $EMAIL_CSV);
|
||
|
$msg->send('smtp', $SMTP, Debug=>0);
|
||
|
say "SMTP $FROM -> $TO - CSV" if $VERBOSE;
|
||
|
}
|
||
|
|
||
|
sub check_return {
|
||
|
my ($status, $content, $uri) = @_;
|
||
|
if ($status !~ /^2/) {
|
||
|
print "URI: $uri\nHTTP RESPONSE: $status\n$content\n";
|
||
|
my $msg = MIME::Lite->new(From => $FROM, To => $TO, Subject => 'Error', Type => 'TEXT',
|
||
|
Data => "URI: $uri\nHTTP RESPONSE: $status\n$content\n",
|
||
|
);
|
||
|
$msg->send('smtp', $SMTP, Debug=>0);
|
||
|
say "SMTP $FROM -> $TO - ERROR" if $VERBOSE;
|
||
|
say "exit 1";
|
||
|
exit 1;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
sub netskope {
|
||
|
### Collect widget IDs
|
||
|
my $uri = "$NTSKP_TENANT/api/v1/reports?token=$NTSKP_TOKEN&op=reportInfo&id=$NTSKP_REPORTID";
|
||
|
my $request = HTTP::Tiny->new('default_headers' => \%HEADERS);
|
||
|
my $response = $request->get($uri);
|
||
|
check_return($response->{'status'}, $response->{'content'}, $uri);
|
||
|
|
||
|
my $json = JSON::PP->new->utf8->decode($response->{'content'});
|
||
|
my $data = $json->{'data'}->{'latestScheduledRunInfo'}->{'widgets'};
|
||
|
if (!$data) { check_return(404, $response->{'content'}, "No Widget Data"); }
|
||
|
my %csv_content;
|
||
|
|
||
|
### Collect widget data and write to CSV
|
||
|
for my $widget (@{$data}) {
|
||
|
$uri = "$NTSKP_TENANT/api/v1/reports?token=$NTSKP_TOKEN&op=widgetData&id=$widget->{'id'}";
|
||
|
$response = $request->get($uri);
|
||
|
print "\n#DEBUG#\nWidget Name: " . $widget->{'name'} . "\nWidgetID: " . $widget->{'id'} . " (ReportID: $NTSKP_REPORTID)\n" . $response->{'content'} if $DEBUG;
|
||
|
check_return($response->{'status'}, $response->{'content'}, $uri);
|
||
|
$csv_content{$widget->{'name'}} = $response->{'content'};
|
||
|
}
|
||
|
|
||
|
### Process domains from CSV
|
||
|
my @blocklist;
|
||
|
for my $widget_name (keys %csv_content) {
|
||
|
my $count = 0;
|
||
|
my $csv = Text::CSV->new({binary => 1, auto_diag => 1});
|
||
|
open my $fh_in, "<", \$csv_content{$widget_name};
|
||
|
my $header = $csv->getline($fh_in);
|
||
|
|
||
|
print "\n## Widget Name: $widget_name\n## Domains: " if $VERBOSE;
|
||
|
$EMAIL_CSV .= "$widget_name\n";
|
||
|
while (my $row = $csv->getline($fh_in)) {
|
||
|
last if ($count == 30);
|
||
|
print "$row->[1]," if $VERBOSE;
|
||
|
$EMAIL_CSV .= "$row->[1],";
|
||
|
push @blocklist, $row->[1];
|
||
|
$count++;
|
||
|
}
|
||
|
print "\n" if $VERBOSE;
|
||
|
$EMAIL_CSV .= "\n";
|
||
|
}
|
||
|
return @blocklist;
|
||
|
}
|
||
|
|
||
|
### Zscaler ###
|
||
|
|
||
|
sub zscaler {
|
||
|
my @domains = @{$_[0]};
|
||
|
|
||
|
### Authenticate
|
||
|
my $now = int(gettimeofday * 1000);
|
||
|
my $n = substr($now, -6);
|
||
|
my $r = sprintf "%06d", $n >> 1;
|
||
|
my $key;
|
||
|
for my $i (0..length($n)-1) {
|
||
|
$key .= substr($ZS_API_KEY, substr($n, $i, 1), 1);
|
||
|
}
|
||
|
for my $i (0..length($r)-1) {
|
||
|
$key .= substr($ZS_API_KEY, substr($r, $i, 1) + 2, 1);
|
||
|
}
|
||
|
my $uri = "$ZS_BASE_URI/authenticatedSession";
|
||
|
my $body = JSON::PP->new->encode({apiKey => $key, username => $ZS_API_USERNAME, password => $ZS_API_PASSWORD, timestamp => $now});
|
||
|
my $jar = HTTP::CookieJar->new;
|
||
|
my $request = HTTP::Tiny->new('default_headers' => \%HEADERS, 'cookie_jar' => $jar);
|
||
|
my $response = $request->post($uri, {'content' => $body});
|
||
|
check_return($response->{'status'}, $response->{'content'}, $uri);
|
||
|
|
||
|
### Get filter list id
|
||
|
$uri = "$ZS_BASE_URI/urlCategories/lite";
|
||
|
$response = $request->get($uri);
|
||
|
check_return($response->{'status'}, $response->{'content'}, $uri);
|
||
|
|
||
|
my $json = JSON::PP->new->utf8->decode($response->{'content'});
|
||
|
my $id;
|
||
|
for my $item (@{$json}) {
|
||
|
if (exists($item->{'configuredName'})) {
|
||
|
if ($item->{'configuredName'} eq $ZS_CATEGORY_NAME) {
|
||
|
$id = $item->{'id'};
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
### Push Domains
|
||
|
$uri = defined($id) ? "$ZS_BASE_URI/urlCategories/$id" : "$ZS_BASE_URI/urlCategories";
|
||
|
my $method = defined($id) ? "put" : "post";
|
||
|
my $description = "$ZS_CATEGORY_DESC\n\nLast Updated: " . strftime("%Y-%m-%d %H:%M:%S", localtime);
|
||
|
$#domains = $#domains >= $ZS_MAX_DOMAINS ? $ZS_MAX_DOMAINS : $#domains;
|
||
|
$body = JSON::PP->new->encode({configuredName => $ZS_CATEGORY_NAME, customCategory => 'true', superCategory => 'SECURITY', urls => \@domains, description => $description});
|
||
|
$response = $request->$method($uri, {'content' => $body});
|
||
|
check_return($response->{'status'}, $response->{'content'}, $uri);
|
||
|
|
||
|
### Delete authenticadSession
|
||
|
$uri = "$ZS_BASE_URI/authenticatedSession";
|
||
|
$response = $request->delete($uri);
|
||
|
check_return($response->{'status'}, $response->{'content'}, $uri);
|
||
|
}
|
||
|
|
||
|
say "Running..." if $VERBOSE;
|
||
|
my @domains = netskope();
|
||
|
zscaler(\@domains);
|
||
|
mail_csv();
|
||
|
say "Completed." if $VERBOSE;
|