Collect Netskope Alerts logs
This document explains how to ingest Netskope Alerts logs to Google Security Operations using Google Cloud Storage V2. Netskope is a cloud security platform that provides real-time data and threat protection for cloud services, websites, and private applications. Netskope Alerts capture security events including DLP violations, malware detections, anomalous behavior, compromised credentials, and policy violations across SaaS, IaaS, and web traffic.
For more information, see Collect Netskope Alert logs.
Before you begin
Make sure that you have the following prerequisites:
- A Google SecOps instance.
- A GCP project with Cloud Storage API enabled.
- Permissions to create and manage GCS buckets and IAM policies.
- Permissions to create Cloud Run services, Pub/Sub topics, and Cloud Scheduler jobs.
- Privileged access to the Netskope tenant admin console.
- A Netskope REST API v2 token with read permissions for alerts.
Collect Netskope API v2 credentials
Create a REST API v2 token
- Sign in to the Netskope tenant admin console (for example,
https://your-tenant.goskope.com). - Go to Settings > Tools > REST API v2.
- Click New Token.
- Provide the following configuration details:
- Token Name: Enter a descriptive name (for example,
SecOps-Alert-Integration). - Expiry: Set the token expiry date according to your security policy.
- Token Name: Enter a descriptive name (for example,
- Under Endpoints, enable the following:
/api/v2/events/data/alert- Read permission.
- Click Save.
- Copy and save the token value securely. The token is displayed only once.
Verify API access
Test your credentials before proceeding with the integration:
NETSKOPE_TENANT="[https://your-tenant.goskope.com](https://your-tenant.goskope.com)" API_TOKEN="your-api-v2-token" curl -v -H "Netskope-Api-Token: ${API_TOKEN}" \ "${NETSKOPE_TENANT}/api/v2/events/data/alert?limit=1"
Create Google Cloud Storage bucket
- Go to the Google Cloud Console.
- Select your project or create a new one.
- In the navigation menu, go to Cloud Storage > Buckets.
- Click Create bucket.
Provide the following configuration details:
Setting Value Name your bucket Enter a globally unique name (for example, netskope-alerts-logs)Location type Choose based on your needs (Region, Dual-region, Multi-region) Location Select the location closest to your Google SecOps instance Storage class Standard (recommended for frequently accessed logs) Access control Uniform (recommended) Click Create.
Create service account for Cloud Run function
The Cloud Run function needs a service account with permissions to write to GCS bucket and be invoked by Pub/Sub.
Create service account
- In the GCP Console, go to IAM & Admin > Service Accounts.
- Click Create Service Account.
- Provide the following configuration details:
- Service account name:
netskope-alerts-collector-sa. - Service account description:
Service account for Cloud Run function to collect Netskope Alerts logs.
- Service account name:
- Click Create and Continue.
- In the Grant this service account access to project section, add the following roles:
- Storage Object Admin
- Cloud Run Invoker
- Cloud Functions Invoker
- Click Continue and then Done.
Grant IAM permissions on GCS bucket
- Go to Cloud Storage > Buckets.
- Click on the bucket
netskope-alerts-logs. - Go to the Permissions tab.
- Click Grant access.
- Provide the following configuration details:
- Add principals: Enter the service account email (for example,
netskope-alerts-collector-sa@PROJECT_ID.iam.gserviceaccount.com). - Assign roles: Select Storage Object Admin.
- Add principals: Enter the service account email (for example,
- Click Save.
Create Pub/Sub topic
Create a Pub/Sub topic that Cloud Scheduler will publish to and the Cloud Run function will subscribe to.
- In the GCP Console, go to Pub/Sub > Topics.
- Click Create topic.
- Provide the following configuration details:
- Topic ID:
netskope-alerts-trigger. - Leave other settings as default.
- Topic ID:
- Click Create.
Create Cloud Run function to collect logs
The Cloud Run function will be triggered by Pub/Sub messages from Cloud Scheduler to fetch alerts from the Netskope REST API v2 and write them to GCS.
- In the GCP Console, go to Cloud Run.
- Click Create service.
- Select Function.
In the Configure section, provide the following configuration details:
Setting Value Service name netskope-alerts-collectorRegion Select region matching your GCS bucket (for example, us-central1)Runtime Python 3.12 or later In the Trigger section:
- Click + Add trigger.
- Select Cloud Pub/Sub.
- In Select a Cloud Pub/Sub topic, choose
netskope-alerts-trigger. - Click Save.
In the Authentication section, select Require authentication and check Identity and Access Management (IAM).
Scroll down to Containers, Networking, Security.
In the Security tab, select the service account
netskope-alerts-collector-sa.In the Containers tab, click Variables & Secrets and add the following:
Variable Name Example Value Description GCS_BUCKETnetskope-alerts-logsGCS bucket name GCS_PREFIXnetskope/alertsPrefix for log files STATE_KEYnetskope/alerts/state.jsonState file path NETSKOPE_TENANThttps://your-tenant.goskope.comNetskope tenant URL NETSKOPE_API_TOKENyour-api-v2-tokenREST API v2 token MAX_RECORDS5000Max records per run PAGE_SIZE1000Records per page LOOKBACK_HOURS24Initial lookback period In the Requests section, set Request timeout to
600seconds.In the Settings tab (Resources), select 512 MiB memory and 1 CPU.
Click Create. After the service is created, the inline code editor will open automatically.
Add function code
- Enter main in the Entry point field.
In the inline code editor, create two files:
- main.py:
import functions_framework from google.cloud import storage import json import os import urllib3 from datetime import datetime, timezone, timedelta import time http = urllib3.PoolManager( timeout=urllib3.Timeout(connect=5.0, read=30.0), retries=False, ) storage_client = storage.Client() GCS_BUCKET = os.environ.get('GCS_BUCKET') GCS_PREFIX = os.environ.get('GCS_PREFIX', 'netskope/alerts') STATE_KEY = os.environ.get('STATE_KEY', 'netskope/alerts/state.json') NETSKOPE_TENANT = os.environ.get('NETSKOPE_TENANT') NETSKOPE_API_TOKEN = os.environ.get('NETSKOPE_API_TOKEN') MAX_RECORDS = int(os.environ.get('MAX_RECORDS', '5000')) PAGE_SIZE = int(os.environ.get('PAGE_SIZE', '1000')) LOOKBACK_HOURS = int(os.environ.get('LOOKBACK_HOURS', '24')) def to_epoch_seconds(dt): if dt.tzinfo is None: dt = dt.replace(tzinfo=timezone.utc) return int(dt.timestamp()) def parse_datetime(value): if isinstance(value, (int, float)): return datetime.fromtimestamp(value, tz=timezone.utc) if isinstance(value, str): if value.endswith("Z"): value = value[:-1] + "+00:00" return datetime.fromisoformat(value) return None @functions_framework.cloud_event def main(cloud_event): if not all([GCS_BUCKET, NETSKOPE_TENANT, NETSKOPE_API_TOKEN]): print('Error: Missing required environment variables') return try: bucket = storage_client.bucket(GCS_BUCKET) state = load_state(bucket, STATE_KEY) now = datetime.now(timezone.utc) last_time = None if isinstance(state, dict) and state.get("last_event_time"): try: last_time = parse_datetime(state["last_event_time"]) last_time = last_time - timedelta(minutes=2) except Exception as e: print(f"Warning: Could not parse last_event_time: {e}") if last_time is None: last_time = now - timedelta(hours=LOOKBACK_HOURS) print(f"Fetching alerts from {last_time.isoformat()} to {now.isoformat()}") records, newest_event_time = fetch_alerts( tenant=NETSKOPE_TENANT, token=NETSKOPE_API_TOKEN, start_time=last_time, end_time=now, page_size=PAGE_SIZE, max_records=MAX_RECORDS, ) if not records: print("No new alert records found.") save_state(bucket, STATE_KEY, to_epoch_seconds(now)) return timestamp = now.strftime('%Y%m%d_%H%M%S') object_key = f"{GCS_PREFIX}/alerts_{timestamp}.ndjson" blob = bucket.blob(object_key) ndjson = '\n'.join([json.dumps(r, ensure_ascii=False) for r in records]) + '\n' blob.upload_from_string(ndjson, content_type='application/x-ndjson') print(f"Wrote {len(records)} records to gs://{GCS_BUCKET}/{object_key}") if newest_event_time: save_state(bucket, STATE_KEY, newest_event_time) else: save_state(bucket, STATE_KEY, to_epoch_seconds(now)) print(f"Successfully processed {len(records)} records") except Exception as e: print(f'Error processing alerts: {str(e)}') raise def load_state(bucket, key): try: blob = bucket.blob(key) if blob.exists(): state_data = blob.download_as_text() return json.loads(state_data) except Exception as e: print(f"Warning: Could not load state: {e}") return {} def save_state(bucket, key, last_event_time): try: state = {'last_event_time': last_event_time} blob = bucket.blob(key) blob.upload_from_string( json.dumps(state, indent=2), content_type='application/json' ) print(f"Saved state: last_event_time={last_event_time}") except Exception as e: print(f"Warning: Could not save state: {e}") def fetch_alerts(tenant, token, start_time, end_time, page_size, max_records): base_url = tenant.rstrip('/') endpoint = f"{base_url}/api/v2/events/data/alert" headers = { 'Netskope-Api-Token': token, 'Accept': 'application/json', 'User-Agent': 'GoogleSecOps-NetskopeAlertCollector/1.0' } records = [] newest_time = None page_num = 0 backoff = 1.0 start_epoch = to_epoch_seconds(start_time) end_epoch = to_epoch_seconds(end_time) operation_id = None url = f"{endpoint}?limit={page_size}&starttime={start_epoch}&endtime={end_epoch}" while True: page_num += 1 if len(records) >= max_records: print(f"Reached max_records limit ({max_records})") break if operation_id: url = f"{endpoint}?limit={page_size}&operation={operation_id}" try: response = http.request('GET', url, headers=headers) if response.status == 429: retry_after = int(response.headers.get('Retry-After', str(int(backoff)))) print(f"Rate limited (429). Retrying after {retry_after}s...") time.sleep(retry_after) backoff = min(backoff * 2, 30.0) continue backoff = 1.0 if response.status != 200: print(f"HTTP Error: {response.status}") response_text = response.data.decode('utf-8') print(f"Response body: {response_text}") return [], None data = json.loads(response.data.decode('utf-8')) page_results = data.get('result', []) if not page_results: print("No more results (empty page)") break print(f"Page {page_num}: Retrieved {len(page_results)} alerts") records.extend(page_results) for event in page_results: try: event_time = event.get('timestamp') if event_time: if newest_time is None or event_time > newest_time: newest_time = event_time except Exception as e: print(f"Warning: Could not parse event time: {e}") wait_time = data.get('wait_time') operation_id = data.get('operation') if not operation_id or len(page_results) < page_size: break if wait_time and wait_time > 0: time.sleep(wait_time) except Exception as e: print(f"Error fetching alerts: {e}") return [], None print(f"Retrieved {len(records)} total records from {page_num} pages") return records, newest_time- requirements.txt:
functions-framework==3.* google-cloud-storage==2.* urllib3>=2.0.0Click Deploy to save and deploy the function.
Create Cloud Scheduler job
- In the GCP Console, go to Cloud Scheduler.
- Click Create Job.
Provide the following configuration details:
Setting Value Name netskope-alerts-collector-hourlyRegion Select same region as Cloud Run function Frequency 0 * * * *(every hour, on the hour)Timezone Select timezone (UTC recommended) Target type Pub/Sub Topic Select netskope-alerts-triggerMessage body {}Click Create.
Configure a feed in Google SecOps
- Go to SIEM Settings > Feeds.
- Click Add New Feed > Configure a single feed.
- Feed name:
Netskope Alerts. - Source type: Google Cloud Storage V2.
- Log type: Netskope Alert.
- Click Get Service Account and copy the email address provided.
- Click Next.
- Storage bucket URL:
gs://netskope-alerts-logs/netskope/alerts/(Include the trailing slash). - Source deletion option: Select according to your preference (e.g., Never for testing).
- Click Next, review, and click Submit.
Grant IAM permissions to the Google SecOps service account
- Go to Cloud Storage > Buckets.
- Click on
netskope-alerts-logs. - Go to the Permissions tab.
- Click Grant access.
- Add principals: Paste the Google SecOps service account email.
- Assign roles: Select Storage Object Viewer. (Use Storage Object Admin if using a deletion option in the feed).
- Click Save.
UDM mapping table
| Log Field | UDM Mapping | Logic |
|---|---|---|
app_activity_label |
additional.fields |
Merged |
count_label |
additional.fields |
Merged |
object_field |
additional.fields |
Merged |
object_id_field |
additional.fields |
Merged |
object_type_field |
additional.fields |
Merged |
publisher_cn_field |
additional.fields |
Merged |
publisher_name_field |
additional.fields |
Merged |
site_label |
additional.fields |
Merged |
tunnel_id_field |
additional.fields |
Merged |
tunnel_type_field |
additional.fields |
Merged |
access_method |
extensions.auth.auth_details |
Directly mapped |
accessmethod |
extensions.auth.auth_details |
Mapped when result != `` |
activity |
extensions.auth.type |
Mapped: Login Failed → MACHINE, Login Successful → MACHINE, Login Attempt → MACHINE |
published |
metadata.event_timestamp |
Parsed as RFC3339 |
timestamp |
metadata.event_timestamp |
Parsed as RFC3339 |
Protocol |
metadata.event_type |
Mapped: HTTP → NETWORK_HTTP |
activity |
metadata.event_type |
Mapped: Introspection Scan → EMAIL_UNCATEGORIZED, Login Failed → USER_LOGIN, `Login ... |
has_principal |
metadata.event_type |
Mapped: true → NETWORK_HTTP, true → NETWORK_CONNECTION |
Id |
metadata.product_log_id |
Mapped when result != `` |
_id |
metadata.product_log_id |
Directly mapped |
protocol |
network.application_protocol |
Renamed/mapped |
from_user |
network.email.from |
Directly mapped |
useridemail |
network.email.to |
Mapped: @ → useridemail |
user_agent |
network.http.parsed_user_agent |
Renamed/mapped |
useragent |
network.http.parsed_user_agent |
Renamed/mapped |
browser_version |
network.http.parsed_user_agent.browser_version |
Directly mapped |
browserversion |
network.http.parsed_user_agent.browser_version |
Mapped when result != `` |
page |
network.http.referral_url |
Directly mapped |
referer |
network.http.referral_url |
Renamed/mapped |
Browser |
network.http.user_agent |
Mapped when result != `` |
browser |
network.http.user_agent |
Directly mapped |
user_agent |
network.http.user_agent |
Directly mapped |
useragent |
network.http.user_agent |
Directly mapped |
ip_protocol_out |
network.ip_protocol |
Directly mapped |
server_bytes |
network.received_bytes |
Renamed/mapped |
client_bytes |
network.sent_bytes |
Renamed/mapped |
browserSessionid |
network.session_id |
Mapped when result != `` |
browser_sess |
network.session_id |
Renamed/mapped |
network_session_id |
network.session_id |
Renamed/mapped |
ja3 |
network.tls.client.ja3 |
Directly mapped |
ja3s |
network.tls.server.ja3s |
Directly mapped |
netskope_pop |
observer.hostname |
Directly mapped |
organization_unit |
principal.administrative_domain |
Directly mapped |
Host |
principal.asset.hostname |
Mapped when result != `` |
hostname |
principal.asset.hostname |
Directly mapped |
instance_id |
principal.asset.hostname |
Directly mapped |
srcIP |
principal.asset.ip |
Merged |
srcip |
principal.asset.ip |
Merged |
Host |
principal.hostname |
Mapped when result != `` |
hostname |
principal.hostname |
Directly mapped |
instance_id |
principal.hostname |
Directly mapped |
srcIP |
principal.ip |
Merged |
srcip |
principal.ip |
Merged |
src_location |
principal.location.city |
Renamed/mapped |
src_country |
principal.location.country_or_region |
Renamed/mapped |
srccountry |
principal.location.country_or_region |
Mapped when result != `` |
src_region |
principal.location.name |
Renamed/mapped |
srcregion |
principal.location.name |
Mapped when result != `` |
srclatitude |
principal.location.region_coordinates.latitude |
Renamed/mapped |
srclongitude |
principal.location.region_coordinates.longitude |
Renamed/mapped |
src_latitude |
principal.location.region_latitude |
Renamed/mapped |
src_longitude |
principal.location.region_longitude |
Renamed/mapped |
os |
principal.platform |
Mapped: (?i)Windows → WINDOWS, (?i)MAC → MAC, (?i)LINUX → LINUX |
os_version |
principal.platform_version |
Renamed/mapped |
osversion |
principal.platform_version |
Mapped when result != `` |
srcport |
principal.port |
Renamed/mapped |
from_user_category_label |
principal.resource.attribute.labels |
Merged |
Device |
principal.resource.resource_subtype |
Mapped when result != `` |
device |
principal.resource.resource_subtype |
Directly mapped |
User |
principal.user.email_addresses |
Merged |
matched_username |
principal.user.email_addresses |
Merged |
user |
principal.user.email_addresses |
Merged |
user |
principal.user.userid |
Renamed/mapped |
_security_result |
security_result |
Merged |
security_result1 |
security_result |
Merged |
app |
target.application |
Renamed/mapped |
appName |
target.application |
Mapped when result != `` |
domain |
target.asset.hostname |
Directly mapped |
dstIP |
target.asset.ip |
Merged |
dst_ip |
target.asset.ip |
Merged |
dstip |
target.asset.ip |
Merged |
dlp_file |
target.file.full_path |
Directly mapped |
file_path |
target.file.full_path |
Directly mapped |
md5 |
target.file.md5 |
Renamed/mapped |
file_type |
target.file.mime_type |
Directly mapped |
sha256 |
target.file.sha256 |
Renamed/mapped |
file_size |
target.file.size |
Renamed/mapped |
domain |
target.hostname |
Directly mapped |
dstIP |
target.ip |
Merged |
dst_ip |
target.ip |
Merged |
dstip |
target.ip |
Merged |
dst_location |
target.location.city |
Renamed/mapped |
dst_country |
target.location.country_or_region |
Renamed/mapped |
dstcountry |
target.location.country_or_region |
Mapped when result != `` |
dst_region |
target.location.name |
Renamed/mapped |
dstregion |
target.location.name |
Mapped when result != `` |
dstlatitude |
target.location.region_coordinates.latitude |
Renamed/mapped |
dstlongitude |
target.location.region_coordinates.longitude |
Renamed/mapped |
dstport |
target.port |
Renamed/mapped |
app_session_label |
target.resource.attribute.labels |
Merged |
to_user_category_label |
target.resource.attribute.labels |
Merged |
Url |
target.url |
Mapped when result != `` |
url |
target.url |
Renamed/mapped |
to_user |
target.user.email_addresses |
Mapped: ^.+@.+$ → touser |
touser |
target.user.email_addresses |
Mapped: ^.+@.+$ → touser |
| N/A | extensions.auth.type |
Constant: MACHINE |
| N/A | metadata.event_type |
Constant: GENERIC_EVENT |
| N/A | metadata.product_name |
Constant: Netskope Alert |
| N/A | metadata.vendor_name |
Constant: Netskope |
| N/A | network.http.parsed_user_agent.family |
Constant: USER_DEFINED |
| N/A | principal.platform |
Constant: WINDOWS |
| N/A | principal.resource.type |
Constant: DEVICE |
| N/A | target.asset.hostname |
Constant: dsthost |
| N/A | target.hostname |
Constant: dsthost |
Need more help? Get answers from Community members and Google SecOps professionals.