Wiz Webhook with Netskope SSE
Wiz Webhook with Netskope SSE
Wiz works to secure cloud infrastructure, using agentless scanning to give cloud security teams immediate visibility into the most critical security risks that need to be addressed. Leveraging its seamless integration with Netskope, organizations gain real-time insights and proactive defense mechanisms against evolving cloud threats. Together, Wiz and Netskope form an unbeatable alliance in safeguarding cloud environments with unparalleled efficiency and precision. This document encompasses a detailed step by step guide for integrating Wiz Issues to influence Netskope NPA policies.
Prerequisites
For this integration, you need:
- An AWS Admin account for provisioning lambda.
- A Netskope Tenant.
- A Netskope API Key.
- A Username for authenticating Wiz Webhooks.
- A Password for authenticating Wiz Webhooks).
Wiz Overview
Wiz secures everything organizations build and run in the cloud. Wiz connects to every cloud environment, scans every layer, and covers every aspect of your cloud security, including elements that normally require installing agents. Its comprehensive approach has all of these cloud security solutions built in.
Joint Use Cases
- Secure private access to AWS resources leveraging Wiz Issues.
- Unified control plane between customer security providers.
Joint Solution
The integration takes advantage of the deep visibility that Wiz has into cloud environments and context of which exposures lead to critical attack paths and allows Netskope to enforce policy to the end user.
The solution leverages Wiz Webhooks along with the Netskope platform API.
A lambda function is deployed in the customer tenant along with an API Gateway to handle the Webhooks. The lambda function authenticates to the Netskope ZTNA (Private Access) APIs.
Wiz provide a list of hostnames that are currently experiencing any OPEN issues.
The lambda function will take the hostnames that is learned from Wiz Webhook and see if the hostname is configured for allow in any Real-time policies. If the hostname is seen there will be a API call placed to add the hostname to a private application and blocked by Netskope.
Deployment Steps
- Generate a REST API v2 token from your Netskope tenant. You need to add the following endpoints to your scope:
For more information about creating a Netskope API token and adding endpoints to the scope, go to REST API v2 Overview. - When you have the token information, you can deploy the lambda function in your AWS tenant.
- Log in to your AWS console (https://console.aws.amazon.com/).
- Go to the AWS service Lambda.
- Deploy a new Lambda function in management console of AWS. Create from scratch or choose the latest python version, and leave all default configuration variables.
- After the Function is created, go to Code and paste the following code in to your function:
import os import json import urllib3 # fetching environmental variables netskope_api_token = os.environ['netskope_api_token'] netskope_tenant = os.environ['netskope_tenant'] username = os.environ['username'] password = os.environ['password'] http = urllib3.PoolManager() # creating python function to get all NPA Publishers from the tenant def get_npa_publishers(): url = f"https://{netskope_tenant}.goskope.com/api/v2/infrastructure/publishers" headers = {'Accept': 'application/json', 'Netskope-api-token': netskope_api_token} response = http.request('GET', url, headers=headers) response_json = json.loads(response.data.decode('utf-8')) return [[x['publisher_id'],x['connected_apps']] for x in response_json['data']['publishers'] if x['apps_count'] > 0] # creating python function to get all NPA Apps for a given publisher ID def get_npa_publisher_apps(pub_id): url = f"https://{netskope_tenant}.goskope.com/api/v2/infrastructure/publishers/{pub_id}/apps" headers = {'Accept': 'application/json', 'Netskope-api-token': netskope_api_token} response = http.request('GET', url, headers=headers) response_json = json.loads(response.data.decode('utf-8')) return [response_json['data'][0]['host']] # creating python function to get all NPA rules from the tenant def get_npa_rules(): url = f"https://{netskope_tenant}.goskope.com/api/v2/policy/npa/rules" headers = {'Accept': 'application/json', 'Netskope-api-token': netskope_api_token} response = http.request('GET', url, headers=headers) return json.loads(response.data.decode('utf-8')) # creating python function to get all NPA private apps from the tenant def get_npa_apps(): url = f"https://{netskope_tenant}.goskope.com/api/v2/steering/apps/private" headers = {'Accept': 'application/json', 'Netskope-api-token': netskope_api_token} response = http.request('GET', url, headers=headers) return json.loads(response.data.decode('utf-8')) # creating python function to update a NPA rule for the tenant def create_npa_rule(create_npa_rule_data): url = f"https://{netskope_tenant}.goskope.com/api/v2/policy/npa/rules" headers = {'Accept': 'application/json', 'Netskope-api-token': netskope_api_token} response = http.request('POST', url, headers=headers, body=json.dumps(npa_policy_data)) return json.loads(response.data.decode('utf-8')) # creating python function to patch a private app to add hostnames def post_wiz_private_app(): # Make the request response = http.request("POST", f"https://{netskope_tenant}.goskope.com/api/v2/steering/apps/private",\ headers={'Accept': 'application/json', 'Netskope-api-token': netskope_api_token}, \ body=json.dumps({ "app_name": "Wiz_Hosts_w_Issues", "host": "wiz_default.io", "protocols": [ {"type": "tcp", "port": "80"}, {"type": "tcp", "port": "443"} ], })) # Decode and return the response return response.data.decode('utf-8') # creating python function to update private app in NPA def update_wiz_private_app(host_list, app_id_to_update): # Make the request response = http.request("PATCH", f"https://{netskope_tenant}.goskope.com/api/v2/steering/apps/private/{app_id_to_update}",\ headers={'Accept': 'application/json', 'Netskope-api-token': netskope_api_token}, \ body=json.dumps({ "app_name": "Wiz_Hosts_w_Issues", "host": host_list, "protocols": [ {"type": "tcp", "port": "80"}, {"type": "tcp", "port": "443"} ], })) # Decode and return the response return response.data.decode('utf-8') # creating function to look for username and password in webhook def check_http_basic_auth(headers): if str(username) not in str(headers) or str(password) not in str(headers): return { 'statusCode': 401, # Unauthorized status code 'body': 'Unauthorized access' } def lambda_handler(event,context): # Check if 'body' key exists in the event dictionary if 'body' in event: # Check if the 'body' is not None and not an empty string if event['body'] and event['body'].strip(): # 'body' is not empty # Proceed with processing the payload payload = event['body'] # Your code to handle the non-empty payload else: # 'body' is empty # Handle the case where payload is empty return { 'statusCode': 400, 'body': 'Empty payload received' } else: # 'body' key does not exist in event dictionary # Handle the case where 'body' is missing return { 'statusCode': 400, 'body': 'No payload found in the request' } # Parse the 'body' JSON data body_data = json.loads(event['body']) # Access elements from the parsed JSON data issueSeverity = body_data.get('issue', {}).get('severity') resource = body_data.get('resource', {}).get('id') issueStatus = body_data.get('issue', {}).get('status') # executing function to validate correct username and password for Webhook Basic Auth check_http_basic_auth(event['headers']) # executing function to get all npa private apps for tenant npa_apps = get_npa_apps() # executing function to get all NPA policies for Netskope tenant npa_rules = get_npa_rules() # creating list of app IDs to update that will later be appended to app_id_to_update = [] # conditional statement to validate that private app Wiz_Hosts_w_Issues already exists if "Wiz_Hosts_w_Issues" in str(json.dumps(npa_apps['data']['private_apps'])): print("Wiz_Hosts_w_Issues private app exists, continuing") # using list comprehension to get all existing application IDs (hostnames), will need these later app_id_to_update = [x['app_id'] for x in npa_apps['data']['private_apps'] if 'Wiz_Hosts_w_Issues' in str(x)] else: # if Wiz_Hosts_w_Issues private app does not exist we will create the app using the Netwskope API print(" need to create Wiz_Hosts_w_Issues private app") # executing function to create Wiz_Hosts_w_Issues private app private_app_response = json.loads(json.dumps(post_wiz_private_app())) # still need to collect placeholder private app names as discussed in post_wiz_private_app func app_id_to_update = json.loads(private_app_response)['data']['app_id'] # validating the private app Wiz_Blocked_Hosts is inside the npa_rules if "Wiz_Hosts_w_Issues" in str(json.dumps(npa_rules)): print("Wiz Block Rule Exists, continuing") else: print("need to createWiz Block Rule") # need to create NPA Rule with Wiz_Hosts_w_Issues private app (executing create_npa_rule func) create_npa_rule_data = {"enabled": "1", "policy_type": "private-app", "rule_data": {"access_method": ["Client"], "external_dlp": False, "json_version": 3, "match_criteria_action": {"action_name": "block", "template": "wiztest"}, "policy_type": "private-app", "privateApps": ["[Wiz_Hosts_w_Issues]"], "privateAppsWithActivities": [{"activities": [{"activity": "any", "list_of_constraints": []}], "appName": "[Wiz_Hosts_w_Issues]"}], "show_dlp_profile_action_table": False, "userType": "user", "version": 1}, "rule_id": "3", "rule_name": "Wiz_Hosts_w_Issues", "rule_order": {"order": "top"}} create_rule_response = create_npa_rule(create_npa_rule_data) print(create_rule_response) # creating clean list of all hostnames from npa_apps npa_apps_hostnames = [hostname['host'] for hostname in npa_apps['data']['private_apps']] npa_apps_hostnames_list = [] final_host_list = [] for host in npa_apps_hostnames: hostname = host.split(',') if len(hostname) > 1: for host2 in hostname: npa_apps_hostnames_list.append(host2) else: npa_apps_hostnames_list.append(hostname[0]) # checking if resource is in the npa_apps_hostnames_list resource_id_from_webhook = str(resource).split('instance/') # Take the last part of the resulting list resource = resource_id_from_webhook[-1] if str(resource) in str(npa_apps_hostnames_list) and 'open' == str(issueStatus).lower(): # if resource is in apps list and the issue status is OPEN see if it is already in Wiz_Host_With_Issues private app wiz_hosts_with_issues_list = [x['host'] for x in npa_apps['data']['private_apps'] if 'Wiz_Hosts_w_Issues' in str(x)] if str(resource) in str(wiz_hosts_with_issues_list): print(f"resource {resource} is already blocked, no action needed") else: print(f"resource {resource} is not blocked, updating NPA private app for Wiz_Hosts_w_Issues") wiz_hosts_with_issues = str(wiz_hosts_with_issues_list[0]).split(',') + [resource] wiz_hosts = ', '.join(wiz_hosts_with_issues) patch_host_list_response = update_wiz_private_app(wiz_hosts, app_id_to_update[0]) print(patch_host_list_response) # now the function will perform a update to NPA private Apps if issueStatus = CLOSED if str(issueStatus).lower() == 'closed': # updating npa private apps to remove resource wiz_hosts_with_issues_list = [x['host'] for x in npa_apps['data']['private_apps'] if 'Wiz_Hosts_w_Issues' in str(x)] wiz_hosts_with_issues = str(wiz_hosts_with_issues_list[0]).replace(resource, '').replace(', ,', ',').strip(', ') patch_host_list_response = update_wiz_private_app(wiz_hosts_with_issues, app_id_to_update[0]) # prepare response body response_body = {} #response_body['npa_apps_hostnames_list'] = npa_apps_hostnames_list response_body['issueSeverity'] = issueSeverity response_body['resource'] = resource response_body['header_info'] = event['headers'] # prepare http response http_result = {} http_result['statusCode'] = 200 http_result['headers'] = {} http_result['headers']['Content-Type'] = 'application/json' http_result['body'] = json.dumps(response_body) return http_result
- Configure the environment variables to authenticate the Wiz Webhook and Netskope API:
- Click Edit.
- Build a Rest API Gateway:
- Select Add Trigger in the lambda dashboard.
- Select API Gateway as the source.
Note
BASIC HTTP Authentication is done on lines 93 in the code pasted in the previous step.
- Go to the API Gateway to make sure the API Endpoint Wiz will use in the integration.
- When there, click the hyperlink for the API endpoint.
Test the Integration
Now that the function and API Gateway are deployed, you can test the endpoint to validate to see the policy change in the Netskope Dashboard, as well as authentication.
Lambda is looking for 3 things inside the webhook: issueSeverity , issueStatus , and resource.
It also tries to authenticate the Webhook using Basic Auth, so you need the username and Password specified in the lambda environment variable to be used.
If the Username and Password in the Headers does not match then a 401 unauthorized error is returned as shown:
Validate the Integration
After successfully integrating a webhook in the API GW Dashboard, you can validate the policies and private apps are seen in the Netskope One platform.
Log in to the Netskope tenant and go to Policies > Real-Time Protection and ensure you see Wiz_Hosts_w_Issues rule at the top.
Also check under the Destination that the private application Wiz_Hosts_w_Issues exists. (The function should automatically create this rule and private application.)
Go to Settings and Security Cloud Platform > App Definition > Private Apps and ensure that you see Wiz_Hosts_w_Issues.
Click into the private app Wiz_Hosts_w_Issues and ensure you see the default placeholder hostname along with the hostname of the resource from the test performed in the API Gateway in previous step.
In the Wiz dashboard you can fill in the Basic Auth parameters and the API Gateway URL from AWS.
For the URL you need to locate that from the API Gateway in the AWS console: (Invoke URL)
After inputting the Username and Password for Basic Auth, you can also filter to what Issue Types will be sent in the Webhooks. For this we recommend anything with a hostname that Netskope clients can have access to, like vulnerability, malware, unpatched software, External Misconfiguration, etc.
Since this toolkit only cares about issues when they are OPEN or RESOLVED, you can filter the amount of webhooks that are sent to the API GW. However, the code has been made to ignore other webhooks as well: