OpenCTI

Bonjour à tous, aujourd’hui on va se pencher sur la plateforme OpenCTI avec docker et portainer, comme d’habitude. OpenCTI c’est une plateforme de Cyber-Threat Intelligence qui a vocation a accueillir et gérer votre base de données d’indicateurs de compromission de sources externes ou internes et à servir de point de pivot avec l’ensemble de votre écosystème : cortex et the hive, AbuseIPDB, abuse.ch, les CVE du NIST, Splunk, et j’en passe… L’intérêt ? c’est que c’est un plateforme industrialisable avec des capacité de reporting intégré, une API toute prête, et un paquet de plugins communautaire déjà fait.

Rappel des articles de la série « Docker et Portainer » :

Bref, vous vous pouvez voir OpenCTI comme un confrère de MISP, ThreatQuotient et Anomali.

Son petit inconvénient c’est que le projet, bien que très actif, est encore jeune (Première release en Juin 2019). Son avantage c’est que le projet est très actif comme je l’ai dit et Open Source (donc pas trop couteux 🙂 ), que c’est un peu plus « propre » qu’une installation de MISP par exemple. Et en cerise sur le gâteau l’ANSSI contribue au projet !

Installation d’OpenCTI avec Docker Compose

Alors la documentation du projet est très bien faite, mais elle ne colle pas avec mon setup derrière un reverse proxy et portainter que vous connaissez. Du coup j’ai refait mon propre docker-compose.yml. Au préalable vous devez préparer un fichier .env qu’il faudra injecter dans portainer en même temps que votre stack docker.

Le fichier .env à préparer :

OPENCTI_ADMIN_EMAIL=etienne@geekeries.org
OPENCTI_ADMIN_PASSWORD=
OPENCTI_ADMIN_TOKEN=
OPENCTI_BASE_URL=http://cti.geekeries.org
MINIO_ROOT_USER=opencti
MINIO_ROOT_PASSWORD=
RABBITMQ_DEFAULT_USER=opencti
RABBITMQ_DEFAULT_PASS=
CONNECTOR_EXPORT_FILE_STIX_ID=dd817c8b-abae-460a-9ebc-97b1551e70e6
CONNECTOR_EXPORT_FILE_CSV_ID=7ba187fb-fde8-4063-92b5-c3da34060dd7
CONNECTOR_EXPORT_FILE_TXT_ID=ca715d9c-bd64-4351-91db-33a8d728a58b
CONNECTOR_IMPORT_FILE_STIX_ID=72327164-0b35-482b-b5d6-a5a3f76b845f
CONNECTOR_IMPORT_DOCUMENT_ID=c3970f8a-ce4b-4497-a381-20b7256f56f0
SMTP_HOSTNAME=smtp.geekeries.org

Quelques modification système recommandée sur le serveur dans doc d’installation. Pas testé sans, je vous laisse juger de la nécessité.

sysctl -w vm.max_map_count=1048575
echo "vm.max_map_count=1048575" >> /etc/sysctl.conf

A partir de là, vous n’avez plus qu’à embarquer la stack suivante

version: '3'
services:
  redis:
    image: redis:7.0.0
    restart: always
    volumes:
      - redisdata:/data
    networks:
      Proxy_net:
        ipv4_address: 172.20.1.1
  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:7.17.4
    volumes:
      - esdata:/usr/share/elasticsearch/data
    environment:
      # Comment out the line below for single-node
      - discovery.type=single-node
      # Uncomment line below below for a cluster of multiple nodes
      # - cluster.name=docker-cluster
      - xpack.ml.enabled=false
      - "ES_JAVA_OPTS=-Xms${ELASTIC_MEMORY_SIZE} -Xmx${ELASTIC_MEMORY_SIZE}"
    restart: always
    ulimits:
      memlock:
        soft: -1
        hard: -1
      nofile:
        soft: 65536
        hard: 65536
    networks:
      Proxy_net:
        ipv4_address: 172.20.1.2
  minio:
    image: minio/minio:RELEASE.2022-05-19T18-20-59Z
    volumes:
      - s3data:/data
    #ports:
      #- "9000:9000"
    expose:
      - 9000
    environment:
      MINIO_ROOT_USER: ${MINIO_ROOT_USER}
      MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
    command: server /data
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
      interval: 30s
      timeout: 20s
      retries: 3
    restart: always
    networks:
      Proxy_net:
        ipv4_address: 172.20.1.3
  rabbitmq:
    image: rabbitmq:3.10-management
    environment:
      - RABBITMQ_DEFAULT_USER=${RABBITMQ_DEFAULT_USER}
      - RABBITMQ_DEFAULT_PASS=${RABBITMQ_DEFAULT_PASS}
    volumes:
      - amqpdata:/var/lib/rabbitmq
    restart: always
    networks:
      Proxy_net:
        ipv4_address: 172.20.1.4
  opencti:
    image: opencti/platform:5.3.7
    environment:
      - NODE_OPTIONS=--max-old-space-size=8096
      - APP__PORT=8080
      - APP__BASE_URL=${OPENCTI_BASE_URL}
      - APP__ADMIN__EMAIL=${OPENCTI_ADMIN_EMAIL}
      - APP__ADMIN__PASSWORD=${OPENCTI_ADMIN_PASSWORD}
      - APP__ADMIN__TOKEN=${OPENCTI_ADMIN_TOKEN}
      - APP__APP_LOGS__LOGS_LEVEL=error
      - REDIS__HOSTNAME=redis
      - REDIS__PORT=6379
      - ELASTICSEARCH__URL=http://elasticsearch:9200
      - MINIO__ENDPOINT=minio
      - MINIO__PORT=9000
      - MINIO__USE_SSL=false
      - MINIO__ACCESS_KEY=${MINIO_ROOT_USER}
      - MINIO__SECRET_KEY=${MINIO_ROOT_PASSWORD}
      - RABBITMQ__HOSTNAME=rabbitmq
      - RABBITMQ__PORT=5672
      - RABBITMQ__PORT_MANAGEMENT=15672
      - RABBITMQ__MANAGEMENT_SSL=false
      - RABBITMQ__USERNAME=${RABBITMQ_DEFAULT_USER}
      - RABBITMQ__PASSWORD=${RABBITMQ_DEFAULT_PASS}
      - SMTP__HOSTNAME=${SMTP_HOSTNAME}
      - SMTP__PORT=25
      - PROVIDERS__LOCAL__STRATEGY=LocalStrategy
    #ports:
      #- "8080:8080"
    expose:
      - 8080
    depends_on:
      - redis
      - elasticsearch
      - minio
      - rabbitmq
    restart: always
    networks:
      Proxy_net:
        ipv4_address: 172.20.1.5
  worker:
    image: opencti/worker:5.3.7
    environment:
      - OPENCTI_URL=http://opencti:8080
      - OPENCTI_TOKEN=${OPENCTI_ADMIN_TOKEN}
      - WORKER_LOG_LEVEL=info
    depends_on:
      - opencti
    deploy:
      mode: replicated
      replicas: 3
    restart: always
    networks:
      Proxy_net:
  connector-export-file-stix:
    image: opencti/connector-export-file-stix:5.3.7
    environment:
      - OPENCTI_URL=http://opencti:8080
      - OPENCTI_TOKEN=${OPENCTI_ADMIN_TOKEN}
      - CONNECTOR_ID=${CONNECTOR_EXPORT_FILE_STIX_ID} # Valid UUIDv4
      - CONNECTOR_TYPE=INTERNAL_EXPORT_FILE
      - CONNECTOR_NAME=ExportFileStix2
      - CONNECTOR_SCOPE=application/json
      - CONNECTOR_CONFIDENCE_LEVEL=15 # From 0 (Unknown) to 100 (Fully trusted)
      - CONNECTOR_LOG_LEVEL=info
    restart: always
    depends_on:
      - opencti
    networks:
      Proxy_net:
        ipv4_address: 172.20.1.7
  connector-export-file-csv:
    image: opencti/connector-export-file-csv:5.3.7
    environment:
      - OPENCTI_URL=http://opencti:8080
      - OPENCTI_TOKEN=${OPENCTI_ADMIN_TOKEN}
      - CONNECTOR_ID=${CONNECTOR_EXPORT_FILE_CSV_ID} # Valid UUIDv4
      - CONNECTOR_TYPE=INTERNAL_EXPORT_FILE
      - CONNECTOR_NAME=ExportFileCsv
      - CONNECTOR_SCOPE=text/csv
      - CONNECTOR_CONFIDENCE_LEVEL=15 # From 0 (Unknown) to 100 (Fully trusted)
      - CONNECTOR_LOG_LEVEL=info
    restart: always
    depends_on:
      - opencti
    networks:
      Proxy_net:
        ipv4_address: 172.20.1.8
  connector-export-file-txt:
    image: opencti/connector-export-file-txt:5.3.7
    environment:
      - OPENCTI_URL=http://opencti:8080
      - OPENCTI_TOKEN=${OPENCTI_ADMIN_TOKEN}
      - CONNECTOR_ID=${CONNECTOR_EXPORT_FILE_TXT_ID} # Valid UUIDv4
      - CONNECTOR_TYPE=INTERNAL_EXPORT_FILE
      - CONNECTOR_NAME=ExportFileTxt
      - CONNECTOR_SCOPE=text/plain
      - CONNECTOR_CONFIDENCE_LEVEL=15 # From 0 (Unknown) to 100 (Fully trusted)
      - CONNECTOR_LOG_LEVEL=info
    restart: always
    depends_on:
      - opencti
    networks:
      Proxy_net:
        ipv4_address: 172.20.1.9
  connector-import-file-stix:
    image: opencti/connector-import-file-stix:5.3.7
    environment:
      - OPENCTI_URL=http://opencti:8080
      - OPENCTI_TOKEN=${OPENCTI_ADMIN_TOKEN}
      - CONNECTOR_ID=${CONNECTOR_IMPORT_FILE_STIX_ID} # Valid UUIDv4
      - CONNECTOR_TYPE=INTERNAL_IMPORT_FILE
      - CONNECTOR_NAME=ImportFileStix
      - CONNECTOR_VALIDATE_BEFORE_IMPORT=true # Validate any bundle before import
      - CONNECTOR_SCOPE=application/json,text/xml
      - CONNECTOR_AUTO=true # Enable/disable auto-import of file
      - CONNECTOR_CONFIDENCE_LEVEL=15 # From 0 (Unknown) to 100 (Fully trusted)
      - CONNECTOR_LOG_LEVEL=info
    restart: always
    depends_on:
      - opencti
    networks:
      Proxy_net:
        ipv4_address: 172.20.1.10
  connector-import-document:
    image: opencti/connector-import-document:5.3.7
    environment:
      - OPENCTI_URL=http://opencti:8080
      - OPENCTI_TOKEN=${OPENCTI_ADMIN_TOKEN}
      - CONNECTOR_ID=${CONNECTOR_IMPORT_DOCUMENT_ID} # Valid UUIDv4
      - CONNECTOR_TYPE=INTERNAL_IMPORT_FILE
      - CONNECTOR_NAME=ImportDocument
      - CONNECTOR_VALIDATE_BEFORE_IMPORT=true # Validate any bundle before import
      - CONNECTOR_SCOPE=application/pdf,text/plain,text/html
      - CONNECTOR_AUTO=true # Enable/disable auto-import of file
      - CONNECTOR_ONLY_CONTEXTUAL=false # Only extract data related to an entity (a report, a threat actor, etc.)
      - CONNECTOR_CONFIDENCE_LEVEL=15 # From 0 (Unknown) to 100 (Fully trusted)
      - CONNECTOR_LOG_LEVEL=info
      - IMPORT_DOCUMENT_CREATE_INDICATOR=true
    restart: always
    depends_on:
      - opencti
    networks:
      Proxy_net:
        ipv4_address: 172.20.1.11
networks:
  Proxy_net:
    external: true
volumes:
  esdata:
  s3data:
  redisdata:
  amqpdata:

Et go : « Deploy Stack » dans Portainer (pensez bien à importer le .env dans la console portainer).

A partir de là vous devriez pouvoir vous loguer sur la plateforme (après configuration de votre reverse proxy NPM) avec les accès spécifiés dans le .env.

Configurer une input de données

Bon c’est bien beau tout ça mais si j’ai pas de données dedans, elle ne me sert pas à grand chose cette plateforme, non ? Et bien oui, et c’est la la 1ère chose à faire : créer des « connecteurs »pour envoyer de la données dans la plateforme. Cela se fait tout naturellement en ajoutant des images dockers dans votre stack. Voici deux exemple pour les CVE et les URL d’urlhaus.

  connector-urlhaus:
    image: opencti/connector-urlhaus:5.3.7
    environment:
      - OPENCTI_URL=http://opencti:8080
      - OPENCTI_TOKEN=
      - HTTPS_PROXY=
      - CONNECTOR_ID=Abusech_urlhaus
      - CONNECTOR_TYPE=EXTERNAL_IMPORT
      - "CONNECTOR_NAME=Abuse.ch URLhaus"
      - CONNECTOR_SCOPE=urlhaus
      - CONNECTOR_CONFIDENCE_LEVEL=40 # From 0 (Unknown) to 100 (Fully trusted)
      - CONNECTOR_UPDATE_EXISTING_DATA=false
      - CONNECTOR_LOG_LEVEL=info
      - URLHAUS_CSV_URL=https://urlhaus.abuse.ch/downloads/csv_recent/
      - URLHAUS_IMPORT_OFFLINE=true
      - URLHAUS_CREATE_INDICATORS=true
      - URLHAUS_INTERVAL=2 # In days, must be strictly greater than 1
    restart: always
    networks:
      Proxy_net:
        ipv4_address: 172.20.1.12

  connector-cve:
    image: opencti/connector-cve:5.3.7
    environment:
      - OPENCTI_URL=http://opencti:8080
      - OPENCTI_TOKEN=
      - HTTPS_PROXY=
      - CONNECTOR_ID=nvdcve
      - CONNECTOR_TYPE=EXTERNAL_IMPORT
      - CONNECTOR_NAME=Common Vulnerabilities and Exposures
      - CONNECTOR_SCOPE=identity,vulnerability
      - CONNECTOR_CONFIDENCE_LEVEL=75 # From 0 (Unknown) to 100 (Fully trusted)
      - CONNECTOR_UPDATE_EXISTING_DATA=false
      - CONNECTOR_RUN_AND_TERMINATE=false
      - CONNECTOR_LOG_LEVEL=info
      - CVE_IMPORT_HISTORY=true # Import history at the first run (after only recent), reset the connector state if you want to re-import
      - CVE_NVD_DATA_FEED=https://nvd.nist.gov/feeds/json/cve/1.1/nvdcve-1.1-recent.json.gz
      - CVE_HISTORY_START_DATE=2002
      - CVE_HISTORY_DATA_FEED=https://nvd.nist.gov/feeds/json/cve/1.1/
      - CVE_INTERVAL=2 # In days, must be strictly greater than 1
    restart: always
    networks:
      Proxy_net:
        ipv4_address: 172.20.1.13

Notez la variable HTTPS_PROXY, ca peut être un peu tricky à faire fonctionner derrière des proxy explicites pour le coup mais on finit par y arriver :-). Anecdote, à mon ancien taf, ils galéraient un peu à rentrer une base du NIST dans un MISP. Avec OpenCTI ca m’a pris moins d’une heure là, téléchargement inclus, (coucou André si tu me lis… 😉 ). Pour finir, une liste des connecteurs est consultable facilement ici.

SI vous commencer je vous conseille fortement de commencer avec :

  • ABUSE.CH URLHAUS
  • COMMON VULNERABILITIES AND EXPOSURES

Puis ajouter :

  • APT & CYBERCRIMINALS CAMPAIGN COLLECTION
  • CYBERCRIME-TRACKER
  • URLHAUS RECENT PAYLOADS

Ensuite vous pourrez passer au augmentation qui nécessite une clé d’API :

  • ipinfo
  • abuseipdb
  • shodan
  • virustotal

Et déjà vous serez pas mal !

Créer des Streams de sortie.

Une fois vos données dans la plateforme vous pouvez simplement créé des streams en allant dans Données, Partage de données, et Créer un Stream « flux CSV » comme ci dessous.

opencti

Ces Streams vont vous permettre de définir des filtres pour exporter ou exposer facilement des sous-ensemble de votre CTI.

Créer des connecteurs pour OpenCTI

Bon et si y’a pas l’input de données qui vous intéresse ? et bien il suffit de créer la vôtre, je vous laisse regarder le code d’un connecteur mais c’est assez simple au final, la plupart des connecteurs d’import sont au final un script python d’une grosse centaine de ligne.

Selon moi, la partie la plus complexe restant le format d’import STIX qui, s’il a le mérite d’être complet et clair, n’est probablement pas le plus simple à utiliser. En particulier, si vous venez avec des besoins assez basique (genre injecter une grosse liste d’IP que jugée suspectes mais avec peu de contexte) le format STIX me semble plus embêtant qu’autre chose. Par contre pour décrire des relations complexes entre vos IoC (tels Hash sont reliés à tel C&C, qui est associé à tel malware qui fait partie de l’APT machin-chouette), la c’est nickel.

J’ai quelques projet de connecteurs en tête. Peut-être je vous ferai un post la dessus plus tard !

Conclusion

Voilà, c’est tout pour aujourd’hui et OpenCTI, de mon point de vue c’est un super outil, bien plus agréable à utiliser que MISP et avec du potentiel à développer. Je vous ajoute un peu de lecture sur la CTI avant de vous laisser :

That’s all Folks : Geekez bien !

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée.

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.