NFLabs. エンジニアブログ

セキュリティやソフトウェア開発に関する情報を発信する技術者向けのブログです。

OpenCTIのコネクタを開発してみた

こんにちは。ソリューション事業部セキュリティソリューション担当の今池です。 この記事はNFLabs. アドベントカレンダー8日目です。普段の業務では、OpenCTIを中心としたサイバー脅威インテリジェンスプラットフォームの運用・開発を中心に行っています。

今回はOpenCTIを実際に運用する中で機能拡張のためにコネクタを開発する機会があったので、その方法を紹介したいと思います。

OpenCTI&コネクタとは

OpenCTIとは、サイバー脅威インテリジェンス(CTI)プラットフォームの一つであり、IOCなどのサイバー攻撃に関する情報を整理・蓄積するために使われます。STIX2.1*1に準拠しており幅広い脅威情報を標準フォーマットで記述できることや後述するコネクタによる高い機能拡張性が特徴です。

余談ではありますが、バージョン 5.4.0 から追加された日本語表示機能は我々のチームが作成しました。翻訳作業を行った際の記事も公開されていますので、ぜひご覧ください。

blog.nflabs.jp

そして、今回のテーマであるコネクタです。コネクタとはOpenCTIの機能を拡張するためのプラグインであり、外部インテリジェンスサービスとの連携機能やデータのエクスポート機能など、コネクタによって様々な機能をOpenCTIに追加することができます。

コネクタは機能に応じて5種類のタイプに分類されています。主要なコネクタはOpenCTI公式のGithubリポジトリ *2 や公式ドキュメント *3 に公開されているので、これらのページを見るとコネクタの連携先やできることのイメージがつきやすいかと思います。

コネクタタイプ 機能
external-import インテリジェンスサービスなどの外部ソースから脅威情報を取得してOpenCTIに保存する
internal-enrichment 外部ソースの情報を既存データに追加する
internal-export-file 既存データをpdfやcsvなどのフォーマットで出力する
internal-import-file ドキュメントから脅威情報を抽出してOpenCTIに保存する
stream SIEMやEDRと連携しリアルタイムでデータの共有や通信を行う

https://www.notion.so/OpenCTI-Ecosystem-868329e9fb734fca89692b2ed6087e76

具体例を一つ挙げると、VirusTotalのinternal-enrichmentコネクタでは、OpenCTIに登録されたIPアドレスについてVirusTotalの判定結果を画像のようにOpenCTIに追加するといった機能があります。

他にも、外部インテリジェンスサービスからレポートや検体のハッシュ値などのインディケータを取得するなどの様々な機能があり、コネクタを使うことで効率よく脅威情報をOpenCTIに蓄積することができます。

コネクタの作り方

現時点(OpenCTI version 5.4.1)でおよそ80種類のコネクタが公開されており、それらを利用するだけでも多くのインテリジェンスサービスとの連携や基本的なデータの入出力が可能になります。

一方で、OpenCTIを実際に運用してみると、インテリジェンスサービスとの連携のために公開されているコネクタを使ってみたものの欲しい情報が得られなかった、また、内部のデータソースと連携したいがコネクタがないというケースもあるかと思います。そんなときには独自のコネクタを作ってみるというのも一つの選択肢です。

開発方法については公式のドキュメント *4 にまとめられており、今回はこちらを参考にコネクタの作成方法を紹介します。

開発全体の流れは以下のようになります。

  1. 環境構築
  2. 機能実装
  3. コンフィグの設定&コンテナイメージのビルド

では、流れに沿って開発手順を見ていきましょう。

1. 環境構築

環境構築と書いてはいますが、実際にすることは公式のリポジトリからコネクタ開発用のテンプレートディレクトリをコピーするだけです。以降はこのテンプレート内のファイルを修正してコネクタの機能を実装していきます。

今回の説明ではテンプレートのディレクトリ構造やファイル名を特に変更せずそのまま使っていますが、最終的にはコンテナ化して動作させることができればいいので、適宜開発しやすいようにカスタマイズして問題ありません。

$ git clone https://github.com/OpenCTI-Platform/connectors.git
$ cp -r connectors/template/ my_connector
$ tree my_connector
my_connector
├── docker-compose.yml
├── Dockerfile
├── entrypoint.sh
├── README.md
└── src
    ├── config.yml.sample
    ├── main.py
    └── requirements.txt

1 directory, 7 files

2. 機能実装

コネクタの機能はsrc/main.pyのコネクタクラス(今回の場合はMyConnector)に実装していきます。ステータスの登録などコネクタ固有の処理は専用のクラス(OpenCTIConnectorHelper)が行ってくれるため実装する必要はなく、実装する必要があるのはインテリジェンスサービスとの連携などのメインとなる機能のみです。

コネクタのタイプによって少し実装が異なりますが *5 、基本的にコネクタクラスにメソッドを追加して処理を記述するという点は同じであるため、今回はinternal-enrichmentタイプのコネクタを例に説明します 。

以下のコードがinternal-enrichmentタイプの基本的な形です。internal-enrichmentタイプのコネクタはプラットフォームからのメッセージを受信することをトリガに動作するため、メッセージを受信するstartメソッドと受け取ったメッセージに応じた処理を行う_process_dataメソッドを新たに追加しています。コネクタのメイン機能をこの_process_dataメソッドに実装します。

# my_connector/src/main.py

class MyConnector:
    def __init__(self):
        config_file_path = os.path.dirname(os.path.abspath(__file__)) + "/config.yml"
        config = (
            yaml.load(open(config_file_path), Loader=yaml.FullLoader)
            if os.path.isfile(config_file_path)
            else {}
        )
        self.helper = OpenCTIConnectorHelper(config)

    def _process_message(self, data: dict[str, str]) -> str:
        # コネクタのメイン機能をここに実装             

    def start(self) -> None:
        self.helper.listen(self._process_message)


if __name__ == "__main__":
    try:
        connector = MyConnector()
        connector.start()
    except Exception as e:
        print(e)
        time.sleep(10)
        sys.exit(0)

3. コンフィグの設定&コンテナイメージのビルド

最後にコンフィグの設定とイメージのビルド、デプロイの方法について説明します。

現状のDockerfiledocker-compose.yml, entrypoint.shはテンプレートをコピーしただけの状態であるため、適切な内容に修正する必要があります。

変更すべき値はTemplateChangemeといった値で書かれているので、以下のようにgrepで抽出するとわかりやすいです。なお、config.ymlをコンフィグとして使うこともできるのですが、今回はコンフィグを環境変数から読み出すため使いません。

$ grep -Ri -e template -e CHANGEME my_connector --exclude=README.md --exclude=config.yml.sample
my_connector/src/main.py:class TemplateConnector:
my_connector/src/main.py:            "TEMPLATE_ATTRIBUTE", ["template", "attribute"], config, True
my_connector/src/main.py:        connector = TemplateConnector()
my_connector/docker-compose.yml:  connector-template:
my_connector/docker-compose.yml:    image: opencti/connector-template:5.4.1
my_connector/docker-compose.yml:      - OPENCTI_TOKEN=ChangeMe
my_connector/docker-compose.yml:      - CONNECTOR_ID=ChangeMe
my_connector/docker-compose.yml:      - CONNECTOR_TYPE=Template_Type
my_connector/docker-compose.yml:      - CONNECTOR_NAME=Template
my_connector/docker-compose.yml:      - CONNECTOR_SCOPE=Template_Scope # MIME type or Stix Object
my_connector/Dockerfile:COPY src /opt/opencti-template
my_connector/Dockerfile:    cd /opt/opencti-connector-template && \
my_connector/entrypoint.sh:cd /opt/opencti-connector-template

コネクタで使用される基本的な環境変数には以下のようなものがあります。

環境変数名 概要
OPENCTI_TOKEN コネクタが使用するためのトークン。OpenCTIの各ユーザに割り当てられているため、適切なユーザのトークンを使用する
CONNECTOR_ID コネクタに割り当てる固有のID(uuidv4)
CONNECTOR_TYPE コネクタのタイプ、EXTERNAL_IMPORT, INTERNAL_ENRICHMENT, INTERNAL_EXPORT_FILE, INTERNAL_IMPORT_FILE, STREAM
CONNECTOR_NAME コネクタの名前
CONNECTOR_SCOPE コネクタが扱うデータのタイプ。application/pdfのようなMIMEやIndicatorのようなSTIXオブジェクト

Dockerfiledocker-compose.yml, entrypoint.shにコネクタに応じた値を設定することで、開発はほぼ完了です。あとは、作成したコネクタのコンテナイメージをビルドし、プラットフォーム側のdocker-compose.ymlにコネクタ側の設定を追加した後に、docker compose up -dでコネクタをデプロイできます。

これがコネクタを開発する際の一連の流れになります。

コネクタを開発してみた

実際に上記で説明した流れに沿ってコネクタを開発してみました。

今回作成したのは、OpenCTIにFileタイプのObervable *6が保存されたときにVirusTotalから関連のあるIPアドレスを取得して、OpenCTIに保存するinternal-enrichmentコネクタです。

コードの中身を一部抜粋して説明します。コードの全体はGithub *7 に公開しているので、興味がある方はそちらを参照してください。

# (snip.)

@dataclass
class VirusTotalEnrichConnector:
    config_file_path: str = os.path.dirname(os.path.abspath(__file__)) + "/config.yml"

    def __post_init__(self):
        self.config = (
            yaml.load(open(self.config_file_path), Loader=yaml.SafeLoader)
            if os.path.isfile(self.config_file_path)
            else {}
        )
        self.helper = OpenCTIConnectorHelper(self.config)
        self.token = get_config_variable(
            "VIRUSTOTAL_TOKEN", ["virustotal", "token"], self.config, False
        )

    def _process_message(self, data: dict[str, str]) -> None:
        vt_client = VirusTotalClient(token=self.token)
        vt_builder = VirusTotalBuilder()

        observable = self.helper.api.stix_cyber_observable.read(id=data["entity_id"])
        related_ip_addresses = vt_client.get_related_ipaddress(
            sha256=observable["observable_value"] # observable["observable_value"]: sha256
        )

        if related_ip_addresses:
            stix2_ipaddress_observables = [
                vt_builder.create_ip_address_observable(ip_address)
                for ip_address in related_ip_addresses
            ]
            stix2_bundle = vt_builder.create_bundle(stix2_ipaddress_observables)

            self.helper.send_stix2_bundle(stix2_bundle.serialize())

    def start(self):
        self.helper.listen(self._process_message)


if __name__ == "__main__":
    try:
        connector = VirusTotalEnrichConnector()
        connector.start()
    except Exception as e:
        print(e)
        time.sleep(10)
        sys.exit(0)

VirusTotalのAPIを叩く処理や取得したIPアドレスをSTIXオブジェクトに変換するなどの一部の処理を別モジュールに切り出していますが、全体の構造はテンプレートと同じで、コネクタクラスに_process_messageメソッドを追加して、その中で一連の処理を実行しています。

処理の流れは以下のようになっています。

  1. プラットフォームにエンティティ(File observable)が保存されると同時に、コネクタがそのエンティティIDを受信する
  2. エンティティIDをもとに、コネクタがプラットフォームに問い合わせハッシュ値を取得する
  3. ハッシュ値をVirusTotalで検索して関連するIPアドレスを取得する
  4. IPアドレスをSTIXオブジェクト( IP Address observable *8 *9 )に変換する
  5. 作成したSTIXオブジェクトをバンドル化してOpenCTIに送信し登録する

最後に、実際に作成したコネクタを動かしてみます。

コンテナイメージをビルドして動かしてみると、起動に成功しプラットフォームと通信ができた時点でコネクタがプラットフォームに登録されます。今回の設定はCONNECTOR_TYPE=INTERNAL_ENRICHMENT, CONNECTOR_NAME=virustotal_enrichment_connectorであり、コネクタの管理ページを見ると正しく反映されていることがわかります。

次に、FileタイプのObervableをOpenCTIに作成したときに関連するIPアドレスを自動で保存するか試してみます。ブラウザ上から新しく作成してみると、

Observableの作成と同時にコネクタが動作し関連するIPアドレスがOpenCTIに保存されており、作成したコネクタが想定通りの動作が確認できました。

おわりに

OpenCTIのコネクタについての概要と開発方法について紹介しました。また、一例として、外部インテリジェンスサービスから脅威情報を取得してOpenCTIに保存する自作のコネクタについてその実装内容とともに紹介させていただきました。

脅威情報を扱う上で役に立つコネクタが多く公開されているので、ぜひOpenCTIとコネクタを使った脅威情報収集を試してみてください。そして、オリジナルのコネクタを開発する際に本記事が有益な情報を提供できれば幸いです。