Raccogliere i log IAM di SailPoint

Supportato in:

Questo documento spiega come importare i log di SailPoint Identity and Access Management (IAM) in Google Security Operations utilizzando Amazon S3. Il parser gestisce i log in formato JSON e XML, trasformandoli nel modello Unified Data Model (UDM). Fa distinzione tra eventi UDM singoli (ProvisioningPlan, AccountRequest, SOAP-ENV), eventi UDM multipli (ProvisioningProject) ed entità UDM (Identity), applicando logica di analisi e mappature dei campi specifiche per ciascuno, inclusa la gestione generica degli eventi per i dati non XML.

Prima di iniziare

Assicurati di soddisfare i seguenti prerequisiti:

  • Un'istanza Google SecOps.
  • Accesso con privilegi a SailPoint Identity Security Cloud.
  • Accesso privilegiato ad AWS (S3, IAM, Lambda, EventBridge).

Raccogli i prerequisiti di SailPoint IAM (ID, chiavi API, ID organizzazione, token)

  1. Accedi alla console di amministrazione di SailPoint Identity Security Cloud come amministratore.
  2. Vai a Impostazioni di sicurezza > globali > Gestione API.
  3. Fai clic su Crea client API.
  4. Seleziona Credenziali client come tipo di concessione.
  5. Fornisci i seguenti dettagli di configurazione:
    • Nome: inserisci un nome descrittivo (ad esempio, Google SecOps Export API).
    • Descrizione: inserisci la descrizione del client API.
    • Ambiti: seleziona sp:scopes:all.
  6. Fai clic su Crea e salva le credenziali API generate in una posizione sicura.
  7. Registra l'URL di base del tenant SailPoint (ad esempio, https://tenant.api.identitynow.com).
  8. Copia e salva in una posizione sicura i seguenti dettagli:
    • IDN_CLIENT_ID.
    • IDN_CLIENT_SECRET.
    • IDN_BASE.

Configura il bucket AWS S3 e IAM per Google SecOps

  1. Crea un bucket Amazon S3 seguendo questa guida utente: Creazione di un bucket
  2. Salva il nome e la regione del bucket per riferimento futuro (ad esempio, sailpoint-iam-logs).
  3. Crea un utente seguendo questa guida utente: Creazione di un utente IAM.
  4. Seleziona l'utente creato.
  5. Seleziona la scheda Credenziali di sicurezza.
  6. Fai clic su Crea chiave di accesso nella sezione Chiavi di accesso.
  7. Seleziona Servizio di terze parti come Caso d'uso.
  8. Fai clic su Avanti.
  9. (Facoltativo) Aggiungi il tag della descrizione.
  10. Fai clic su Crea chiave di accesso.
  11. Fai clic su Scarica file CSV per salvare la chiave di accesso e la chiave di accesso segreta per riferimento futuro.
  12. Fai clic su Fine.
  13. Seleziona la scheda Autorizzazioni.
  14. Fai clic su Aggiungi autorizzazioni nella sezione Criteri per le autorizzazioni.
  15. Seleziona Aggiungi autorizzazioni.
  16. Seleziona Allega direttamente i criteri.
  17. Cerca i criteri AmazonS3FullAccess.
  18. Seleziona la policy.
  19. Fai clic su Avanti.
  20. Fai clic su Aggiungi autorizzazioni.

Configura il ruolo e il criterio IAM per i caricamenti S3

  1. Nella console AWS, vai a IAM > Policy.
  2. Fai clic su Crea criterio > scheda JSON.
  3. Copia e incolla i seguenti criteri.
  4. JSON delle policy (sostituisci sailpoint-iam-logs se hai inserito un nome del bucket diverso):

    {
      "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"
        }
      ]
    }
    
  5. Fai clic su Avanti > Crea criterio.

  6. Vai a IAM > Ruoli > Crea ruolo > Servizio AWS > Lambda.

  7. Allega il criterio appena creato.

  8. Assegna al ruolo il nome SailPointIamToS3Role e fai clic su Crea ruolo.

Crea la funzione Lambda

  1. Nella console AWS, vai a Lambda > Funzioni > Crea funzione.
  2. Fai clic su Crea autore da zero.
  3. Fornisci i seguenti dettagli di configurazione:

    Impostazione Valore
    Nome sailpoint_iam_to_s3
    Tempo di esecuzione Python 3.13
    Architettura x86_64
    Ruolo di esecuzione SailPointIamToS3Role
  4. Dopo aver creato la funzione, apri la scheda Codice, elimina lo stub e incolla il seguente codice (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())
    
  5. Vai a Configurazione > Variabili di ambiente.

  6. Fai clic su Modifica > Aggiungi nuova variabile di ambiente.

  7. Inserisci le variabili di ambiente fornite nella tabella seguente, sostituendo i valori di esempio con i tuoi valori.

    Variabili di ambiente

    Chiave Valore di esempio
    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 (dal passaggio 2)
    IDN_CLIENT_SECRET your-client-secret (dal passaggio 2)
    IDN_SCOPE sp:scopes:all
    PAGE_SIZE 250
    MAX_PAGES 20
  8. Dopo aver creato la funzione, rimani sulla relativa pagina (o apri Lambda > Funzioni > la tua funzione).

  9. Seleziona la scheda Configurazione.

  10. Nel riquadro Configurazione generale, fai clic su Modifica.

  11. Modifica Timeout impostando 5 minuti (300 secondi) e fai clic su Salva.

Creare una pianificazione EventBridge

  1. Vai a Amazon EventBridge > Scheduler > Crea pianificazione.
  2. Fornisci i seguenti dettagli di configurazione:
    • Programma ricorrente: Tariffa (1 hour).
    • Target: la tua funzione Lambda sailpoint_iam_to_s3.
    • Nome: sailpoint-iam-1h
  3. Fai clic su Crea pianificazione.

(Facoltativo) Crea chiavi e utenti IAM di sola lettura per Google SecOps

  1. Vai alla console AWS > IAM > Utenti.
  2. Fai clic su Add users (Aggiungi utenti).
  3. Fornisci i seguenti dettagli di configurazione:
    • Utente: inserisci secops-reader.
    • Tipo di accesso: seleziona Chiave di accesso - Accesso programmatico.
  4. Fai clic su Crea utente.
  5. Collega la criterio per la lettura minima (personalizzata): Utenti > secops-reader > Autorizzazioni > Aggiungi autorizzazioni > Collega le norme direttamente > Crea norma.
  6. 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"
        }
      ]
    }
    
  7. Name = secops-reader-policy.

  8. Fai clic su Crea criterio > cerca/seleziona > Avanti > Aggiungi autorizzazioni.

  9. Crea la chiave di accesso per secops-reader: Credenziali di sicurezza > Chiavi di accesso.

  10. Fai clic su Crea chiave di accesso.

  11. Scarica il .CSV. Incollerai questi valori nel feed.

Configura un feed in Google SecOps per importare i log IAM di SailPoint

  1. Vai a Impostazioni SIEM > Feed.
  2. Fai clic su + Aggiungi nuovo feed.
  3. Nel campo Nome feed, inserisci un nome per il feed (ad esempio, SailPoint IAM logs).
  4. Seleziona Amazon S3 V2 come Tipo di origine.
  5. Seleziona SailPoint IAM come Tipo di log.
  6. Fai clic su Avanti.
  7. Specifica i valori per i seguenti parametri di input:
    • URI S3: s3://sailpoint-iam-logs/sailpoint/iam/
    • Opzioni di eliminazione dell'origine: seleziona l'opzione di eliminazione in base alle tue preferenze.
    • Età massima del file: includi i file modificati nell'ultimo numero di giorni. Il valore predefinito è 180 giorni.
    • ID chiave di accesso: chiave di accesso utente con accesso al bucket S3.
    • Chiave di accesso segreta: chiave segreta dell'utente con accesso al bucket S3.
    • Spazio dei nomi dell'asset: lo spazio dei nomi dell'asset.
    • Etichette di importazione: l'etichetta applicata agli eventi di questo feed.
  8. Fai clic su Avanti.
  9. Controlla la nuova configurazione del feed nella schermata Finalizza e poi fai clic su Invia.

Tabella di mappatura UDM

Campo log Mappatura UDM Logic
action metadata.description Il valore del campo action del log non elaborato.
actor.name principal.user.user_display_name Il valore del campo actor.name del log non elaborato.
attributes.accountName principal.user.group_identifiers Il valore del campo attributes.accountName del log non elaborato.
attributes.appId target.asset_id "App ID: " concatenato al valore del campo attributes.appId del log non elaborato.
attributes.attributeName additional.fields[0].value.string_value Il valore del campo attributes.attributeName del log non elaborato, inserito in un oggetto additional.fields. La chiave è impostata su "Nome attributo".
attributes.attributeValue additional.fields[1].value.string_value Il valore del campo attributes.attributeValue del log non elaborato, inserito in un oggetto additional.fields. La chiave è impostata su "Valore attributo".
attributes.cloudAppName target.application Il valore del campo attributes.cloudAppName del log non elaborato.
attributes.hostName target.hostname, target.asset.hostname Il valore del campo attributes.hostName del log non elaborato.
attributes.interface additional.fields[2].value.string_value Il valore del campo attributes.interface del log non elaborato, inserito in un oggetto additional.fields. La chiave è impostata su "Interface".
attributes.operation security_result.action_details Il valore del campo attributes.operation del log non elaborato.
attributes.previousValue additional.fields[3].value.string_value Il valore del campo attributes.previousValue del log non elaborato, inserito in un oggetto additional.fields. La chiave è impostata su "Valore precedente".
attributes.provisioningResult security_result.detection_fields.value Il valore del campo attributes.provisioningResult del log non elaborato, inserito in un oggetto security_result.detection_fields. La chiave è impostata su "Provisioning Result".
attributes.sourceId principal.labels[0].value Il valore del campo attributes.sourceId del log non elaborato, inserito in un oggetto principal.labels. La chiave è impostata su "ID origine".
attributes.sourceName principal.labels[1].value Il valore del campo attributes.sourceName del log non elaborato, inserito in un oggetto principal.labels. La chiave è impostata su "Nome origine".
auditClassName metadata.product_event_type Il valore del campo auditClassName del log non elaborato.
created metadata.event_timestamp.seconds, metadata.event_timestamp.nanos Il valore del campo created del log non elaborato, convertito in timestamp se instant.epochSecond non è presente.
id metadata.product_log_id Il valore del campo id del log non elaborato.
instant.epochSecond metadata.event_timestamp.seconds Il valore del campo instant.epochSecond del log non elaborato, utilizzato per il timestamp.
ipAddress principal.asset.ip, principal.ip Il valore del campo ipAddress del log non elaborato.
interface additional.fields[0].value.string_value Il valore del campo interface del log non elaborato, inserito in un oggetto additional.fields. La chiave è impostata su "interface".
loggerName intermediary.application Il valore del campo loggerName del log non elaborato.
message metadata.description, security_result.description Utilizzato per vari scopi, tra cui l'impostazione della descrizione nei metadati e in security_result e l'estrazione di contenuti XML.
name security_result.description Il valore del campo name del log non elaborato.
operation target.resource.attribute.labels[0].value, metadata.product_event_type Il valore del campo operation del log non elaborato, inserito in un oggetto target.resource.attribute.labels. La chiave è impostata su "operation". Utilizzato anche per metadata.product_event_type.
org principal.administrative_domain Il valore del campo org del log non elaborato.
pod principal.location.name Il valore del campo pod del log non elaborato.
referenceClass additional.fields[1].value.string_value Il valore del campo referenceClass del log non elaborato, inserito in un oggetto additional.fields. La chiave è impostata su "referenceClass".
referenceId additional.fields[2].value.string_value Il valore del campo referenceId del log non elaborato, inserito in un oggetto additional.fields. La chiave è impostata su "referenceId".
sailPointObjectName additional.fields[3].value.string_value Il valore del campo sailPointObjectName del log non elaborato, inserito in un oggetto additional.fields. La chiave è impostata su "sailPointObjectName".
serverHost principal.hostname, principal.asset.hostname Il valore del campo serverHost del log non elaborato.
stack additional.fields[4].value.string_value Il valore del campo stack del log non elaborato, inserito in un oggetto additional.fields. Il tasto è impostato su "Pila".
status security_result.severity_details Il valore del campo status del log non elaborato.
target additional.fields[4].value.string_value Il valore del campo target del log non elaborato, inserito in un oggetto additional.fields. La chiave è impostata su "target".
target.name principal.user.userid Il valore del campo target.name del log non elaborato.
technicalName security_result.summary Il valore del campo technicalName del log non elaborato.
thrown.cause.message xml_body, detailed_message Il valore del campo thrown.cause.message del log non elaborato, utilizzato per estrarre i contenuti XML.
thrown.message xml_body, detailed_message Il valore del campo thrown.message del log non elaborato, utilizzato per estrarre i contenuti XML.
trackingNumber additional.fields[5].value.string_value Il valore del campo trackingNumber del log non elaborato, inserito in un oggetto additional.fields. La chiave è impostata su "Numero di tracciamento".
type metadata.product_event_type Il valore del campo type del log non elaborato.
_version metadata.product_version Il valore del campo _version del log non elaborato.
N/D metadata.event_timestamp Derivato dai campi instant.epochSecond o created.
N/D metadata.event_type Determinato dalla logica del parser in base a vari campi, tra cui has_principal_user, has_target_application, technicalName e action. Il valore predefinito è "GENERIC_EVENT".
N/D metadata.log_type Imposta il valore su "SAILPOINT_IAM".
N/D metadata.product_name Imposta su IAM.
N/D metadata.vendor_name Imposta su "SAILPOINT".
N/D extensions.auth.type Impostato su "AUTHTYPE_UNSPECIFIED" in determinate condizioni.
N/D target.resource.attribute.labels[0].key Imposta il valore su "operation".

Hai bisogno di ulteriore assistenza? Ricevi risposte dai membri della community e dai professionisti di Google SecOps.