#!/usr/bin/env perl # # Copyright 2020, Mischa Peters , Netskope. # Netskope_ZScalerImporter.pl # Version 3.0 - 20200615 - rewrite to Perl # Version 3.1 - 20200812 - split domains when comma separated in CSV # Version 3.2 - 20200909 - de-duplication of Zscaler URL category # # 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.016; 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 $LOGMODE = "DEBUG"; my @CONFIG_FILES = grep { -e } ('./netskope.cnf', './.netskope.cnf', '/etc/netskope.cnf', "$ENV{'HOME'}/.netskope.cnf", "$ENV{'HOME'}/netskope.cnf"); my $config = Config::Tiny->read($CONFIG_FILES[-1], 'utf8'); my $USER_COUNT = $config->{report}{USER_COUNT}; my $MAX_DOMAIN= $config->{report}{MAX_DOMAIN}; 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, Filename => 'zscaler_blocklist.csv'); $msg->send('smtp', $SMTP, Debug=>0); say "MAIL From: $FROM -> $TO - Attach CSV" if $LOGMODE; } sub _check_return { my ($status, $content, $uri) = @_; if ($status =~ /^2/ && $LOGMODE) { print "URI: $uri\nHTTP RESPONSE: $status\n"; print "CONTENT:\n$content\n" if ($LOGMODE eq "DEBUG"); } if ($status !~ /^2/) { print "URI: $uri\nHTTP RESPONSE: $status\n$content\n"; my $msg = MIME::Lite->new(From => $FROM, To => $TO, Subject => 'API Error along the way', Type => 'TEXT', Data => "URI: $uri\nHTTP RESPONSE: $status\n$content\n\n", ); $msg->attach(Type => 'text/csv', Data => $EMAIL_CSV); $msg->send('smtp', $SMTP, Debug=>0); say "MAIL From: $FROM -> $TO - Error Notification" if $LOGMODE; say "exit 1"; exit 1; } } sub netskope { my @existing_domains = @{$_[0]}; ### 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, 'proxy' => $PROXY); 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 ($LOGMODE && $LOGMODE eq "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 $domain; my $csv = Text::CSV->new({binary => 1, auto_diag => 1}); open my $fh_in, "<", \$csv_content{$widget_name}; my @headers = $csv->column_names($csv->getline($fh_in)); print "*** ", join(" - ", @headers), " ***\n\n" if $LOGMODE; print "\n## Widget Name: $widget_name\n## Domains: " if $LOGMODE; $EMAIL_CSV .= "$widget_name\n"; $EMAIL_CSV .= "Application,Domain,Category,CCI,Blocked Events,Users\n"; DOMAIN: while (my $row = $csv->getline_hr($fh_in)) { last DOMAIN if ($count == $MAX_DOMAIN); #next DOMAIN if ($row->{'Blocked Events'} > 0); if ($row->{'Users'} < $USER_COUNT) { print "$row->{'Domain'}," if ($LOGMODE ne "DEBUG"); print "$row->{'Application'} - $row->{'Domain'} - $row->{'Category'}, $row->{'CCI'}, $row->{'Blocked Events'}, $row->{'Users'}\n" if ($LOGMODE eq "DEBUG"); if ($row->{'Domain'} =~ /,/) { PARSE: for my $item (split (/,/, $row->{'Domain'})) { next PARSE if (grep(/$item/, @existing_domains)); push @blocklist, $item; $domain .= $item . " "; } if ($domain) { $EMAIL_CSV .= "$row->{'Application'},$domain,$row->{'Category'},$row->{'CCI'},$row->{'Blocked Events'},$row->{'Users'}\n"; $count++; } } else { next DOMAIN if (!grep(/$row->{'Domain'}/, @existing_domains)); push @blocklist, $row->{'Domain'}; $EMAIL_CSV .= "$row->{'Application'},$row->{'Domain'},$row->{'Category'},$row->{'CCI'},$row->{'Blocked Events'},$row->{'Users'}\n"; $count++; } } } print "\n\n" if $LOGMODE; $EMAIL_CSV .= "\n"; print "COUNT: $count\n"; } return @blocklist; } ### Zscaler ### sub zscaler_get { ### 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, 'proxy' => $PROXY, 'cookie_jar' => $jar); 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'}; } } } $uri = "$ZS_BASE_URI/urlCategories/$id"; my $method = "get"; $response = $request->get($uri); _check_return($response->{'status'}, $response->{'content'}, $uri); $json = JSON::PP->new->utf8->decode($response->{'content'}); my $data = $json->{'urls'}; my @convert = (); for my $item (@{$data}) { push @convert, $item; } ### Delete authenticadSession $uri = "$ZS_BASE_URI/authenticatedSession"; $response = $request->delete($uri); _check_return($response->{'status'}, $response->{'content'}, $uri); return @convert; } sub zscaler_push { 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, 'proxy' => $PROXY, 'cookie_jar' => $jar); 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?action=ADD_TO_LIST" : "$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); splice @domains, $ZS_MAX_DOMAINS if @domains > $ZS_MAX_DOMAINS; $body = JSON::PP->new->encode({configuredName => $ZS_CATEGORY_NAME, customCategory => 'true', superCategory => 'SECURITY', urls => \@domains, description => $description}); print "$body\n" if ($LOGMODE eq "DEBUG"); $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 in $LOGMODE mode..." if $LOGMODE; my @existing_domains = zscaler_get(); my @domains = netskope(\@existing_domains); print "Total Domains Pushed: " . scalar @domains . "\n" if $LOGMODE; zscaler_push(\@domains); mail_csv(); say "Completed." if $LOGMODE;