Recoger registros de auditoría y de problemas a nivel de grupo de Snyk
En esta guía se explica cómo puede ingerir registros de auditoría y de problemas a nivel de grupo de Snyk en Google Security Operations mediante Amazon S3.
Antes de empezar
Asegúrate de que cumples los siguientes requisitos previos:
- Instancia de Google SecOps
- Acceso privilegiado al grupo Snyk (token de API con acceso de lectura; ID de grupo)
- Acceso privilegiado a AWS (S3, IAM, Lambda y EventBridge)
Obtener el ID de grupo y el token de API de Snyk
- En la interfaz de usuario de Snyk, ve a Ajustes de la cuenta > Token de la API y genera el token de la API.
- Copia y guarda el token en un lugar seguro para usarlo más adelante como
SNYK_TOKEN
. - Cambia a tu grupo y abre Configuración del grupo.
- Copia y guarda el ID de grupo de la URL (
https://app.snyk.io/group/<GROUP_ID>/...
) para usarlo más adelante comoGROUP_ID
. - Endpoint de la API base:
https://api.snyk.io
(sustitúyelo porAPI_BASE
si es necesario). - Asigna el rol Administrador de grupo al usuario con el token. El usuario debe poder ver los registros de auditoría de grupos y los problemas de grupos.
Configurar un segmento de AWS S3 y IAM para Google SecOps
- Crea un segmento de Amazon S3 siguiendo esta guía de usuario: Crear un segmento.
- Guarda el nombre y la región del segmento para consultarlos más adelante (por ejemplo,
snyk-group-logs
). - Crea un usuario siguiendo esta guía: Crear un usuario de gestión de identidades y accesos.
- Selecciona el Usuario creado.
- Selecciona la pestaña Credenciales de seguridad.
- En la sección Claves de acceso, haz clic en Crear clave de acceso.
- Selecciona Servicio de terceros como Caso práctico.
- Haz clic en Siguiente.
- Opcional: añade 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 usarlas más adelante.
- Haz clic en Listo.
- Selecciona la pestaña Permisos.
- En la sección Políticas de permisos, haz clic en Añadir permisos.
- Selecciona Añadir permisos.
- Seleccione Adjuntar políticas directamente.
- Busca y selecciona la política AmazonS3FullAccess.
- Haz clic en Siguiente.
- Haz clic en Añadir permisos.
Configurar la política y el rol de gestión de identidades y accesos para las subidas de S3
- En la consola de AWS, ve a IAM > Policies > Create policy > pestaña JSON.
Introduce la siguiente política (incluye acceso de escritura para todos los objetos del bucket y acceso de lectura al archivo de estado que usa tu función Lambda):
{ "Version": "2012-10-17", "Statement": [ { "Sid": "PutAllSnykGroupObjects", "Effect": "Allow", "Action": ["s3:PutObject", "s3:GetObject"], "Resource": "arn:aws:s3:::snyk-group-logs/*" } ] }
- Sustituye
snyk-group-logs
si has introducido otro nombre de segmento.
- Sustituye
Haz clic en Siguiente > Crear política.
Ve a IAM > Roles > Crear rol > Servicio de AWS > Lambda.
Adjunte la política que acaba de crear.
Dale el nombre
WriteSnykGroupToS3Role
al rol y haz clic en Crear rol.
Crear la función Lambda
- En la consola de AWS, ve a Lambda > Funciones > Crear función.
- Haz clic en Crear desde cero.
- Proporciona los siguientes detalles de configuración:
Ajuste | Valor |
---|---|
Nombre | snyk_group_audit_issues_to_s3 |
Tiempo de ejecución | Python 3.13 |
Arquitectura | x86_64 |
Rol de ejecución | WriteSnykGroupToS3Role |
Una vez creada la función, abra la pestaña Código, elimine el stub e introduzca el siguiente código (
snyk_group_audit_issues_to_s3.py
):#!/usr/bin/env python3 # Lambda: Pull Snyk Group-level Audit Logs + Issues to S3 (no transform) import os import json import time import urllib.parse from urllib.request import Request, urlopen from urllib.parse import urlparse, parse_qs from urllib.error import HTTPError import boto3 API_BASE = os.environ.get("API_BASE", "https://api.snyk.io").rstrip("/") SNYK_TOKEN = os.environ["SNYK_TOKEN"].strip() GROUP_ID = os.environ["GROUP_ID"].strip() BUCKET = os.environ["S3_BUCKET"].strip() PREFIX = os.environ.get("S3_PREFIX", "snyk/group/").strip() STATE_KEY = os.environ.get("STATE_KEY", "snyk/group/state.json").strip() # Page sizes & limits AUDIT_SIZE = int(os.environ.get("AUDIT_PAGE_SIZE", "100")) # audit uses 'size' (max 100) ISSUES_LIMIT = int(os.environ.get("ISSUES_PAGE_LIMIT", "200")) # issues uses 'limit' MAX_PAGES = int(os.environ.get("MAX_PAGES", "20")) # API versions (Snyk REST requires a 'version' param) AUDIT_API_VERSION = os.environ.get("SNYK_AUDIT_API_VERSION", "2021-06-04").strip() ISSUES_API_VERSION = os.environ.get("SNYK_ISSUES_API_VERSION", "2024-10-15").strip() # First-run lookback for audit to avoid huge backfills LOOKBACK_SECONDS = int(os.environ.get("LOOKBACK_SECONDS", "3600")) HDRS = { "Authorization": f"token {SNYK_TOKEN}", "Accept": "application/vnd.api+json", } s3 = boto3.client("s3") def _get_state() -> dict: try: obj = s3.get_object(Bucket=BUCKET, Key=STATE_KEY) return json.loads(obj["Body"].read() or b"{}") except Exception: return {} def _put_state(state: dict): s3.put_object( Bucket=BUCKET, Key=STATE_KEY, Body=json.dumps(state, 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 _http_get(url: str) -> dict: req = Request(url, method="GET", headers=HDRS) try: with urlopen(req, timeout=60) as r: return json.loads(r.read().decode("utf-8")) except HTTPError as e: if e.code in (429, 500, 502, 503, 504): delay = int(e.headers.get("Retry-After", "1")) time.sleep(max(1, delay)) with urlopen(req, timeout=60) as r2: return json.loads(r2.read().decode("utf-8")) raise def _write_page(kind: str, payload: dict) -> str: ts = time.gmtime() key = f"{PREFIX.rstrip('/')}/{time.strftime('%Y/%m/%d/%H%M%S', ts)}-snyk-{kind}.json" s3.put_object( Bucket=BUCKET, Key=key, Body=json.dumps(payload, separators=(",", ":")).encode("utf-8"), ContentType="application/json", ) return key def _next_href(links: dict | None) -> str | None: if not links: return None nxt = links.get("next") if not nxt: return None if isinstance(nxt, str): return nxt if isinstance(nxt, dict): return nxt.get("href") return None # -------- Audit Logs -------- def pull_audit_logs(state: dict) -> dict: cursor = state.get("audit_cursor") pages = 0 total = 0 base = f"{API_BASE}/rest/groups/{GROUP_ID}/audit_logs/search" params: dict[str, object] = {"version": AUDIT_API_VERSION, "size": AUDIT_SIZE} if cursor: params["cursor"] = cursor else: now = time.time() params["from"] = _iso(now - LOOKBACK_SECONDS) params["to"] = _iso(now) while pages < MAX_PAGES: url = f"{base}?{urllib.parse.urlencode(params, doseq=True)}" payload = _http_get(url) _write_page("audit", payload) data_items = (payload.get("data") or {}).get("items") or [] if isinstance(data_items, list): total += len(data_items) nxt = _next_href(payload.get("links")) if not nxt: break q = parse_qs(urlparse(nxt).query) cur = (q.get("cursor") or [None])[0] if not cur: break params = {"version": AUDIT_API_VERSION, "size": AUDIT_SIZE, "cursor": cur} state["audit_cursor"] = cur pages += 1 return {"pages": pages + 1 if total else pages, "items": total, "cursor": state.get("audit_cursor")} # -------- Issues -------- def pull_issues(state: dict) -> dict: cursor = state.get("issues_cursor") # stores 'starting_after' pages = 0 total = 0 base = f"{API_BASE}/rest/groups/{GROUP_ID}/issues" params: dict[str, object] = {"version": ISSUES_API_VERSION, "limit": ISSUES_LIMIT} if cursor: params["starting_after"] = cursor while pages < MAX_PAGES: url = f"{base}?{urllib.parse.urlencode(params, doseq=True)}" payload = _http_get(url) _write_page("issues", payload) data_items = payload.get("data") or [] if isinstance(data_items, list): total += len(data_items) nxt = _next_href(payload.get("links")) if not nxt: break q = parse_qs(urlparse(nxt).query) cur = (q.get("starting_after") or [None])[0] if not cur: break params = {"version": ISSUES_API_VERSION, "limit": ISSUES_LIMIT, "starting_after": cur} state["issues_cursor"] = cur pages += 1 return {"pages": pages + 1 if total else pages, "items": total, "cursor": state.get("issues_cursor")} def lambda_handler(event=None, context=None): state = _get_state() audit_res = pull_audit_logs(state) issues_res = pull_issues(state) _put_state(state) return {"ok": True, "audit": audit_res, "issues": issues_res} if __name__ == "__main__": print(lambda_handler())
Ve a Configuración > Variables de entorno > Editar > Añadir nueva variable de entorno.
Introduce las siguientes variables de entorno y sustituye los valores por los tuyos:
Clave Ejemplo S3_BUCKET
snyk-group-logs
S3_PREFIX
snyk/group/
STATE_KEY
snyk/group/state.json
SNYK_TOKEN
xxxxxxxx-xxxx-xxxx-xxxx-xxxx
GROUP_ID
<group_uuid>
API_BASE
https://api.snyk.io
SNYK_AUDIT_API_VERSION
2021-06-04
SNYK_ISSUES_API_VERSION
2024-10-15
AUDIT_PAGE_SIZE
100
ISSUES_PAGE_LIMIT
200
MAX_PAGES
20
LOOKBACK_SECONDS
3600
Una vez creada la función, permanece en su página (o abre Lambda > Funciones >
<your-function>
).Seleccione 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.
Crear una programación de EventBridge
- Ve a Amazon EventBridge > Scheduler > Create schedule (Amazon EventBridge > Programador > Crear programación).
- Proporcione los siguientes detalles de configuración:
- Programación periódica: Precio (
1 hour
). - Destino: tu función Lambda
snyk_group_audit_issues_to_s3
. - Nombre:
snyk-group-audit-issues-1h
.
- Programación periódica: Precio (
- Haz clic en Crear programación.
Opcional: Crear un usuario y claves de gestión de identidades y accesos de solo lectura para Google SecOps
- En la consola de AWS, ve a IAM > Usuarios > Añadir usuarios.
- Haz clic en Add users (Añadir usuarios).
- Proporcione los siguientes detalles de configuración:
- Usuario:
secops-reader
. - Tipo de acceso: Clave de acceso (acceso programático).
- Usuario:
- Haz clic en Crear usuario.
- Asigna una política de lectura mínima (personalizada): Usuarios > secops-reader > Permisos > Añadir permisos > Asignar políticas directamente > Crear política.
En el editor de JSON, introduce la siguiente política:
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": ["s3:GetObject"], "Resource": "arn:aws:s3:::snyk-group-logs/*" }, { "Effect": "Allow", "Action": ["s3:ListBucket"], "Resource": "arn:aws:s3:::snyk-group-logs" } ] }
Asigna el nombre
secops-reader-policy
.Ve a Crear política > busca o selecciona > Siguiente > Añadir permisos.
Ve a Credenciales de seguridad > Claves de acceso > Crear clave de acceso.
Descarga el archivo CSV (estos valores se introducen en el feed).
Configurar un feed en Google SecOps para ingerir los registros de auditoría y de problemas a nivel de grupo de Snyk
- Ve a Configuración de SIEM > Feeds.
- Haz clic en + Añadir nuevo feed.
- En el campo Nombre del feed, introduce un nombre para el feed (por ejemplo,
Snyk Group Audit/Issues
). - Selecciona Amazon S3 V2 como Tipo de fuente.
- Selecciona Registros de auditoría o problemas a nivel de grupo de Snyk como Tipo de registro.
- Haz clic en Siguiente.
- Especifique los valores de los siguientes parámetros de entrada:
- URI de S3:
s3://snyk-group-logs/snyk/group/
- Opciones de eliminación de la fuente: selecciona la opción de eliminación que prefieras.
- Antigüedad máxima del archivo: incluye los archivos modificados en los últimos días. El valor predeterminado es 180 días.
- ID de clave de acceso: clave de acceso de 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:
snyk.group
- Etiquetas de ingesta: añádelas si quieres.
- URI de S3:
- Haz clic en Siguiente.
- Revise la configuración de la nueva fuente en la pantalla Finalizar y, a continuación, haga clic en Enviar.
¿Necesitas más ayuda? Recibe respuestas de los miembros de la comunidad y de los profesionales de Google SecOps.