Collect Trellix Email Security Cloud (formerly FireEye ETP) logs
This document explains how to ingest Trellix Email Security - Cloud Edition (formerly known as FireEye ETP) logs to Google Security Operations using Google Cloud Storage V2 via a Cloud Run function.
Trellix Email Security - Cloud Edition, is a cloud-based email security gateway that protects against advanced email threats including phishing, malware, business email compromise, and impersonation attacks. The solution provides comprehensive inbound and outbound email security with URL defense, attachment sandboxing, and real-time threat intelligence powered by the Trellix Advanced Research Center.
Before you begin
Make sure that you have the following prerequisites:
- A Google SecOps instance.
- A Google Cloud project with the following APIs enabled:
- Cloud Storage
- Cloud Run functions
- Cloud Scheduler
- Pub/Sub
- Cloud Build
- Permissions to create and manage GCS buckets, Cloud Run functions, Pub/Sub topics, and Cloud Scheduler jobs.
- Privileged access to the FireEye ETP (Trellix Email Security - Cloud Edition) admin console.
- Administrator permissions to create API keys in the Trellix portal.
- A FireEye ETP API key with access to the Alerts endpoint.
Create Google Cloud Storage bucket
- Go to the Google Cloud Console.
- Select your project.
- 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 (e.g., fireeye-etp-logs)Location type Choose based on your needs (Region, Dual-region, Multi-region) Location Select the location (e.g., us-central1)Storage class Standard (recommended for frequently accessed logs) Access control Uniform (recommended) Protection tools Optional: Enable object versioning or retention policy Click Create.
Collect FireEye ETP API credentials
To enable the Cloud Run function to retrieve alerts from FireEye ETP, you need to create an API key with appropriate permissions.
Create API key
- Sign in to the FireEye ETP admin console.
- In the top navigation bar, click My Settings.
- Click the API Keys tab.
- Click Create API Key.
- In the Products section, select Email Threat Prevention.
- In the Entitlements section, select all available entitlements.
- Click Create or Generate.
Record API credentials
Record the following information:
- API Key: Your unique API key.
- Base URL: The fully qualified domain name for your region (e.g.,
etp.us.fireeye.com).
Common base URLs include:
etp.us.fireeye.com(US)etp.eu.fireeye.com(EU)etp.ap.fireeye.com(APAC)
Verify API permissions
| Product/Entitlement | Purpose |
|---|---|
| Email Threat Prevention | Access to email alerts, trace data, and threat information |
| All Entitlements | Full access to alerts, email trace, and quarantine APIs |
Test API access
Verify that your API key is valid:
curl -s -o /dev/null -w "%{http_code}" \ -H "x-fireeye-api-key: YOUR_API_KEY" \ "[https://etp.us.fireeye.com/api/v1/alerts?size=1](https://etp.us.fireeye.com/api/v1/alerts?size=1)"
A 200 response code confirms that the API key is valid.
Create service account for Cloud Run function
The Cloud Run function requires a service account with permissions to write logs to GCS and receive Pub/Sub messages.
- In the Google Cloud Console, go to IAM & Admin > Service Accounts.
- Click Create Service Account.
- Provide a name (e.g.,
fireeye-etp-ingestion) and description. - Grant the following roles:
- Storage Object Admin
- Cloud Run Invoker
- Click Done.
Create Pub/Sub topic
- In the Google Cloud Console, go to Pub/Sub > Topics.
- Click Create Topic.
- Topic ID:
fireeye-etp-trigger. - Click Create.
Create Cloud Run function
Create a Cloud Run function that queries the FireEye ETP Alerts API and writes results as NDJSON to GCS.
Prepare the function code
- In the Google Cloud Console, go to Cloud Run functions.
- Click Create function.
- Environment: 2nd gen.
- Function name:
fireeye-etp-ingestion. - Region: Select the same region as your GCS bucket.
- Trigger type: Cloud Pub/Sub.
- Cloud Pub/Sub topic:
fireeye-etp-trigger. - Service account: Select the service account created earlier.
- Click Next.
- Runtime: Python 3.11 (or later).
- Entry point:
main. In main.py, paste the following:
"""Cloud Run function to ingest FireEye ETP alerts into GCS.""" import json import os import time from datetime import datetime, timedelta, timezone import functions_framework import urllib3 from google.cloud import storage GCS_BUCKET = os.environ["GCS_BUCKET"] GCS_PREFIX = os.environ.get("GCS_PREFIX", "fireeye_etp") STATE_KEY = os.environ.get("STATE_KEY", "fireeye_etp_state.json") API_KEY = os.environ["API_KEY"] API_BASE = os.environ.get("API_BASE", "etp.us.fireeye.com") MAX_RECORDS = int(os.environ.get("MAX_RECORDS", "10000")) PAGE_SIZE = int(os.environ.get("PAGE_SIZE", "100")) LOOKBACK_HOURS = int(os.environ.get("LOOKBACK_HOURS", "1")) http = urllib3.PoolManager() gcs = storage.Client() def _load_state() -> dict: """Load the last event time from GCS state file.""" bucket = gcs.bucket(GCS_BUCKET) blob = bucket.blob(f"{GCS_PREFIX}/{STATE_KEY}") if blob.exists(): return json.loads(blob.download_as_text()) return {} def _save_state(state: dict) -> None: """Persist the state dict back to GCS.""" bucket = gcs.bucket(GCS_BUCKET) blob = bucket.blob(f"{GCS_PREFIX}/{STATE_KEY}") blob.upload_from_string( json.dumps(state), content_type="application/json" ) def _api_get(path: str, params: dict, retries: int = 5) -> dict: """Execute a GET request against the FireEye ETP API with retry on 429.""" url = f"https://{API_BASE}{path}" headers = { "x-fireeye-api-key": API_KEY, "Accept": "application/json", } backoff = 2 for attempt in range(retries): resp = http.request( "GET", url, headers=headers, fields=params ) if resp.status == 200: return json.loads(resp.data.decode("utf-8")) if resp.status == 429: wait = backoff * (2 ** attempt) print( f"Rate limited (429). Retrying in {wait}s " f"(attempt {attempt + 1}/{retries})." ) time.sleep(wait) continue raise RuntimeError( f"FireEye ETP API error: {resp.status} — {resp.data.decode('utf-8')}" ) raise RuntimeError( "FireEye ETP API rate limit exceeded after maximum retries." ) def _fetch_alerts(since: str) -> list: """Fetch alerts from FireEye ETP with offset-based pagination.""" all_alerts = [] offset = 0 while len(all_alerts) < MAX_RECORDS: params = { "from_last_modified_on": since, "size": str(PAGE_SIZE), "offset": str(offset), } data = _api_get("/api/v1/alerts", params) alerts = data.get("data", []) if not alerts: break all_alerts.extend(alerts) offset += len(alerts) if len(alerts) < PAGE_SIZE: break return all_alerts[:MAX_RECORDS] def _write_ndjson(alerts: list, run_ts: str) -> str: """Write alerts as NDJSON to GCS and return the blob path.""" bucket = gcgcs.bucketCS_BUCKET) blob_path = ( f"{GCS_PREFIX}/year={run_ts[:4]}/month={run_ts[5:7]}/" f"day={run_ts[8:10]}/{run_ts}_alerts.ndjson" ) blob = bucket.blob(blob_path) ndjson = "\n".join(json.dumps(a, separators=(",", ":")) for a in alerts) blob.upupload_from_stringdjson, content_type="application/x-ndjson") return blob_path @functions_framework.cloud_event def main(cloud_event): """Entry point triggered by Pub/Sub via Cloud Scheduler.""" state = _load_state() now = datetime.now(timezone.utc) since = ststateet( "last_event_time", (now - timedelta(hours=LOOKBACK_HOURS)).strftime( "%Y-%m-%dT%H:%M:%S.000" ), ) print(f"Fetching FireEye ETP alerts since {since}.") alerts = _fetch_alerts(since) if not alerts: print("No new alerts found.") return "OK" run_ts = now.strftime("%Y-%m-%dT%H%M%SZ") blob_path = _write_ndjson(alerts, run_ts) print(f"Wrote {len(alerts)} alerts to gs://{GCS_BUCKET}/{blob_path}.") latest = max( a.get("attributes", {}) .get("meta", {}) .get("last_modified_on", since) for a in alerts ) state["last_event_time"] = latest _save_state(state) print(f"State updated. last_event_time={latest}.") return "OK"In requirements.txt, paste:
functions-framework==3.* google-cloud-storage==2.* urllib3==2.*
Configure environment variables
Under Runtime environment variables, add the following:
| Variable | Example Value |
|---|---|
GCS_BUCKET |
fireeye-etp-logs |
GCS_PREFIX |
fireeye_etp |
STATE_KEY |
fireeye_etp_state.json |
API_KEY |
Your FireEye ETP API key |
API_BASE |
etp.us.fireeye.com |
MAX_RECORDS |
10000 |
PAGE_SIZE |
100 |
LOOKBACK_HOURS |
1 |
- Set Memory allocated to 256 MB and Timeout to 540 seconds.
- Click Deploy.
Create Cloud Scheduler job
- In the Google Cloud Console, go to Cloud Scheduler.
- Click Create Job.
- Name:
fireeye-etp-ingestion-schedule. - Frequency:
*/5 * * * *(every 5 minutes). - Timezone:
UTC. - Click Continue.
- Target type: Pub/Sub.
- Cloud Pub/Sub topic:
fireeye-etp-trigger. - Message body:
{"run": true}. - Click Create.
Retrieve the Google SecOps service account
- Go to SIEM Settings > Feeds.
- Click Add New Feed and select Configure a single feed.
- Feed name:
FireEye ETP Alerts. - Source type: Google Cloud Storage V2.
- Log type: FireEye ETP.
- Click Get Service Account and copy the email displayed.
- Click Next.
- Storage bucket URL:
gs://fireeye-etp-logs/fireeye_etp/(Include the trailing slash). - Source deletion option: Select according to your preference.
- Click Next, review, and click Submit.
Grant IAM permissions
The service account needs Storage Object Viewer role on your bucket.
- Go to Cloud Storage > Buckets.
- Select your bucket and click the Permissions tab.
- Click Grant access.
- Add principals: Paste the Google SecOps service account email.
- Assign roles: Select Storage Object Viewer.
- Click Save.
UDM mapping table
| Log Field | UDM Mapping | Logic |
|---|---|---|
| about | Merged from about_field | |
| about.file.full_path | Value from entry.attributes.email.attachment if not empty | |
| about.file.md5 | Value from entry.attributes.alert.malware_md5 | |
| about.hostname | Extracted from entry.attributes.email.attachment | |
| about.url | Normalized full_path if starts with hxxp | |
| additional.fields | Merged from numerous internal labels and metadata fields | |
| intermediary | Merged from intermediary | |
| intermediary.ip | Merged from alert.smtp-message.ip_address | |
| intermediary.location.country_or_region | Set to %{alert.smtp-message.country} | |
| intermediary.user.email_addresses | Merged from src_email | |
| metadata.description | Set to %{alert.name} | |
| metadata.event_timestamp | Matched from accepted_timestamp, last_modified_on, or attack-time | |
| metadata.event_type | Categorized based on network, user, or status update conditions | |
| metadata.product_log_id | Set to message ID, attribute ID, or alert UUID | |
| metadata.product_version | Set to %{version} | |
| metadata.url_back_to_product | Set to %{entry.links.detail} | |
| network.application_protocol | Set to "SMTP" | |
| network.direction | Uppercased traffic_type (INBOUND/OUTBOUND) | |
| network.dns_domain | Set to %{attributes.domain} | |
| network.email.cc | Merged from cc fields in headers or arrays | |
| network.email.from | Set from SMTP or header "from" fields | |
| network.email.mail_id | Set from downstream or original message ID | |
| network.email.subject | Merged from header subject fields | |
| network.email.to | Merged from SMTP recipients or header "to" fields | |
| principal.administrative_domain | Set to ETP message ID or source domain | |
| principal.asset.hostname | Set to source host or downstream hostname | |
| principal.asset.ip | Merged from sender_ip or source_ip fields | |
| principal.asset.mac | Merged from source MAC if not empty | |
| principal.hostname | Set to source host or downstream hostname | |
| principal.ip | Merged from sender_ip or source_ip fields | |
| principal.labels | Merged with mail_from and rcpt_to labels | |
| principal.location.country_or_region | Set to source country code | |
| principal.mac | Merged from source MAC if not empty | |
| principal.user.email_addresses | Merged from alert.smtp-message.from | |
| principal.user.user_display_name | Set from header display name fields | |
| principal.user.userid | Set to original message ID | |
| principal.email | Set from header email fields | |
| security_result | Merged from multiple result structures | |
| security_result.about.resource.attribute.creation_time | Matched from alert timestamp fields | |
| security_result.action | "BLOCK" if status is dropped, quarantined, or deleted | |
| security_result.action_details | Set to %{alert.action} | |
| security_result.attack_details.tactics | Merged based on MITRE IDs or names | |
| security_result.attack_details.techniques | Merged based on MITRE technique data | |
| security_result.category | "MAIL_PHISHING" if threat_type matches Phishing | |
| security_result.category_details | Merged from threat_type or verdict | |
| security_result.detection_fields | Merged from numerous status and verdict labels | |
| security_result.risk_score | Set to 5.0 or 10.0 based on severity | |
| security_result.severity | Set to high/medium/low based on alert severity | |
| security_result.severity_details | Set to %{alert_severity} | |
| security_result.summary | Set from last_malware or downstream description | |
| security_result.threat_id | Set from legacy ID or trace ID | |
| security_result.threat_name | Set from summary or malware name | |
| security_result.verdict_info | Merged if verdict is RISKWARE | |
| target.asset.hostname | Set from delivery message hostname | |
| target.file.first_seen_time | Matched from alert attack-time | |
| target.file.first_submission_time | Matched from malware submission time | |
| target.file.full_path | Set from original malware path | |
| target.file.md5 | Set from malware MD5 sum | |
| target.file.names | Merged from malware name | |
| target.file.sha256 | Set from malware SHA256 | |
| target.file.size | Set from delivery message bytes | |
| target.hostname | Set from delivery message hostname | |
| target.labels | Merged with queue and protocol labels | |
| target.url | Set from original malware URL | |
| target.user.email_addresses | Merged from target email and recipient fields | |
| metadata.vendor_name | Set to "FireEye" | |
| metadata.product_name | Set to "ETP" |
Need more help? Get answers from Community members and Google SecOps professionals.