Mengumpulkan log SailPoint IAM
Dokumen ini menjelaskan cara Anda dapat menyerap log Identity and Access Management (IAM) SailPoint ke Google Security Operations menggunakan Amazon S3. Parser menangani log dalam format JSON dan XML, lalu mengubahnya menjadi Model Data Terpadu (UDM). UDM membedakan antara peristiwa UDM tunggal (ProvisioningPlan, AccountRequest, SOAP-ENV), peristiwa multi-UDM (ProvisioningProject), dan entitas UDM (Identity), dengan menerapkan logika parsing dan pemetaan kolom tertentu untuk setiap peristiwa, termasuk penanganan peristiwa generik untuk data non-XML.
Sebelum memulai
Pastikan Anda memenuhi prasyarat berikut:
- Instance Google SecOps.
- Akses istimewa ke SailPoint Identity Security Cloud.
- Akses istimewa ke AWS (S3, IAM, Lambda, EventBridge).
Kumpulkan prasyarat SailPoint IAM (ID, kunci API, ID organisasi, token)
- Login ke Konsol Admin SailPoint Identity Security Cloud sebagai administrator.
- Buka Global > Setelan Keamanan > Pengelolaan API.
- Klik Create API Client.
- Pilih Client Credentials sebagai jenis pemberian.
- Berikan detail konfigurasi berikut:
- Nama: Masukkan nama deskriptif (misalnya,
Google SecOps Export API
). - Deskripsi: Masukkan deskripsi untuk klien API.
- Cakupan: Pilih
sp:scopes:all
.
- Nama: Masukkan nama deskriptif (misalnya,
- Klik Buat dan simpan kredensial API yang dibuat di lokasi yang aman.
- Catat URL dasar tenant SailPoint Anda (misalnya,
https://tenant.api.identitynow.com
). - Salin dan simpan detail berikut di lokasi yang aman:
- IDN_CLIENT_ID.
- IDN_CLIENT_SECRET.
- IDN_BASE.
Mengonfigurasi bucket AWS S3 dan IAM untuk Google SecOps
- Buat bucket Amazon S3 dengan mengikuti panduan pengguna ini: Membuat bucket
- Simpan Name dan Region bucket untuk referensi di masa mendatang (misalnya,
sailpoint-iam-logs
). - Buat Pengguna dengan mengikuti panduan pengguna ini: Membuat pengguna IAM.
- Pilih Pengguna yang dibuat.
- Pilih tab Kredensial keamanan.
- Klik Create Access Key di bagian Access Keys.
- Pilih Layanan pihak ketiga sebagai Kasus penggunaan.
- Klik Berikutnya.
- Opsional: Tambahkan tag deskripsi.
- Klik Create access key.
- Klik Download CSV file untuk menyimpan Access Key dan Secret Access Key untuk referensi di masa mendatang.
- Klik Selesai.
- Pilih tab Permissions.
- Klik Tambahkan izin di bagian Kebijakan izin.
- Pilih Tambahkan izin.
- Pilih Lampirkan kebijakan secara langsung.
- Cari kebijakan AmazonS3FullAccess.
- Pilih kebijakan.
- Klik Berikutnya.
- Klik Add permissions.
Mengonfigurasi kebijakan dan peran IAM untuk upload S3
- Di konsol AWS, buka IAM > Policies.
- Klik Buat kebijakan > tab JSON.
- Salin dan tempel kebijakan berikut.
Policy JSON (ganti
sailpoint-iam-logs
jika Anda memasukkan nama bucket yang berbeda):{ "Version": "2012-10-17", "Statement": [ { "Sid": "AllowPutObjects", "Effect": "Allow", "Action": "s3:PutObject", "Resource": "arn:aws:s3:::sailpoint-iam-logs/*" }, { "Sid": "AllowGetStateObject", "Effect": "Allow", "Action": "s3:GetObject", "Resource": "arn:aws:s3:::sailpoint-iam-logs/sailpoint/iam/state.json" } ] }
Klik Berikutnya > Buat kebijakan.
Buka IAM > Roles > Create role > AWS service > Lambda.
Lampirkan kebijakan yang baru dibuat.
Beri nama peran
SailPointIamToS3Role
, lalu klik Buat peran.
Buat fungsi Lambda
- Di Konsol AWS, buka Lambda > Functions > Create function.
- Klik Buat dari awal.
Berikan detail konfigurasi berikut:
Setelan Nilai Nama sailpoint_iam_to_s3
Runtime Python 3.13 Arsitektur x86_64 Peran eksekusi SailPointIamToS3Role
Setelah fungsi dibuat, buka tab Code, hapus stub, dan tempelkan kode berikut (
sailpoint_iam_to_s3.py
).#!/usr/bin/env python3 # Lambda: Pull SailPoint Identity Security Cloud audit events and store raw JSON payloads to S3 # - Uses /v3/search API with pagination for audit events. # - Preserves vendor-native JSON format for identity events. # - Retries with exponential backoff; unique S3 keys to avoid overwrites. import os, json, time, uuid, urllib.parse from urllib.request import Request, urlopen from urllib.error import URLError, HTTPError import boto3 S3_BUCKET = os.environ["S3_BUCKET"] S3_PREFIX = os.environ.get("S3_PREFIX", "sailpoint/iam/") STATE_KEY = os.environ.get("STATE_KEY", "sailpoint/iam/state.json") WINDOW_SEC = int(os.environ.get("WINDOW_SECONDS", "3600")) # default 1h HTTP_TIMEOUT= int(os.environ.get("HTTP_TIMEOUT", "60")) IDN_BASE = os.environ["IDN_BASE"] # e.g. https://tenant.api.identitynow.com CLIENT_ID = os.environ["IDN_CLIENT_ID"] CLIENT_SECRET = os.environ["IDN_CLIENT_SECRET"] SCOPE = os.environ.get("IDN_SCOPE", "sp:scopes:all") PAGE_SIZE = int(os.environ.get("PAGE_SIZE", "250")) MAX_PAGES = int(os.environ.get("MAX_PAGES", "20")) MAX_RETRIES = int(os.environ.get("MAX_RETRIES", "3")) USER_AGENT = os.environ.get("USER_AGENT", "sailpoint-iam-to-s3/1.0") s3 = boto3.client("s3") def _load_state(): try: obj = s3.get_object(Bucket=S3_BUCKET, Key=STATE_KEY) return json.loads(obj["Body"].read()) except Exception: return {} def _save_state(st): s3.put_object( Bucket=S3_BUCKET, Key=STATE_KEY, Body=json.dumps(st, separators=(",", ":")).encode("utf-8"), ContentType="application/json", ) def _iso(ts: float) -> str: return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(ts)) def _get_oauth_token() -> str: """Get OAuth2 access token using Client Credentials flow""" token_url = f"{IDN_BASE.rstrip('/')}/oauth/token" data = urllib.parse.urlencode({ 'grant_type': 'client_credentials', 'client_id': CLIENT_ID, 'client_secret': CLIENT_SECRET, 'scope': SCOPE }).encode('utf-8') req = Request(token_url, data=data, method="POST") req.add_header("Content-Type", "application/x-www-form-urlencoded") req.add_header("User-Agent", USER_AGENT) with urlopen(req, timeout=HTTP_TIMEOUT) as r: response = json.loads(r.read()) return response["access_token"] def _search_events(access_token: str, created_from: str, search_after: list = None) -> list: """Search for audit events using SailPoint's /v3/search API""" search_url = f"{IDN_BASE.rstrip('/')}/v3/search" # Build search query for events created after specified time query_str = f'created:">={created_from}"' payload = { "indices": ["events"], "query": {"query": query_str}, "sort": ["created", "+id"], "limit": PAGE_SIZE } if search_after: payload["searchAfter"] = search_after attempt = 0 while True: req = Request(search_url, data=json.dumps(payload).encode('utf-8'), method="POST") req.add_header("Content-Type", "application/json") req.add_header("Accept", "application/json") req.add_header("Authorization", f"Bearer {access_token}") req.add_header("User-Agent", USER_AGENT) try: with urlopen(req, timeout=HTTP_TIMEOUT) as r: response = json.loads(r.read()) # Handle different response formats if isinstance(response, list): return response return response.get("results", response.get("data", [])) except (HTTPError, URLError) as e: attempt += 1 print(f"HTTP error on attempt {attempt}: {e}") if attempt > MAX_RETRIES: raise # exponential backoff with jitter time.sleep(min(60, 2 ** attempt) + (time.time() % 1)) def _put_events_data(events: list, from_ts: float, to_ts: float, page_num: int) -> str: # Create unique S3 key for events data ts_path = time.strftime("%Y/%m/%d", time.gmtime(to_ts)) uniq = f"{int(time.time()*1e6)}_{uuid.uuid4().hex[:8]}" key = f"{S3_PREFIX}{ts_path}/sailpoint_iam_{int(from_ts)}_{int(to_ts)}_p{page_num:03d}_{uniq}.json" s3.put_object( Bucket=S3_BUCKET, Key=key, Body=json.dumps(events, separators=(",", ":")).encode("utf-8"), ContentType="application/json", Metadata={ 'source': 'sailpoint-iam', 'from_timestamp': str(int(from_ts)), 'to_timestamp': str(int(to_ts)), 'page_number': str(page_num), 'events_count': str(len(events)) } ) return key def _get_item_id(item: dict) -> str: """Extract ID from event item, trying multiple possible fields""" for field in ("id", "uuid", "eventId", "_id"): if field in item and item[field]: return str(item[field]) return "" def lambda_handler(event=None, context=None): st = _load_state() now = time.time() from_ts = float(st.get("last_to_ts") or (now - WINDOW_SEC)) to_ts = now # Get OAuth token access_token = _get_oauth_token() created_from = _iso(from_ts) print(f"Fetching SailPoint IAM events from: {created_from}") # Handle pagination state last_created = st.get("last_created") last_id = st.get("last_id") search_after = [last_created, last_id] if (last_created and last_id) else None pages = 0 total_events = 0 written_keys = [] newest_created = last_created or created_from newest_id = last_id or "" while pages < MAX_PAGES: events = _search_events(access_token, created_from, search_after) if not events: break # Write page to S3 key = _put_events_data(events, from_ts, to_ts, pages + 1) written_keys.append(key) total_events += len(events) # Update pagination state from last item last_event = events[-1] last_event_created = last_event.get("created") or last_event.get("metadata", {}).get("created") last_event_id = _get_item_id(last_event) if last_event_created: newest_created = last_event_created if last_event_id: newest_id = last_event_id search_after = [newest_created, newest_id] pages += 1 # If we got less than page size, we're done if len(events) < PAGE_SIZE: break print(f"Successfully retrieved {total_events} events across {pages} pages") # Save state for next run st["last_to_ts"] = to_ts st["last_created"] = newest_created st["last_id"] = newest_id st["last_successful_run"] = now _save_state(st) return { "statusCode": 200, "body": { "success": True, "pages": pages, "total_events": total_events, "s3_keys": written_keys, "from_timestamp": from_ts, "to_timestamp": to_ts, "last_created": newest_created, "last_id": newest_id } } if __name__ == "__main__": print(lambda_handler())
Buka Configuration > Environment variables.
Klik Edit > Tambahkan variabel lingkungan baru.
Masukkan variabel lingkungan yang diberikan dalam tabel berikut, dengan mengganti nilai contoh dengan nilai Anda.
Variabel lingkungan
Kunci Nilai contoh S3_BUCKET
sailpoint-iam-logs
S3_PREFIX
sailpoint/iam/
STATE_KEY
sailpoint/iam/state.json
WINDOW_SECONDS
3600
HTTP_TIMEOUT
60
MAX_RETRIES
3
USER_AGENT
sailpoint-iam-to-s3/1.0
IDN_BASE
https://tenant.api.identitynow.com
IDN_CLIENT_ID
your-client-id
(dari langkah 2)IDN_CLIENT_SECRET
your-client-secret
(dari langkah 2)IDN_SCOPE
sp:scopes:all
PAGE_SIZE
250
MAX_PAGES
20
Setelah fungsi dibuat, tetap buka halamannya (atau buka Lambda > Functions > your-function).
Pilih tab Configuration
Di panel Konfigurasi umum, klik Edit.
Ubah Waktu Tunggu menjadi 5 menit (300 detik), lalu klik Simpan.
Membuat jadwal EventBridge
- Buka Amazon EventBridge > Scheduler > Create schedule.
- Berikan detail konfigurasi berikut:
- Jadwal berulang: Tarif (
1 hour
). - Target: Fungsi Lambda Anda
sailpoint_iam_to_s3
. - Name:
sailpoint-iam-1h
.
- Jadwal berulang: Tarif (
- Klik Buat jadwal.
(Opsional) Buat pengguna & kunci IAM hanya baca untuk Google SecOps
- Buka Konsol AWS > IAM > Pengguna.
- Klik Add users.
- Berikan detail konfigurasi berikut:
- Pengguna: Masukkan
secops-reader
. - Jenis akses: Pilih Kunci akses – Akses terprogram.
- Pengguna: Masukkan
- Klik Buat pengguna.
- Lampirkan kebijakan baca minimal (kustom): Pengguna > secops-reader > Izin > Tambahkan izin > Lampirkan kebijakan secara langsung > Buat kebijakan.
JSON:
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": ["s3:GetObject"], "Resource": "arn:aws:s3:::sailpoint-iam-logs/*" }, { "Effect": "Allow", "Action": ["s3:ListBucket"], "Resource": "arn:aws:s3:::sailpoint-iam-logs" } ] }
Nama =
secops-reader-policy
.Klik Buat kebijakan > cari/pilih > Berikutnya > Tambahkan izin.
Buat kunci akses untuk
secops-reader
: Kredensial keamanan > Kunci akses.Klik Create access key.
Download
.CSV
. (Anda akan menempelkan nilai ini ke feed).
Mengonfigurasi feed di Google SecOps untuk memproses log IAM SailPoint
- Buka Setelan SIEM > Feed.
- Klik + Tambahkan Feed Baru.
- Di kolom Nama feed, masukkan nama untuk feed (misalnya,
SailPoint IAM logs
). - Pilih Amazon S3 V2 sebagai Jenis sumber.
- Pilih SailPoint IAM sebagai Jenis log.
- Klik Berikutnya.
- Tentukan nilai untuk parameter input berikut:
- URI S3:
s3://sailpoint-iam-logs/sailpoint/iam/
- Opsi penghapusan sumber: Pilih opsi penghapusan sesuai preferensi Anda.
- Usia File Maksimum: Menyertakan file yang diubah dalam jumlah hari terakhir. Defaultnya adalah 180 hari.
- ID Kunci Akses: Kunci akses pengguna dengan akses ke bucket S3.
- Kunci Akses Rahasia: Kunci rahasia pengguna dengan akses ke bucket S3.
- Namespace aset: Namespace aset.
- Label penyerapan: Label yang diterapkan ke peristiwa dari feed ini.
- URI S3:
- Klik Berikutnya.
- Tinjau konfigurasi feed baru Anda di layar Selesaikan, lalu klik Kirim.
Tabel pemetaan UDM
Kolom log | Pemetaan UDM | Logika |
---|---|---|
action |
metadata.description |
Nilai kolom action dari log mentah. |
actor.name |
principal.user.user_display_name |
Nilai kolom actor.name dari log mentah. |
attributes.accountName |
principal.user.group_identifiers |
Nilai kolom attributes.accountName dari log mentah. |
attributes.appId |
target.asset_id |
"ID Aplikasi: " digabungkan dengan nilai kolom attributes.appId dari log mentah. |
attributes.attributeName |
additional.fields[0].value.string_value |
Nilai kolom attributes.attributeName dari log mentah, ditempatkan dalam objek additional.fields . Kunci ditetapkan ke "Nama Atribut". |
attributes.attributeValue |
additional.fields[1].value.string_value |
Nilai kolom attributes.attributeValue dari log mentah, ditempatkan dalam objek additional.fields . Kuncinya ditetapkan ke "Nilai Atribut". |
attributes.cloudAppName |
target.application |
Nilai kolom attributes.cloudAppName dari log mentah. |
attributes.hostName |
target.hostname , target.asset.hostname |
Nilai kolom attributes.hostName dari log mentah. |
attributes.interface |
additional.fields[2].value.string_value |
Nilai kolom attributes.interface dari log mentah, ditempatkan dalam objek additional.fields . Kuncinya ditetapkan ke "Interface". |
attributes.operation |
security_result.action_details |
Nilai kolom attributes.operation dari log mentah. |
attributes.previousValue |
additional.fields[3].value.string_value |
Nilai kolom attributes.previousValue dari log mentah, ditempatkan dalam objek additional.fields . Kuncinya ditetapkan ke "Nilai Sebelumnya". |
attributes.provisioningResult |
security_result.detection_fields.value |
Nilai kolom attributes.provisioningResult dari log mentah, ditempatkan dalam objek security_result.detection_fields . Kuncinya ditetapkan ke "Provisioning Result". |
attributes.sourceId |
principal.labels[0].value |
Nilai kolom attributes.sourceId dari log mentah, ditempatkan dalam objek principal.labels . Kuncinya ditetapkan ke "ID Sumber". |
attributes.sourceName |
principal.labels[1].value |
Nilai kolom attributes.sourceName dari log mentah, ditempatkan dalam objek principal.labels . Kuncinya ditetapkan ke "Nama Sumber". |
auditClassName |
metadata.product_event_type |
Nilai kolom auditClassName dari log mentah. |
created |
metadata.event_timestamp.seconds , metadata.event_timestamp.nanos |
Nilai kolom created dari log mentah, dikonversi menjadi stempel waktu jika instant.epochSecond tidak ada. |
id |
metadata.product_log_id |
Nilai kolom id dari log mentah. |
instant.epochSecond |
metadata.event_timestamp.seconds |
Nilai kolom instant.epochSecond dari log mentah, yang digunakan untuk stempel waktu. |
ipAddress |
principal.asset.ip , principal.ip |
Nilai kolom ipAddress dari log mentah. |
interface |
additional.fields[0].value.string_value |
Nilai kolom interface dari log mentah, ditempatkan dalam objek additional.fields . Kuncinya ditetapkan ke "interface". |
loggerName |
intermediary.application |
Nilai kolom loggerName dari log mentah. |
message |
metadata.description , security_result.description |
Digunakan untuk berbagai tujuan, termasuk menyetel deskripsi dalam metadata dan security_result, serta mengekstrak konten XML. |
name |
security_result.description |
Nilai kolom name dari log mentah. |
operation |
target.resource.attribute.labels[0].value , metadata.product_event_type |
Nilai kolom operation dari log mentah, ditempatkan dalam objek target.resource.attribute.labels . Kuncinya ditetapkan ke "operation". Juga digunakan untuk metadata.product_event_type . |
org |
principal.administrative_domain |
Nilai kolom org dari log mentah. |
pod |
principal.location.name |
Nilai kolom pod dari log mentah. |
referenceClass |
additional.fields[1].value.string_value |
Nilai kolom referenceClass dari log mentah, ditempatkan dalam objek additional.fields . Kuncinya ditetapkan ke "referenceClass". |
referenceId |
additional.fields[2].value.string_value |
Nilai kolom referenceId dari log mentah, ditempatkan dalam objek additional.fields . Kunci ditetapkan ke "referenceId". |
sailPointObjectName |
additional.fields[3].value.string_value |
Nilai kolom sailPointObjectName dari log mentah, ditempatkan dalam objek additional.fields . Kuncinya ditetapkan ke "sailPointObjectName". |
serverHost |
principal.hostname , principal.asset.hostname |
Nilai kolom serverHost dari log mentah. |
stack |
additional.fields[4].value.string_value |
Nilai kolom stack dari log mentah, ditempatkan dalam objek additional.fields . Kuncinya ditetapkan ke "Stack". |
status |
security_result.severity_details |
Nilai kolom status dari log mentah. |
target |
additional.fields[4].value.string_value |
Nilai kolom target dari log mentah, ditempatkan dalam objek additional.fields . Kuncinya ditetapkan ke "target". |
target.name |
principal.user.userid |
Nilai kolom target.name dari log mentah. |
technicalName |
security_result.summary |
Nilai kolom technicalName dari log mentah. |
thrown.cause.message |
xml_body , detailed_message |
Nilai kolom thrown.cause.message dari log mentah, yang digunakan untuk mengekstrak konten XML. |
thrown.message |
xml_body , detailed_message |
Nilai kolom thrown.message dari log mentah, yang digunakan untuk mengekstrak konten XML. |
trackingNumber |
additional.fields[5].value.string_value |
Nilai kolom trackingNumber dari log mentah, ditempatkan dalam objek additional.fields . Kuncinya ditetapkan ke "Nomor Pelacakan". |
type |
metadata.product_event_type |
Nilai kolom type dari log mentah. |
_version |
metadata.product_version |
Nilai kolom _version dari log mentah. |
T/A | metadata.event_timestamp |
Diperoleh dari kolom instant.epochSecond atau created . |
T/A | metadata.event_type |
Ditentukan oleh logika parser berdasarkan berbagai kolom, termasuk has_principal_user , has_target_application , technicalName , dan action . Nilai defaultnya adalah "GENERIC_EVENT". |
T/A | metadata.log_type |
Tetapkan ke "SAILPOINT_IAM". |
T/A | metadata.product_name |
Tetapkan ke IAM . |
T/A | metadata.vendor_name |
Tetapkan ke "SAILPOINT". |
T/A | extensions.auth.type |
Disetel ke "AUTHTYPE_UNSPECIFIED" dalam kondisi tertentu. |
T/A | target.resource.attribute.labels[0].key |
Tetapkan ke "operation". |
Perlu bantuan lain? Dapatkan jawaban dari anggota Komunitas dan profesional Google SecOps.