Collecter les journaux Cisco vManage SD-WAN
Ce document explique comment ingérer des journaux Cisco vManage SD-WAN dans Google Security Operations à l'aide d'Amazon S3.
Avant de commencer
Assurez-vous de remplir les conditions suivantes :
- Une instance Google SecOps
- Un accès privilégié à la console de gestion Cisco vManage SD-WAN
- Un accès privilégié à AWS (S3, IAM, Lambda, EventBridge)
Collecter les prérequis Cisco vManage SD-WAN (identifiants et URL de base)
- Connectez-vous à la console de gestion Cisco vManage.
- Accédez à Administration > Settings > Users (Administration > Paramètres > Utilisateurs).
- Créez un utilisateur ou utilisez un utilisateur administrateur existant disposant de droits d'accès à l'API.
- Copiez et enregistrez les informations suivantes dans un emplacement sécurisé :
- Username (Nom d'utilisateur)
- Password (Mot de passe)
- vManage Base URL (URL de base vManage) (par exemple,
https://your-vmanage-server:8443)
Configurer le bucket AWS S3 et IAM pour Google SecOps
- Créez un bucket Amazon S3 en suivant ce guide de l'utilisateur : Créer un bucket
- Enregistrez le nom et la région du bucket pour référence ultérieure (par exemple,
cisco-sdwan-logs-bucket). - Créez un utilisateur en suivant ce guide de l'utilisateur : Créer un utilisateur IAM.
- Sélectionnez l'utilisateur créé.
- Sélectionnez l'onglet Security credentials (Identifiants de sécurité).
- Cliquez sur Create Access Key (Créer une clé d'accès) dans la section Access Keys (Clés d'accès).
- Sélectionnez Third-party service (Service tiers) comme Use case (Cas d'utilisation).
- Cliquez sur Next (Suivant).
- Facultatif : ajoutez un tag de description.
- Cliquez sur Create access key (Créer une clé d'accès).
- Cliquez sur Download CSV file (Télécharger le fichier CSV) pour enregistrer la clé d'accès et la clé d'accès secrète pour une utilisation ultérieure.
- Cliquez sur OK.
- Sélectionnez l'onglet Permissions (Autorisations).
- Cliquez sur Add permissions (Ajouter des autorisations) dans la section Permissions policies (Règles d'autorisation).
- Sélectionnez Add permissions (Ajouter des autorisations).
- Sélectionnez Attach policies directly (Joindre directement des règles).
- Recherchez et sélectionnez la règle AmazonS3FullAccess.
- Cliquez sur Next (Suivant).
- Cliquez sur Add permissions (Ajouter des autorisations).
Configurer la règle et le rôle IAM pour les importations S3
- Dans la console AWS, accédez à IAM > Policies > Create policy > JSON tab (IAM > Règles > Créer une règle > Onglet JSON).
Saisissez la règle suivante :
{ "Version": "2012-10-17", "Statement": [ { "Sid": "AllowPutObjects", "Effect": "Allow", "Action": "s3:PutObject", "Resource": "arn:aws:s3:::cisco-sdwan-logs-bucket/*" }, { "Sid": "AllowGetStateObject", "Effect": "Allow", "Action": "s3:GetObject", "Resource": "arn:aws:s3:::cisco-sdwan-logs-bucket/cisco-sdwan/state.json" } ] }- Remplacez
cisco-sdwan-logs-bucketsi vous avez saisi un autre nom de bucket.
- Remplacez
Cliquez sur Next > Create policy (Suivant > Créer une règle).
Accédez à IAM > Roles (IAM > Rôles).
Cliquez sur Create role > AWS service > Lambda (Créer un rôle > Service AWS > Lambda).
Associez la règle que vous venez de créer.
Nommez le rôle
cisco-sdwan-lambda-role, puis cliquez sur Create role (Créer un rôle).
Créer la fonction Lambda
- Dans la console AWS, accédez à Lambda > Functions > Create function (Lambda > Fonctions > Créer une fonction).
- Cliquez sur Author from scratch (Créer à partir de zéro).
Fournissez les informations de configuration suivantes :
Paramètre Valeur Nom cisco-sdwan-log-collectorDurée d'exécution Python 3.13 Architecture x86_64 Rôle d'exécution cisco-sdwan-lambda-roleUne fois la fonction créée, ouvrez l'onglet Code, supprimez le stub et saisissez le code suivant (
cisco-sdwan-log-collector.py) :import json import boto3 import os import urllib3 import urllib.parse from datetime import datetime, timezone from botocore.exceptions import ClientError # Disable SSL warnings for self-signed certificates urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) # Environment variables VMANAGE_HOST = os.environ['VMANAGE_HOST'] VMANAGE_USERNAME = os.environ['VMANAGE_USERNAME'] VMANAGE_PASSWORD = os.environ['VMANAGE_PASSWORD'] S3_BUCKET = os.environ['S3_BUCKET'] S3_PREFIX = os.environ['S3_PREFIX'] STATE_KEY = os.environ['STATE_KEY'] s3_client = boto3.client('s3') http = urllib3.PoolManager(cert_reqs='CERT_NONE') class VManageAPI: def __init__(self, host, username, password): self.host = host.rstrip('/') self.username = username self.password = password self.cookies = None self.token = None def authenticate(self): """Authenticate with vManage and get session tokens""" try: # Login to get JSESSIONID login_url = f"{self.host}/j_security_check" login_data = urllib.parse.urlencode({ 'j_username': self.username, 'j_password': self.password }) response = http.request( 'POST', login_url, body=login_data, headers={'Content-Type': 'application/x-www-form-urlencoded'}, timeout=30 ) # Check if login was successful (vManage returns HTML on failure) if b'<html>' in response.data or response.status != 200: raise Exception("Authentication failed") # Extract cookies self.cookies = {} if 'Set-Cookie' in response.headers: cookie_header = response.headers['Set-Cookie'] for cookie in cookie_header.split(';'): if 'JSESSIONID=' in cookie: self.cookies['JSESSIONID'] = cookie.split('JSESSIONID=')[1].split(';')[0] break if not self.cookies.get('JSESSIONID'): raise Exception("Failed to get JSESSIONID") # Get XSRF token token_url = f"{self.host}/dataservice/client/token" headers = { 'Content-Type': 'application/json', 'Cookie': f"JSESSIONID={self.cookies['JSESSIONID']}" } response = http.request('GET', token_url, headers=headers, timeout=30) if response.status == 200: self.token = response.data.decode('utf-8') return True else: raise Exception(f"Failed to get XSRF token: {response.status}") except Exception as e: print(f"Authentication error: {e}") return False def get_headers(self): """Get headers for API requests""" return { 'Content-Type': 'application/json', 'Cookie': f"JSESSIONID={self.cookies['JSESSIONID']}", 'X-XSRF-TOKEN': self.token } def get_audit_logs(self, last_timestamp=None): """Get audit logs from vManage""" try: url = f"{self.host}/dataservice/auditlog" headers = self.get_headers() # Build query for recent logs query = { "query": { "condition": "AND", "rules": [] }, "size": 10000 } # Add timestamp filter if provided if last_timestamp: # Convert timestamp to epoch milliseconds for vManage API if isinstance(last_timestamp, str): try: dt = datetime.fromisoformat(last_timestamp.replace('Z', '+00:00')) epoch_ms = int(dt.timestamp() * 1000) except: epoch_ms = int(last_timestamp) else: epoch_ms = int(last_timestamp) query["query"]["rules"].append({ "value": [str(epoch_ms)], "field": "entry_time", "type": "date", "operator": "greater" }) else: # Get last 1 hour of logs by default query["query"]["rules"].append({ "value": ["1"], "field": "entry_time", "type": "date", "operator": "last_n_hours" }) response = http.request( 'POST', url, body=json.dumps(query), headers=headers, timeout=60 ) if response.status == 200: return json.loads(response.data.decode('utf-8')) else: print(f"Failed to get audit logs: {response.status}") return None except Exception as e: print(f"Error getting audit logs: {e}") return None def get_alarms(self, last_timestamp=None): """Get alarms from vManage""" try: url = f"{self.host}/dataservice/alarms" headers = self.get_headers() # Build query for recent alarms query = { "query": { "condition": "AND", "rules": [] }, "size": 10000 } # Add timestamp filter if provided if last_timestamp: # Convert timestamp to epoch milliseconds for vManage API if isinstance(last_timestamp, str): try: dt = datetime.fromisoformat(last_timestamp.replace('Z', '+00:00')) epoch_ms = int(dt.timestamp() * 1000) except: epoch_ms = int(last_timestamp) else: epoch_ms = int(last_timestamp) query["query"]["rules"].append({ "value": [str(epoch_ms)], "field": "entry_time", "type": "date", "operator": "greater" }) else: # Get last 1 hour of alarms by default query["query"]["rules"].append({ "value": ["1"], "field": "entry_time", "type": "date", "operator": "last_n_hours" }) response = http.request( 'POST', url, body=json.dumps(query), headers=headers, timeout=60 ) if response.status == 200: return json.loads(response.data.decode('utf-8')) else: print(f"Failed to get alarms: {response.status}") return None except Exception as e: print(f"Error getting alarms: {e}") return None def get_events(self, last_timestamp=None): """Get events from vManage""" try: url = f"{self.host}/dataservice/events" headers = self.get_headers() # Build query for recent events query = { "query": { "condition": "AND", "rules": [] }, "size": 10000 } # Add timestamp filter if provided if last_timestamp: # Convert timestamp to epoch milliseconds for vManage API if isinstance(last_timestamp, str): try: dt = datetime.fromisoformat(last_timestamp.replace('Z', '+00:00')) epoch_ms = int(dt.timestamp() * 1000) except: epoch_ms = int(last_timestamp) else: epoch_ms = int(last_timestamp) query["query"]["rules"].append({ "value": [str(epoch_ms)], "field": "entry_time", "type": "date", "operator": "greater" }) else: # Get last 1 hour of events by default query["query"]["rules"].append({ "value": ["1"], "field": "entry_time", "type": "date", "operator": "last_n_hours" }) response = http.request( 'POST', url, body=json.dumps(query), headers=headers, timeout=60 ) if response.status == 200: return json.loads(response.data.decode('utf-8')) else: print(f"Failed to get events: {response.status}") return None except Exception as e: print(f"Error getting events: {e}") return None def get_last_run_time(): """Get the last successful run timestamp from S3""" try: response = s3_client.get_object(Bucket=S3_BUCKET, Key=STATE_KEY) state_data = json.loads(response['Body'].read()) return state_data.get('last_run_time') except ClientError as e: if e.response['Error']['Code'] == 'NoSuchKey': print("No previous state found, collecting last hour of logs") return None else: print(f"Error reading state: {e}") return None except Exception as e: print(f"Error reading state: {e}") return None def update_last_run_time(timestamp): """Update the last successful run timestamp in S3""" try: state_data = { 'last_run_time': timestamp, 'updated_at': datetime.now(timezone.utc).isoformat() } s3_client.put_object( Bucket=S3_BUCKET, Key=STATE_KEY, Body=json.dumps(state_data), ContentType='application/json' ) print(f"Updated state with timestamp: {timestamp}") except Exception as e: print(f"Error updating state: {e}") def upload_logs_to_s3(logs_data, log_type, timestamp): """Upload logs to S3 bucket""" try: if not logs_data or 'data' not in logs_data or not logs_data['data']: print(f"No {log_type} data to upload") return # Create filename with timestamp dt = datetime.now(timezone.utc) filename = f"{S3_PREFIX}{log_type}/{dt.strftime('%Y/%m/%d')}/{log_type}_{dt.strftime('%Y%m%d_%H%M%S')}.json" # Upload to S3 s3_client.put_object( Bucket=S3_BUCKET, Key=filename, Body=json.dumps(logs_data), ContentType='application/json' ) print(f"Uploaded {len(logs_data['data'])} {log_type} records to s3://{S3_BUCKET}/{filename}") except Exception as e: print(f"Error uploading {log_type} to S3: {e}") def lambda_handler(event, context): """Main Lambda handler function""" print(f"Starting Cisco vManage log collection at {datetime.now(timezone.utc)}") try: # Get last run time last_run_time = get_last_run_time() # Initialize vManage API client vmanage = VManageAPI(VMANAGE_HOST, VMANAGE_USERNAME, VMANAGE_PASSWORD) # Authenticate if not vmanage.authenticate(): return { 'statusCode': 500, 'body': json.dumps('Failed to authenticate with vManage') } print("Successfully authenticated with vManage") # Current timestamp for state tracking (store as epoch milliseconds) current_time = int(datetime.now(timezone.utc).timestamp() * 1000) # Collect different types of logs log_types = [ ('audit_logs', vmanage.get_audit_logs), ('alarms', vmanage.get_alarms), ('events', vmanage.get_events) ] total_records = 0 for log_type, get_function in log_types: try: print(f"Collecting {log_type}...") logs_data = get_function(last_run_time) if logs_data: upload_logs_to_s3(logs_data, log_type, current_time) if 'data' in logs_data: total_records += len(logs_data['data']) except Exception as e: print(f"Error processing {log_type}: {e}") continue # Update state with current timestamp update_last_run_time(current_time) print(f"Collection completed. Total records processed: {total_records}") return { 'statusCode': 200, 'body': json.dumps({ 'message': 'Log collection completed successfully', 'total_records': total_records, 'timestamp': datetime.now(timezone.utc).isoformat() }) } except Exception as e: print(f"Lambda execution error: {e}") return { 'statusCode': 500, 'body': json.dumps(f'Error: {str(e)}') }Accédez à Configuration > Variables d'environnement.
Cliquez sur Edit > Add new environment variable (Modifier > Ajouter une variable d'environnement).
Saisissez les variables d'environnement suivantes en remplaçant par vos valeurs :
Clé Exemple de valeur S3_BUCKETcisco-sdwan-logs-bucketS3_PREFIXcisco-sdwan/STATE_KEYcisco-sdwan/state.jsonVMANAGE_HOSThttps://your-vmanage-server:8443VMANAGE_USERNAMEyour-vmanage-usernameVMANAGE_PASSWORDyour-vmanage-passwordUne fois la fonction créée, restez sur sa page (ou accédez à Lambda > Functions > cisco-sdwan-log-collector).
Sélectionnez l'onglet Configuration.
Dans le panneau General configuration (Configuration générale), cliquez sur Edit (Modifier).
Définissez le délai avant expiration sur 5 minutes (300 secondes) , puis cliquez sur Save (Enregistrer).
Créer une programmation EventBridge
- Accédez à Amazon EventBridge > Scheduler > Create schedule (Amazon EventBridge > Planificateur > Créer une programmation).
- Fournissez les informations de configuration suivantes :
- Recurring schedule (Programmation récurrente) : Rate (Fréquence) (
1 hour) - Target (Cible) : votre fonction Lambda
cisco-sdwan-log-collector - Name (Nom) :
cisco-sdwan-log-collector-1h
- Recurring schedule (Programmation récurrente) : Rate (Fréquence) (
- Cliquez sur Create schedule (Créer la programmation).
Facultatif : Créer un utilisateur et des clés IAM en lecture seule pour Google SecOps
- Dans la console AWS , accédez à IAM > Users > Add users (IAM > Utilisateurs > Ajouter des utilisateurs).
- Cliquez sur Add users (Ajouter des utilisateurs).
- Fournissez les informations de configuration suivantes :
- User (Utilisateur) :
secops-reader - Access type (Type d'accès) : Access key — Programmatic access (Clé d'accès – Accès programmatique)
- User (Utilisateur) :
- Cliquez sur Create user (Créer un utilisateur).
- Associez une règle de lecture minimale (personnalisée) : Users > secops-reader > Permissions > Add permissions > Attach policies directly > Create policy (Utilisateurs > secops-reader > Autorisations > Ajouter des autorisations > Joindre directement des règles > Créer une règle).
Dans l'éditeur JSON, saisissez la règle suivante :
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": ["s3:ListBucket"], "Resource": "arn:aws:s3:::cisco-sdwan-logs-bucket", "Condition": { "StringLike": { "s3:prefix": ["cisco-sdwan/*"] } } }, { "Effect": "Allow", "Action": ["s3:GetObject"], "Resource": "arn:aws:s3:::cisco-sdwan-logs-bucket/cisco-sdwan/*" } ] }Nommez la règle
secops-reader-policy.Cliquez sur Create policy (Créer une règle).
Revenez à la création de l'utilisateur, recherchez et sélectionnez
secops-reader-policy.Cliquez sur Next: Tags (Suivant : Tags).
Cliquez sur Next: Review (Suivant : Vérifier).
Cliquez sur Create user (Créer un utilisateur).
Téléchargez le CSV (ces valeurs sont saisies dans le flux).
Configurer un flux dans Google SecOps pour ingérer les journaux Cisco vManage SD-WAN
- Accédez à Paramètres SIEM > Flux.
- Cliquez sur + Add New Feed (+ Ajouter un flux).
- Dans le champ Feed name (Nom du flux), saisissez un nom pour le flux (par exemple,
Cisco SD-WAN logs). - Sélectionnez Amazon S3 V2 comme Source type (Type de source).
- Sélectionnez Cisco vManage SD-WAN comme Log type (Type de journal).
- Cliquez sur Next (Suivant).
- Spécifiez les valeurs des paramètres d'entrée suivants :
- S3 URI (URI S3) :
s3://cisco-sdwan-logs-bucket/cisco-sdwan/ - Source deletion options (Options de suppression de la source) : sélectionnez l'option de suppression de votre choix.
- Maximum File Age (Âge maximal des fichiers) : incluez les fichiers modifiés au cours des derniers jours. Par défaut, 180 jours.
- Access Key ID (ID de clé d'accès) : clé d'accès utilisateur avec accès au bucket S3.
- Secret Access Key (Clé d'accès secrète) : clé secrète de l'utilisateur avec accès au bucket S3.
- Asset namespace (Espace de noms de l'élément) : l'espace de noms de l'élément.
- Ingestion labels (Libellés d'ingestion) : libellé appliqué aux événements de ce flux.
- S3 URI (URI S3) :
- Cliquez sur Next (Suivant).
- Vérifiez la configuration de votre nouveau flux dans l'écran Finalize (Finaliser), puis cliquez sur Submit (Envoyer).
Vous avez encore besoin d'aide ? Obtenez des réponses auprès des membres de la communauté et des professionnels Google SecOps.