Recopila registros de SSO de Delinea
En este documento, se explica cómo transferir registros de inicio de sesión único (SSO) de Delinea (antes Centrify) a Google Security Operations con Amazon S3. El analizador extrae los registros y controla los formatos JSON y syslog. Analiza pares clave-valor, marcas de tiempo y otros campos relevantes, y los asigna al modelo de UDM con lógica específica para controlar errores de acceso, agentes de usuario, niveles de gravedad, mecanismos de autenticación y varios tipos de eventos. Prioriza FailUserName
sobre NormalizedUser
para las direcciones de correo electrónico de destino en los eventos de error.
Antes de comenzar
Asegúrate de cumplir con los siguientes requisitos previos:
- Es una instancia de Google SecOps.
- Acceso con privilegios al arrendatario de SSO de Delinea (Centrify)
- Acceso con privilegios a AWS (S3, Identity and Access Management [IAM], Lambda, EventBridge).
Recopila los requisitos previos del SSO de Delinea (Centrify) (IDs, claves de API, IDs de organización, tokens)
- Accede al Delinea Admin Portal.
- Ve a Apps > Agregar apps.
- Busca OAuth2 Client y haz clic en Add.
- Haz clic en Sí en el diálogo Agregar aplicación web.
- Haz clic en Cerrar en el cuadro de diálogo Agregar apps web.
- En la página Configuración de la aplicación, establece los siguientes parámetros de configuración:
- Pestaña General:
- ID de aplicación: Ingresa un identificador único (por ejemplo,
secops-oauth-client
). - Nombre de la aplicación: Ingresa un nombre descriptivo (por ejemplo,
SecOps Data Export
). - Descripción de la aplicación: Ingresa una descripción (por ejemplo,
OAuth client for exporting audit events to SecOps
).
- ID de aplicación: Ingresa un identificador único (por ejemplo,
- Pestaña Confianza:
- Application is Confidential: Marca esta opción.
- Tipo de ID de cliente: Selecciona Confidencial.
- ID de cliente emitido: Copia y guarda este valor.
- Secreto del cliente emitido: Copia y guarda este valor.
- Pestaña Tokens:
- Métodos de autenticación: Selecciona Credenciales del cliente.
- Token Type: Selecciona Jwt RS256.
- Pestaña Scope:
- Agrega el alcance siem con la descripción SIEM Integration Access.
- Agrega el permiso redrock/query con la descripción Acceso a la API de Query.
- Pestaña General:
- Haz clic en Guardar para crear el cliente de OAuth.
- Ve a Servicios principales > Usuarios > Agregar usuario.
- Configura el usuario del servicio:
- Nombre de acceso: Ingresa el ID de cliente del paso 6.
- Dirección de correo electrónico: Ingresa una dirección de correo electrónico válida (campo obligatorio).
- Nombre visible: Ingresa un nombre descriptivo (por ejemplo,
SecOps Service User
). - Contraseña y Confirmar contraseña: Ingresa el secreto del cliente del paso 6.
- Estado: Selecciona Es un cliente de OAuth confidencial.
- Haz clic en Crear usuario.
- Ve a Access > Roles y asigna el usuario de servicio a un rol con los permisos adecuados para consultar eventos de auditoría.
- Copia y guarda en una ubicación segura los siguientes detalles:
- URL del arrendatario: Es la URL de tu arrendatario de Centrify (por ejemplo,
https://yourtenant.my.centrify.com
). - ID de cliente: Del paso 6
- Secreto del cliente: Del paso 6
- ID de la aplicación de OAuth: De la configuración de la aplicación
- URL del arrendatario: Es la URL de tu arrendatario de Centrify (por ejemplo,
Configura el bucket de AWS S3 y el IAM para Google SecOps
- Crea un bucket de Amazon S3 siguiendo esta guía del usuario: Crea un bucket.
- Guarda el Nombre y la Región del bucket para futuras referencias (por ejemplo,
delinea-centrify-logs-bucket
). - Crea un usuario siguiendo esta guía del usuario: Cómo crear un usuario de IAM.
- Selecciona el usuario creado.
- Selecciona la pestaña Credenciales de seguridad.
- Haz clic en Crear clave de acceso en la sección Claves de acceso.
- Selecciona Servicio de terceros como Caso de uso.
- Haz clic en Siguiente.
- Opcional: Agrega una etiqueta de descripción.
- Haz clic en Crear clave de acceso.
- Haz clic en Descargar archivo .CSV para guardar la clave de acceso y la clave de acceso secreta para consultarlas en el futuro.
- Haz clic en Listo.
- Selecciona la pestaña Permisos.
- Haz clic en Agregar permisos en la sección Políticas de permisos.
- Selecciona Agregar permisos.
- Selecciona Adjuntar políticas directamente.
- Busca la política AmazonS3FullAccess.
- Selecciona la política.
- Haz clic en Siguiente.
- Haz clic en Agregar permisos.
Configura la política y el rol de IAM para las cargas de S3
- En la consola de AWS, ve a IAM > Políticas.
- Haz clic en Crear política > pestaña JSON.
- Copia y pega la siguiente política.
JSON de la política (reemplaza
delinea-centrify-logs-bucket
si ingresaste un nombre de bucket diferente):{ "Version": "2012-10-17", "Statement": [ { "Sid": "AllowPutObjects", "Effect": "Allow", "Action": "s3:PutObject", "Resource": "arn:aws:s3:::delinea-centrify-logs-bucket/*" }, { "Sid": "AllowGetStateObject", "Effect": "Allow", "Action": "s3:GetObject", "Resource": "arn:aws:s3:::delinea-centrify-logs-bucket/centrify-sso-logs/state.json" } ] }
Haz clic en Siguiente > Crear política.
Ve a IAM > Roles.
Haz clic en Crear rol > Servicio de AWS > Lambda.
Adjunta la política recién creada y la política administrada AWSLambdaBasicExecutionRole (para el registro de CloudWatch).
Asigna el nombre
CentrifySSOLogExportRole
al rol y haz clic en Crear rol.
Crea la función Lambda
- En la consola de AWS, ve a Lambda > Functions > Create function.
- Haz clic en Author from scratch.
Proporciona los siguientes detalles de configuración:
Configuración Valor Nombre CentrifySSOLogExport
Tiempo de ejecución Python 3.13 Arquitectura x86_64 Rol de ejecución CentrifySSOLogExportRole
Después de crear la función, abre la pestaña Code, borra el código auxiliar y pega el siguiente código (
CentrifySSOLogExport.py
).import json import boto3 import requests import base64 from datetime import datetime, timedelta import os from typing import Dict, List, Optional def lambda_handler(event, context): """ Lambda function to fetch Delinea Centrify SSO audit events and store them in S3 """ # Environment variables S3_BUCKET = os.environ['S3_BUCKET'] S3_PREFIX = os.environ['S3_PREFIX'] STATE_KEY = os.environ['STATE_KEY'] # Centrify API credentials TENANT_URL = os.environ['TENANT_URL'] CLIENT_ID = os.environ['CLIENT_ID'] CLIENT_SECRET = os.environ['CLIENT_SECRET'] OAUTH_APP_ID = os.environ['OAUTH_APP_ID'] # Optional parameters PAGE_SIZE = int(os.environ.get('PAGE_SIZE', '1000')) MAX_PAGES = int(os.environ.get('MAX_PAGES', '10')) s3_client = boto3.client('s3') try: # Get last execution state last_timestamp = get_last_state(s3_client, S3_BUCKET, STATE_KEY) # Get OAuth access token access_token = get_oauth_token(TENANT_URL, CLIENT_ID, CLIENT_SECRET, OAUTH_APP_ID) # Fetch audit events events = fetch_audit_events(TENANT_URL, access_token, last_timestamp, PAGE_SIZE, MAX_PAGES) if events: # Store events in S3 current_timestamp = datetime.utcnow() filename = f"{S3_PREFIX}centrify-sso-events-{current_timestamp.strftime('%Y%m%d_%H%M%S')}.json" store_events_to_s3(s3_client, S3_BUCKET, filename, events) # Update state with latest timestamp latest_timestamp = get_latest_event_timestamp(events) update_state(s3_client, S3_BUCKET, STATE_KEY, latest_timestamp) print(f"Successfully processed {len(events)} events and stored to {filename}") else: print("No new events found") return { 'statusCode': 200, 'body': json.dumps(f'Successfully processed {len(events) if events else 0} events') } except Exception as e: print(f"Error processing Centrify SSO logs: {str(e)}") return { 'statusCode': 500, 'body': json.dumps(f'Error: {str(e)}') } def get_oauth_token(tenant_url: str, client_id: str, client_secret: str, oauth_app_id: str) -> str: """ Get OAuth access token using client credentials flow """ # Create basic auth token credentials = f"{client_id}:{client_secret}" basic_auth = base64.b64encode(credentials.encode()).decode() token_url = f"{tenant_url}/oauth2/token/{oauth_app_id}" headers = { 'Authorization': f'Basic {basic_auth}', 'X-CENTRIFY-NATIVE-CLIENT': 'True', 'Content-Type': 'application/x-www-form-urlencoded' } data = { 'grant_type': 'client_credentials', 'scope': 'siem redrock/query' } response = requests.post(token_url, headers=headers, data=data) response.raise_for_status() token_data = response.json() return token_data['access_token'] def fetch_audit_events(tenant_url: str, access_token: str, last_timestamp: str, page_size: int, max_pages: int) -> List[Dict]: """ Fetch audit events from Centrify using the Redrock/query API """ query_url = f"{tenant_url}/Redrock/query" headers = { 'Authorization': f'Bearer {access_token}', 'X-CENTRIFY-NATIVE-CLIENT': 'True', 'Content-Type': 'application/json' } # Build SQL query with timestamp filter if last_timestamp: sql_query = f"Select * from Event where WhenOccurred > '{last_timestamp}' ORDER BY WhenOccurred ASC" else: # First run - get events from last 24 hours sql_query = "Select * from Event where WhenOccurred > datefunc('now', '-1') ORDER BY WhenOccurred ASC" payload = { "Script": sql_query, "args": { "PageSize": page_size, "Limit": page_size * max_pages, "Caching": -1 } } response = requests.post(query_url, headers=headers, json=payload) response.raise_for_status() response_data = response.json() if not response_data.get('success', False): raise Exception(f"API query failed: {response_data.get('Message', 'Unknown error')}") # Parse the response result = response_data.get('Result', {}) columns = {col['Name']: i for i, col in enumerate(result.get('Columns', []))} raw_results = result.get('Results', []) events = [] for raw_event in raw_results: event = {} row_data = raw_event.get('Row', {}) # Map column names to values for col_name, col_index in columns.items(): if col_name in row_data and row_data[col_name] is not None: event[col_name] = row_data[col_name] # Add metadata event['_source'] = 'centrify_sso' event['_collected_at'] = datetime.utcnow().isoformat() + 'Z' events.append(event) return events def get_last_state(s3_client, bucket: str, state_key: str) -> Optional[str]: """ Get the last processed timestamp from S3 state file """ try: response = s3_client.get_object(Bucket=bucket, Key=state_key) state_data = json.loads(response['Body'].read().decode('utf-8')) return state_data.get('last_timestamp') except s3_client.exceptions.NoSuchKey: print("No previous state found, starting from 24 hours ago") return None except Exception as e: print(f"Error reading state: {e}") return None def update_state(s3_client, bucket: str, state_key: str, timestamp: str): """ Update the state file with the latest processed timestamp """ state_data = { 'last_timestamp': timestamp, 'updated_at': datetime.utcnow().isoformat() + 'Z' } s3_client.put_object( Bucket=bucket, Key=state_key, Body=json.dumps(state_data), ContentType='application/json' ) def store_events_to_s3(s3_client, bucket: str, key: str, events: List[Dict]): """ Store events as JSONL (one JSON object per line) in S3 """ # Convert to JSONL format (one JSON object per line) jsonl_content = 'n'.join(json.dumps(event, default=str) for event in events) s3_client.put_object( Bucket=bucket, Key=key, Body=jsonl_content, ContentType='application/x-ndjson' ) def get_latest_event_timestamp(events: List[Dict]) -> str: """ Get the latest timestamp from the events for state tracking """ if not events: return datetime.utcnow().isoformat() + 'Z' latest = None for event in events: when_occurred = event.get('WhenOccurred') if when_occurred: if latest is None or when_occurred > latest: latest = when_occurred return latest or datetime.utcnow().isoformat() + 'Z'
Ve a Configuration > Environment variables.
Haz clic en Editar > Agregar nueva variable de entorno.
Ingresa las variables de entorno que se proporcionan en la siguiente tabla y reemplaza los valores de ejemplo por tus valores.
Variables de entorno
Clave Valor de ejemplo S3_BUCKET
delinea-centrify-logs-bucket
S3_PREFIX
centrify-sso-logs/
STATE_KEY
centrify-sso-logs/state.json
TENANT_URL
https://yourtenant.my.centrify.com
CLIENT_ID
your-client-id
CLIENT_SECRET
your-client-secret
OAUTH_APP_ID
your-oauth-application-id
OAUTH_SCOPE
siem
PAGE_SIZE
1000
MAX_PAGES
10
Después de crear la función, permanece en su página (o abre Lambda > Funciones > tu-función).
Selecciona la pestaña Configuración.
En el panel Configuración general, haz clic en Editar.
Cambia Tiempo de espera a 5 minutos (300 segundos) y haz clic en Guardar.
Crea una programación de EventBridge
- Ve a Amazon EventBridge > Scheduler > Create schedule.
- Proporciona los siguientes detalles de configuración:
- Programación recurrente: Frecuencia (
1 hour
) - Destino: Tu función Lambda
CentrifySSOLogExport
. - Nombre:
CentrifySSOLogExport-1h
.
- Programación recurrente: Frecuencia (
- Haz clic en Crear programación.
(Opcional) Crea un usuario y claves de IAM de solo lectura para Google SecOps
- En la consola de AWS, ve a IAM > Usuarios.
- Haz clic en Agregar usuarios.
- Proporciona los siguientes detalles de configuración:
- Usuario: Ingresa
secops-reader
. - Tipo de acceso: Selecciona Clave de acceso: Acceso programático.
- Usuario: Ingresa
- Haz clic en Crear usuario.
- Adjunta una política de lectura mínima (personalizada): Usuarios > secops-reader > Permisos.
- Haz clic en Agregar permisos > Adjuntar políticas directamente.
- Selecciona Crear política.
JSON:
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": ["s3:GetObject"], "Resource": "arn:aws:s3:::delinea-centrify-logs-bucket/*" }, { "Effect": "Allow", "Action": ["s3:ListBucket"], "Resource": "arn:aws:s3:::delinea-centrify-logs-bucket" } ] }
Nombre =
secops-reader-policy
.Haz clic en Crear política > busca o selecciona > Siguiente.
Haz clic en Agregar permisos.
Crea una clave de acceso para
secops-reader
: Credenciales de seguridad > Claves de acceso.Haz clic en Crear clave de acceso.
Descarga el
.CSV
. (Pegarás estos valores en el feed).
Configura un feed en Google SecOps para transferir los registros de SSO de Delinea (Centrify)
- Ve a Configuración de SIEM > Feeds.
- Haz clic en + Agregar feed nuevo.
- En el campo Nombre del feed, ingresa un nombre para el feed (por ejemplo,
Delinea Centrify SSO logs
). - Selecciona Amazon S3 V2 como el Tipo de fuente.
- Selecciona Centrify como el Tipo de registro.
- Haz clic en Siguiente.
- Especifica valores para los siguientes parámetros de entrada:
- URI de S3:
s3://delinea-centrify-logs-bucket/centrify-sso-logs/
- Opciones de borrado de la fuente: Selecciona la opción de borrado según tu preferencia.
- Antigüedad máxima del archivo: Incluye los archivos modificados en la cantidad de días especificada. El valor predeterminado es de 180 días.
- ID de clave de acceso: Clave de acceso del usuario con acceso al bucket de S3.
- Clave de acceso secreta: Clave secreta del usuario con acceso al bucket de S3.
- Espacio de nombres del recurso: Es el espacio de nombres del recurso.
- Etiquetas de transferencia: Es la etiqueta que se aplica a los eventos de este feed.
- URI de S3:
- Haz clic en Siguiente.
- Revisa la nueva configuración del feed en la pantalla Finalizar y, luego, haz clic en Enviar.
Tabla de asignación de UDM
Campo de registro | Asignación de UDM | Lógica |
---|---|---|
AccountID |
security_result.detection_fields.value |
El valor de AccountID del registro sin procesar se asigna a un objeto security_result.detection_fields con key :Account ID . |
ApplicationName |
target.application |
El valor de ApplicationName del registro sin procesar se asigna al campo target.application . |
AuthorityFQDN |
target.asset.network_domain |
El valor de AuthorityFQDN del registro sin procesar se asigna al campo target.asset.network_domain . |
AuthorityID |
target.asset.asset_id |
El valor de AuthorityID del registro sin procesar se asigna al campo target.asset.asset_id , con el prefijo "AuthorityID:". |
AzDeploymentId |
security_result.detection_fields.value |
El valor de AzDeploymentId del registro sin procesar se asigna a un objeto security_result.detection_fields con key :AzDeploymentId . |
AzRoleId |
additional.fields.value.string_value |
El valor de AzRoleId del registro sin procesar se asigna a un objeto additional.fields con key :AzRole Id . |
AzRoleName |
target.user.attribute.roles.name |
El valor de AzRoleName del registro sin procesar se asigna al campo target.user.attribute.roles.name . |
ComputerFQDN |
principal.asset.network_domain |
El valor de ComputerFQDN del registro sin procesar se asigna al campo principal.asset.network_domain . |
ComputerID |
principal.asset.asset_id |
El valor de ComputerID del registro sin procesar se asigna al campo principal.asset.asset_id , con el prefijo "ComputerId:". |
ComputerName |
about.hostname |
El valor de ComputerName del registro sin procesar se asigna al campo about.hostname . |
CredentialId |
security_result.detection_fields.value |
El valor de CredentialId del registro sin procesar se asigna a un objeto security_result.detection_fields con key :Credential Id . |
DirectoryServiceName |
security_result.detection_fields.value |
El valor de DirectoryServiceName del registro sin procesar se asigna a un objeto security_result.detection_fields con key :Directory Service Name . |
DirectoryServiceNameLocalized |
security_result.detection_fields.value |
El valor de DirectoryServiceNameLocalized del registro sin procesar se asigna a un objeto security_result.detection_fields con key :Directory Service Name Localized . |
DirectoryServiceUuid |
security_result.detection_fields.value |
El valor de DirectoryServiceUuid del registro sin procesar se asigna a un objeto security_result.detection_fields con key :Directory Service Uuid . |
EventMessage |
security_result.summary |
El valor de EventMessage del registro sin procesar se asigna al campo security_result.summary . |
EventType |
metadata.product_event_type |
El valor de EventType del registro sin procesar se asigna al campo metadata.product_event_type . También se usa para determinar el metadata.event_type . |
FailReason |
security_result.summary |
El valor de FailReason del registro sin procesar se asigna al campo security_result.summary cuando está presente. |
FailUserName |
target.user.email_addresses |
El valor de FailUserName del registro sin procesar se asigna al campo target.user.email_addresses cuando está presente. |
FromIPAddress |
principal.ip |
El valor de FromIPAddress del registro sin procesar se asigna al campo principal.ip . |
ID |
security_result.detection_fields.value |
El valor de ID del registro sin procesar se asigna a un objeto security_result.detection_fields con key :ID . |
InternalTrackingID |
metadata.product_log_id |
El valor de InternalTrackingID del registro sin procesar se asigna al campo metadata.product_log_id . |
JumpType |
additional.fields.value.string_value |
El valor de JumpType del registro sin procesar se asigna a un objeto additional.fields con key :Jump Type . |
NormalizedUser |
target.user.email_addresses |
El valor de NormalizedUser del registro sin procesar se asigna al campo target.user.email_addresses . |
OperationMode |
additional.fields.value.string_value |
El valor de OperationMode del registro sin procesar se asigna a un objeto additional.fields con key :Operation Mode . |
ProxyId |
security_result.detection_fields.value |
El valor de ProxyId del registro sin procesar se asigna a un objeto security_result.detection_fields con key :Proxy Id . |
RequestUserAgent |
network.http.user_agent |
El valor de RequestUserAgent del registro sin procesar se asigna al campo network.http.user_agent . |
SessionGuid |
network.session_id |
El valor de SessionGuid del registro sin procesar se asigna al campo network.session_id . |
Tenant |
additional.fields.value.string_value |
El valor de Tenant del registro sin procesar se asigna a un objeto additional.fields con key :Tenant . |
ThreadType |
additional.fields.value.string_value |
El valor de ThreadType del registro sin procesar se asigna a un objeto additional.fields con key :Thread Type . |
UserType |
principal.user.attribute.roles.name |
El valor de UserType del registro sin procesar se asigna al campo principal.user.attribute.roles.name . |
WhenOccurred |
metadata.event_timestamp |
El valor de WhenOccurred del registro sin procesar se analiza y se asigna al campo metadata.event_timestamp . Este campo también propaga el campo timestamp de nivel superior. Valor codificado "SSO". Se determina según el campo EventType . Si EventType no está presente o no coincide con ningún criterio específico, el valor predeterminado es STATUS_UPDATE . Puede ser USER_LOGIN , USER_CREATION , USER_RESOURCE_ACCESS , USER_LOGOUT o USER_CHANGE_PASSWORD . Valor codificado "CENTRIFY_SSO". Valor codificado "SSO". Valor codificado "Centrify". Si el campo message contiene un ID de sesión, se extrae y se usa. De lo contrario, el valor predeterminado es “1”. Se extrae del campo host si está disponible, que proviene del encabezado de syslog. Se extrae del campo pid si está disponible, que proviene del encabezado de syslog. Si UserGuid está presente, se usa su valor. De lo contrario, si el campo message contiene un ID de usuario, se extrae y se usa. Se establece en "ALLOW" si Level es "Info" y en "BLOCK" si está presente FailReason . Se establece en "AUTH_VIOLATION" si FailReason está presente. Se determina según el campo Level . Se establece en "INFORMATIONAL" si Level es "Info", en "MEDIUM" si Level es "Warning" y en "ERROR" si Level es "Error". |
¿Necesitas más ayuda? Obtén respuestas de miembros de la comunidad y profesionales de Google SecOps.