Recoger registros de auditoría de Harness IO
En este documento se explica cómo ingerir registros de auditoría de Harness IO en Google Security Operations mediante Amazon S3.
Antes de empezar
Asegúrate de que cumples los siguientes requisitos previos:
- Una instancia de Google SecOps
- Acceso privilegiado a Harness con permisos para:
- Crear claves de API
- de auditoría de acceso a datos.
- Ver la configuración de la cuenta
- Acceso con privilegios a AWS (S3, IAM, Lambda y EventBridge).
Recoger las credenciales de la API de Harness
Crear una clave de API en Harness
- Inicia sesión en la plataforma Harness.
- Haz clic en tu perfil de usuario.
- Ve a Mis claves de API.
- Haz clic en + Clave de API.
- Proporcione los siguientes detalles de configuración:
- Nombre: introduce un nombre descriptivo (por ejemplo,
Google SecOps Integration). - Descripción: descripción opcional.
- Nombre: introduce un nombre descriptivo (por ejemplo,
- Haz clic en Guardar.
- Haz clic en + Token para crear un token.
- Proporcione los siguientes detalles de configuración:
- Nombre: introduce
Chronicle Feed Token. - Definir vencimiento: selecciona un plazo de vencimiento adecuado o Sin vencimiento (para uso en producción).
- Nombre: introduce
- Haz clic en Generate Token (Generar token).
Copia y guarda el valor del token de forma segura. Este token se usará como valor del encabezado
x-api-key.
Obtener el ID de cuenta de Harness
- En Harness Platform, anota el ID de cuenta de la URL.
- URL de ejemplo:
https://app.harness.io/ng/account/YOUR_ACCOUNT_ID/... - La parte
YOUR_ACCOUNT_IDes su identificador de cuenta.
- URL de ejemplo:
- También puedes ir a Configuración de la cuenta > Información general para ver tu identificador de cuenta.
Copia y guarda el ID de cuenta para usarlo en la función Lambda.
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,
harness-io-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 en 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 futuras consultas.
- 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 la política AmazonS3FullAccess.
- Selecciona la política.
- 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 Lambda a S3
- En la consola de AWS, ve a IAM > Policies > Create policy > JSON tab.
Copia y pega la siguiente política:
{ "Version": "2012-10-17", "Statement": [ { "Sid": "AllowPutHarnessObjects", "Effect": "Allow", "Action": "s3:PutObject", "Resource": "arn:aws:s3:::harness-io-logs/harness/audit/*" }, { "Sid": "AllowGetStateObject", "Effect": "Allow", "Action": "s3:GetObject", "Resource": "arn:aws:s3:::harness-io-logs/harness/audit/state.json" } ] }- Sustituye
harness-io-logssi has introducido otro nombre de segmento.
- Sustituye
Haz clic en Siguiente.
Ponle un nombre a la política
HarnessToS3Policyy haz clic en Crear política.Ve a IAM > Roles > Crear rol.
Seleccione Servicio de AWS como tipo de entidad de confianza.
Selecciona Lambda como caso práctico.
Haz clic en Siguiente.
Busca y selecciona las siguientes políticas:
HarnessToS3Policy(la política que acabas de crear)AWSLambdaBasicExecutionRole(para CloudWatch Logs)
Haz clic en Siguiente.
Dale el nombre
HarnessAuditLambdaRoleal 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 harness-audit-to-s3Tiempo de ejecución Python 3.13 Arquitectura x86_64 Rol de ejecución HarnessAuditLambdaRoleHaz clic en Crear función.
Una vez creada la función, abra la pestaña Código.
Elimina el código de stub predeterminado e introduce el siguiente código de función Lambda:
Código de la función Lambda (
harness_audit_to_s3.py)#!/usr/bin/env python3 """ Harness.io Audit Logs to S3 Lambda Fetches audit logs from Harness API and writes to S3 for Chronicle ingestion. """ import os import json import time import uuid import logging import urllib.parse from datetime import datetime, timedelta, timezone from urllib.request import Request, urlopen from urllib.error import HTTPError, URLError import boto3 # Configuration from Environment Variables API_BASE = os.environ.get("HARNESS_API_BASE", "https://app.harness.io").rstrip("/") ACCOUNT_ID = os.environ["HARNESS_ACCOUNT_ID"] API_KEY = os.environ["HARNESS_API_KEY"] BUCKET = os.environ["S3_BUCKET"] PREFIX = os.environ.get("S3_PREFIX", "harness/audit").strip("/") STATE_KEY = os.environ.get("STATE_KEY", "harness/audit/state.json") PAGE_SIZE = min(int(os.environ.get("PAGE_SIZE", "50")), 100) START_MINUTES_BACK = int(os.environ.get("START_MINUTES_BACK", "60")) # Optional filters (NEW) FILTER_MODULES = os.environ.get("FILTER_MODULES", "").split(",") if os.environ.get("FILTER_MODULES") else None FILTER_ACTIONS = os.environ.get("FILTER_ACTIONS", "").split(",") if os.environ.get("FILTER_ACTIONS") else None STATIC_FILTER = os.environ.get("STATIC_FILTER") # e.g., "EXCLUDE_LOGIN_EVENTS" MAX_RETRIES = int(os.environ.get("MAX_RETRIES", "3")) # AWS clients s3 = boto3.client("s3") # HTTP headers for Harness API HDRS = { "x-api-key": API_KEY, "Content-Type": "application/json", "Accept": "application/json", } # Logging configuration logger = logging.getLogger() logger.setLevel(logging.INFO) # ============================================ # State Management Functions # ============================================ def _read_state(): """Read checkpoint state from S3.""" try: obj = s3.get_object(Bucket=BUCKET, Key=STATE_KEY) state = json.loads(obj["Body"].read()) since_ms = state.get("since") page_token = state.get("pageToken") logger.info(f"State loaded: since={since_ms}, pageToken={page_token}") return since_ms, page_token except s3.exceptions.NoSuchKey: logger.info("No state file found, starting fresh collection") start_time = datetime.now(timezone.utc) - timedelta(minutes=START_MINUTES_BACK) since_ms = int(start_time.timestamp() * 1000) logger.info(f"Initial since timestamp: {since_ms} ({start_time.isoformat()})") return since_ms, None except Exception as e: logger.error(f"Error reading state: {e}") raise def _write_state(since_ms: int, page_token: str = None): """Write checkpoint state to S3.""" state = { "since": since_ms, "pageToken": page_token, "lastRun": int(time.time() * 1000), "lastRunISO": datetime.now(timezone.utc).isoformat() } try: s3.put_object( Bucket=BUCKET, Key=STATE_KEY, Body=json.dumps(state, indent=2).encode(), ContentType="application/json" ) logger.info(f"State saved: since={since_ms}, pageToken={page_token}") except Exception as e: logger.error(f"Error writing state: {e}") raise # ============================================ # Harness API Functions # ============================================ def _fetch_harness_audits(since_ms: int, page_token: str = None, retry_count: int = 0): """ Fetch audit logs from Harness API with retry logic. API Endpoint: POST /audit/api/audits/listV2 Documentation: https://apidocs.harness.io/audit/getauditeventlistv2 """ try: # Build URL with query parameters url = ( f"{API_BASE}/audit/api/audits/listV2" f"?accountIdentifier={urllib.parse.quote(ACCOUNT_ID)}" f"&pageSize={PAGE_SIZE}" ) if page_token: url += f"&pageToken={urllib.parse.quote(page_token)}" logger.info(f"Fetching from: {url[:100]}...") # Build request body with time filter and optional filters body_data = { "startTime": since_ms, "endTime": int(time.time() * 1000), "filterType": "Audit" } if FILTER_MODULES: body_data["modules"] = [m.strip() for m in FILTER_MODULES if m.strip()] logger.info(f"Applying module filter: {body_data['modules']}") if FILTER_ACTIONS: body_data["actions"] = [a.strip() for a in FILTER_ACTIONS if a.strip()] logger.info(f"Applying action filter: {body_data['actions']}") if STATIC_FILTER: body_data["staticFilter"] = STATIC_FILTER logger.info(f"Applying static filter: {STATIC_FILTER}") logger.debug(f"Request body: {json.dumps(body_data)}") # Make POST request req = Request( url, data=json.dumps(body_data).encode('utf-8'), headers=HDRS, method="POST" ) resp = urlopen(req, timeout=30) resp_text = resp.read().decode('utf-8') resp_data = json.loads(resp_text) if "status" not in resp_data: logger.warning(f"Response missing 'status' field: {resp_text[:200]}") # Check response status if resp_data.get("status") != "SUCCESS": error_msg = resp_data.get("message", "Unknown error") raise Exception(f"API returned status: {resp_data.get('status')} - {error_msg}") # Extract data from response structure data_obj = resp_data.get("data", {}) if not data_obj: logger.warning("Response 'data' object is empty or missing") events = data_obj.get("content", []) has_next = data_obj.get("hasNext", False) next_token = data_obj.get("pageToken") logger.info(f"API response: {len(events)} events, hasNext={has_next}, pageToken={next_token}") if not events and data_obj: logger.info(f"Empty events but data present. Data keys: {list(data_obj.keys())}") return { "events": events, "hasNext": has_next, "pageToken": next_token } except HTTPError as e: error_body = e.read().decode() if hasattr(e, 'read') else '' if e.code == 401: logger.error("Authentication failed: Invalid API key") raise Exception("Invalid Harness API key. Check HARNESS_API_KEY environment variable.") elif e.code == 403: logger.error("Authorization failed: Insufficient permissions") raise Exception("API key lacks required audit:read permissions") elif e.code == 429: retry_after = int(e.headers.get("Retry-After", "60")) logger.warning(f"Rate limit exceeded. Retry after {retry_after} seconds (attempt {retry_count + 1}/{MAX_RETRIES})") if retry_count < MAX_RETRIES: logger.info(f"Waiting {retry_after} seconds before retry...") time.sleep(retry_after) logger.info(f"Retrying request (attempt {retry_count + 2}/{MAX_RETRIES})") return _fetch_harness_audits(since_ms, page_token, retry_count + 1) else: raise Exception(f"Max retries ({MAX_RETRIES}) exceeded for rate limiting") elif e.code == 400: logger.error(f"Bad request: {error_body}") raise Exception(f"Invalid request parameters: {error_body}") else: logger.error(f"HTTP {e.code}: {e.reason} - {error_body}") raise Exception(f"Harness API error {e.code}: {e.reason}") except URLError as e: logger.error(f"Network error: {e.reason}") raise Exception(f"Failed to connect to Harness API: {e.reason}") except json.JSONDecodeError as e: logger.error(f"Invalid JSON response: {e}") logger.error(f"Response text (first 500 chars): {resp_text[:500] if 'resp_text' in locals() else 'N/A'}") raise Exception("Harness API returned invalid JSON") except Exception as e: logger.error(f"Unexpected error in _fetch_harness_audits: {e}", exc_info=True) raise # ============================================ # S3 Upload Functions # ============================================ def _upload_to_s3(events: list) -> str: """ Upload audit events to S3 in JSONL format. Each line is a complete JSON object (one event per line). """ if not events: logger.info("No events to upload") return None try: # Create JSONL content (one JSON object per line) jsonl_lines = [json.dumps(event) for event in events] jsonl_content = "\n".join(jsonl_lines) # Generate S3 key with timestamp and UUID timestamp = datetime.now(timezone.utc) key = ( f"{PREFIX}/" f"{timestamp:%Y/%m/%d}/" f"harness-audit-{timestamp:%Y%m%d-%H%M%S}-{uuid.uuid4()}.jsonl" ) # Upload to S3 s3.put_object( Bucket=BUCKET, Key=key, Body=jsonl_content.encode('utf-8'), ContentType="application/x-ndjson", Metadata={ "event-count": str(len(events)), "source": "harness-audit-lambda", "collection-time": timestamp.isoformat() } ) logger.info(f"Uploaded {len(events)} events to s3://{BUCKET}/{key}") return key except Exception as e: logger.error(f"Error uploading to S3: {e}", exc_info=True) raise # ============================================ # Main Orchestration Function # ============================================ def fetch_and_store(): """ Main function to fetch audit logs from Harness and store in S3. Handles pagination and state management. """ logger.info("=== Harness Audit Collection Started ===") logger.info(f"Configuration: API_BASE={API_BASE}, ACCOUNT_ID={ACCOUNT_ID[:8]}..., PAGE_SIZE={PAGE_SIZE}") if FILTER_MODULES: logger.info(f"Module filter enabled: {FILTER_MODULES}") if FILTER_ACTIONS: logger.info(f"Action filter enabled: {FILTER_ACTIONS}") if STATIC_FILTER: logger.info(f"Static filter enabled: {STATIC_FILTER}") try: # Step 1: Read checkpoint state since_ms, page_token = _read_state() if page_token: logger.info(f"Resuming pagination from saved pageToken") else: since_dt = datetime.fromtimestamp(since_ms / 1000, tz=timezone.utc) logger.info(f"Starting new collection from: {since_dt.isoformat()}") # Step 2: Collect all events with pagination all_events = [] current_page_token = page_token page_count = 0 max_pages = 100 # Safety limit has_next = True while has_next and page_count < max_pages: page_count += 1 logger.info(f"--- Fetching page {page_count} ---") # Fetch one page of results result = _fetch_harness_audits(since_ms, current_page_token) # Extract events events = result.get("events", []) all_events.extend(events) logger.info(f"Page {page_count}: {len(events)} events (total: {len(all_events)})") # Check pagination status has_next = result.get("hasNext", False) current_page_token = result.get("pageToken") if not has_next: logger.info("Pagination complete (hasNext=False)") break if not current_page_token: logger.warning("hasNext=True but no pageToken, stopping pagination") break # Small delay between pages to avoid rate limiting time.sleep(0.5) if page_count >= max_pages: logger.warning(f"Reached max pages limit ({max_pages}), stopping") # Step 3: Upload collected events to S3 if all_events: s3_key = _upload_to_s3(all_events) logger.info(f"Successfully uploaded {len(all_events)} total events") else: logger.info("No new events to upload") s3_key = None # Step 4: Update checkpoint state if not has_next: # Pagination complete - update since to current time for next run new_since = int(time.time() * 1000) _write_state(new_since, None) logger.info(f"Pagination complete, state updated with new since={new_since}") else: # Pagination incomplete - save pageToken for continuation _write_state(since_ms, current_page_token) logger.info(f"Pagination incomplete, saved pageToken for next run") # Step 5: Return result result = { "statusCode": 200, "message": "Success", "eventsCollected": len(all_events), "pagesProcessed": page_count, "paginationComplete": not has_next, "s3Key": s3_key, "filters": { "modules": FILTER_MODULES, "actions": FILTER_ACTIONS, "staticFilter": STATIC_FILTER } } logger.info(f"Collection completed: {json.dumps(result)}") return result except Exception as e: logger.error(f"Collection failed: {e}", exc_info=True) result = { "statusCode": 500, "message": "Error", "error": str(e), "errorType": type(e).__name__ } return result finally: logger.info("=== Harness Audit Collection Finished ===") # ============================================ # Lambda Handler # ============================================ def lambda_handler(event, context): """AWS Lambda handler function.""" return fetch_and_store() # ============================================ # Local Testing # ============================================ if __name__ == "__main__": # For local testing result = lambda_handler(None, None) print(json.dumps(result, indent=2))
Haz clic en Implementar para guardar el código de la función.
Configurar variables de entorno de Lambda
- En la página de la función Lambda, selecciona la pestaña Configuración.
- En la barra lateral izquierda, haz clic en Variables de entorno.
- Haz clic en Editar.
Haz clic en Añadir variable de entorno para cada uno de los siguientes elementos:
Variables de entorno obligatorias:
Clave Valor Descripción HARNESS_ACCOUNT_IDTu ID de cuenta de Harness Identificador de cuenta de Harness HARNESS_API_KEYTu token de clave de API Token con permisos audit:read S3_BUCKETharness-io-logsNombre del segmento de S3 S3_PREFIXharness/auditPrefijo de los objetos de S3 STATE_KEYharness/audit/state.jsonRuta del archivo de estado en S3 Variables de entorno opcionales:
Clave Valor predeterminado Descripción HARNESS_API_BASEhttps://app.harness.ioURL base de la API Harness PAGE_SIZE50Eventos por página (máximo 100) START_MINUTES_BACK60Periodo inicial de retrospectiva en minutos FILTER_MODULESNinguno Módulos separados por comas (por ejemplo, CD,CI,CE)FILTER_ACTIONSNinguno Acciones separadas por comas (por ejemplo, CREATE,UPDATE,DELETE)STATIC_FILTERNinguno Filtro predefinido: EXCLUDE_LOGIN_EVENTSoEXCLUDE_SYSTEM_EVENTSMAX_RETRIES3Número máximo de reintentos para el límite de frecuencia Haz clic en Guardar.
Configurar el tiempo de espera y la memoria de Lambda
- En la página de la función Lambda, selecciona la pestaña Configuración.
- En la barra lateral de la izquierda, haz clic en Configuración general.
- Haz clic en Editar.
- Proporcione los siguientes detalles de configuración:
- Memoria:
256 MB(recomendado) - Tiempo de espera:
5 min 0 sec(300 segundos)
- Memoria:
- 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:
- Nombre de la programación: introduce
harness-audit-hourly. - Descripción: descripción opcional.
- Nombre de la programación: introduce
- Haz clic en Siguiente.
- En Patrón de programación, selecciona Programación periódica.
- Selecciona Programación basada en tarifas.
- Proporcione los siguientes detalles de configuración:
- Expresión de la tarifa: introduce
1 hour.
- Expresión de la tarifa: introduce
- Haz clic en Siguiente.
- En Destino, proporciona los siguientes detalles de configuración:
- API de destino: selecciona Invocar AWS Lambda.
- Función Lambda: selecciona tu función
harness-audit-to-s3.
- Haz clic en Siguiente.
- Revisa la configuración de la programación.
- Haz clic en Crear programación.
Crear un usuario de gestión de identidades y accesos de solo lectura para Google SecOps
Este usuario de IAM permite que Google SecOps lea los registros del bucket de S3.
- Ve a AWS Console > IAM > Users > Create user (Consola de AWS > IAM > Usuarios > Crear usuario).
- Proporcione los siguientes detalles de configuración:
- Nombre de usuario: introduce
chronicle-s3-reader.
- Nombre de usuario: introduce
- Haz clic en Siguiente.
- Seleccione Adjuntar políticas directamente.
- Haz clic en Crear política.
- Selecciona la pestaña JSON.
Pega la siguiente política:
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "s3:GetObject" ], "Resource": "arn:aws:s3:::harness-io-logs/harness/audit/*" }, { "Effect": "Allow", "Action": [ "s3:ListBucket" ], "Resource": "arn:aws:s3:::harness-io-logs", "Condition": { "StringLike": { "s3:prefix": "harness/audit/*" } } } ] }Haz clic en Siguiente.
Asigna un nombre a la política
ChronicleHarnessS3ReadPolicy.Haz clic en Crear política.
Vuelve a la pestaña de creación de usuarios y actualiza la lista de políticas.
Busca y selecciona
ChronicleHarnessS3ReadPolicy.Haz clic en Siguiente.
Revise la información y haga clic en Crear usuario.
Crear claves de acceso para el usuario lector
- En la página Usuarios de IAM, selecciona el usuario
chronicle-s3-reader. - Selecciona la pestaña Credenciales de seguridad.
- 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.
- Haga clic en Descargar archivo CSV para guardar el ID de clave de acceso y la clave de acceso secreta.
- Haz clic en Listo.
Configurar un feed en Google SecOps para ingerir registros de Harness IO
- Ve a Configuración de SIEM > Feeds.
- Haz clic en Añadir nuevo.
- En la página siguiente, haga clic en Configurar un solo feed.
- En el campo Nombre del feed, introduce un nombre para el feed (por ejemplo,
Harness Audit Logs). - Selecciona Amazon S3 V2 como Tipo de fuente.
- Selecciona Harness IO como Tipo de registro.
- Haz clic en Siguiente.
Especifique los valores de los siguientes parámetros de entrada:
- URI de S3: introduce el URI del contenedor de S3 con la ruta del prefijo:
s3://harness-io-logs/harness/audit/ Opción de eliminación de la fuente: selecciona la opción de eliminación que prefieras:
- Nunca: no elimina ningún archivo después de las transferencias (opción recomendada al principio).
- Si la transferencia se realiza correctamente: elimina todos los archivos y directorios vacíos después de que la transferencia se haya completado.
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: introduce el ID de clave de acceso del
chronicle-s3-readerusuario.Clave de acceso secreta: introduce la clave de acceso secreta del usuario
chronicle-s3-reader.Espacio de nombres de recursos: el espacio de nombres de recursos. Introduce
harness.audit.Etiquetas de ingestión: etiquetas opcionales que se aplicarán a los eventos de este feed.
- URI de S3: introduce el URI del contenedor de S3 con la ruta del prefijo:
Haz clic en Siguiente.
Revise la configuración de su nuevo feed 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.