Collecter les journaux Cisco vManage SD-WAN

Compatible avec :

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)

  1. Connectez-vous à la console de gestion Cisco vManage.
  2. Accédez à Administration > Settings > Users (Administration > Paramètres > Utilisateurs).
  3. Créez un utilisateur ou utilisez un utilisateur administrateur existant disposant de droits d'accès à l'API.
  4. 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

  1. Créez un bucket Amazon S3 en suivant ce guide de l'utilisateur : Créer un bucket
  2. Enregistrez le nom et la région du bucket pour référence ultérieure (par exemple, cisco-sdwan-logs-bucket).
  3. Créez un utilisateur en suivant ce guide de l'utilisateur : Créer un utilisateur IAM.
  4. Sélectionnez l'utilisateur créé.
  5. Sélectionnez l'onglet Security credentials (Identifiants de sécurité).
  6. Cliquez sur Create Access Key (Créer une clé d'accès) dans la section Access Keys (Clés d'accès).
  7. Sélectionnez Third-party service (Service tiers) comme Use case (Cas d'utilisation).
  8. Cliquez sur Next (Suivant).
  9. Facultatif : ajoutez un tag de description.
  10. Cliquez sur Create access key (Créer une clé d'accès).
  11. 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.
  12. Cliquez sur OK.
  13. Sélectionnez l'onglet Permissions (Autorisations).
  14. Cliquez sur Add permissions (Ajouter des autorisations) dans la section Permissions policies (Règles d'autorisation).
  15. Sélectionnez Add permissions (Ajouter des autorisations).
  16. Sélectionnez Attach policies directly (Joindre directement des règles).
  17. Recherchez et sélectionnez la règle AmazonS3FullAccess.
  18. Cliquez sur Next (Suivant).
  19. Cliquez sur Add permissions (Ajouter des autorisations).

Configurer la règle et le rôle IAM pour les importations S3

  1. Dans la console AWS, accédez à IAM > Policies > Create policy > JSON tab (IAM > Règles > Créer une règle > Onglet JSON).
  2. 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-bucket si vous avez saisi un autre nom de bucket.
  3. Cliquez sur Next > Create policy (Suivant > Créer une règle).

  4. Accédez à IAM > Roles (IAM > Rôles).

  5. Cliquez sur Create role > AWS service > Lambda (Créer un rôle > Service AWS > Lambda).

  6. Associez la règle que vous venez de créer.

  7. Nommez le rôle cisco-sdwan-lambda-role, puis cliquez sur Create role (Créer un rôle).

Créer la fonction Lambda

  1. Dans la console AWS, accédez à Lambda > Functions > Create function (Lambda > Fonctions > Créer une fonction).
  2. Cliquez sur Author from scratch (Créer à partir de zéro).
  3. Fournissez les informations de configuration suivantes :

    Paramètre Valeur
    Nom cisco-sdwan-log-collector
    Durée d'exécution Python 3.13
    Architecture x86_64
    Rôle d'exécution cisco-sdwan-lambda-role
  4. Une 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)}')
            }
    

  5. Accédez à Configuration > Variables d'environnement.

  6. Cliquez sur Edit > Add new environment variable (Modifier > Ajouter une variable d'environnement).

  7. Saisissez les variables d'environnement suivantes en remplaçant par vos valeurs :

    Clé Exemple de valeur
    S3_BUCKET cisco-sdwan-logs-bucket
    S3_PREFIX cisco-sdwan/
    STATE_KEY cisco-sdwan/state.json
    VMANAGE_HOST https://your-vmanage-server:8443
    VMANAGE_USERNAME your-vmanage-username
    VMANAGE_PASSWORD your-vmanage-password
  8. Une fois la fonction créée, restez sur sa page (ou accédez à Lambda > Functions > cisco-sdwan-log-collector).

  9. Sélectionnez l'onglet Configuration.

  10. Dans le panneau General configuration (Configuration générale), cliquez sur Edit (Modifier).

  11. Définissez le délai avant expiration sur 5 minutes (300 secondes) , puis cliquez sur Save (Enregistrer).

Créer une programmation EventBridge

  1. Accédez à Amazon EventBridge > Scheduler > Create schedule (Amazon EventBridge > Planificateur > Créer une programmation).
  2. 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
  3. Cliquez sur Create schedule (Créer la programmation).

Facultatif : Créer un utilisateur et des clés IAM en lecture seule pour Google SecOps

  1. Dans la console AWS , accédez à IAM > Users > Add users (IAM > Utilisateurs > Ajouter des utilisateurs).
  2. Cliquez sur Add users (Ajouter des utilisateurs).
  3. 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)
  4. Cliquez sur Create user (Créer un utilisateur).
  5. 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).
  6. 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/*"
        }
      ]
    }
    
  7. Nommez la règle secops-reader-policy.

  8. Cliquez sur Create policy (Créer une règle).

  9. Revenez à la création de l'utilisateur, recherchez et sélectionnez secops-reader-policy.

  10. Cliquez sur Next: Tags (Suivant : Tags).

  11. Cliquez sur Next: Review (Suivant : Vérifier).

  12. Cliquez sur Create user (Créer un utilisateur).

  13. 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

  1. Accédez à Paramètres SIEM > Flux.
  2. Cliquez sur + Add New Feed (+ Ajouter un flux).
  3. Dans le champ Feed name (Nom du flux), saisissez un nom pour le flux (par exemple, Cisco SD-WAN logs).
  4. Sélectionnez Amazon S3 V2 comme Source type (Type de source).
  5. Sélectionnez Cisco vManage SD-WAN comme Log type (Type de journal).
  6. Cliquez sur Next (Suivant).
  7. 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.
  8. Cliquez sur Next (Suivant).
  9. 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.