こんにちは。ソリューション事業部セキュリティソリューション担当の今池です。 この記事はNFLabs. アドベントカレンダー8日目です。普段の業務では、OpenCTIを中心としたサイバー脅威インテリジェンスプラットフォームの運用・開発を中心に行っています。
今回はOpenCTIを実際に運用する中で機能拡張のためにコネクタを開発する機会があったので、その方法を紹介したいと思います。
OpenCTI&コネクタとは
OpenCTIとは、サイバー脅威インテリジェンス(CTI)プラットフォームの一つであり、IOCなどのサイバー攻撃に関する情報を整理・蓄積するために使われます。STIX2.1*1に準拠しており幅広い脅威情報を標準フォーマットで記述できることや後述するコネクタによる高い機能拡張性が特徴です。
余談ではありますが、バージョン 5.4.0 から追加された日本語表示機能は我々のチームが作成しました。翻訳作業を行った際の記事も公開されていますので、ぜひご覧ください。
そして、今回のテーマであるコネクタです。コネクタとはOpenCTIの機能を拡張するためのプラグインであり、外部インテリジェンスサービスとの連携機能やデータのエクスポート機能など、コネクタによって様々な機能をOpenCTIに追加することができます。
コネクタは機能に応じて5種類のタイプに分類されています。主要なコネクタはOpenCTI公式のGithubリポジトリ *2 や公式ドキュメント *3 に公開されているので、これらのページを見るとコネクタの連携先やできることのイメージがつきやすいかと思います。
コネクタタイプ | 機能 |
---|---|
external-import | インテリジェンスサービスなどの外部ソースから脅威情報を取得してOpenCTIに保存する |
internal-enrichment | 外部ソースの情報を既存データに追加する |
internal-export-file | 既存データをpdfやcsvなどのフォーマットで出力する |
internal-import-file | ドキュメントから脅威情報を抽出してOpenCTIに保存する |
stream | SIEMやEDRと連携しリアルタイムでデータの共有や通信を行う |
具体例を一つ挙げると、VirusTotalのinternal-enrichment
コネクタでは、OpenCTIに登録されたIPアドレスについてVirusTotalの判定結果を画像のようにOpenCTIに追加するといった機能があります。
他にも、外部インテリジェンスサービスからレポートや検体のハッシュ値などのインディケータを取得するなどの様々な機能があり、コネクタを使うことで効率よく脅威情報をOpenCTIに蓄積することができます。
コネクタの作り方
現時点(OpenCTI version 5.4.1)でおよそ80種類のコネクタが公開されており、それらを利用するだけでも多くのインテリジェンスサービスとの連携や基本的なデータの入出力が可能になります。
一方で、OpenCTIを実際に運用してみると、インテリジェンスサービスとの連携のために公開されているコネクタを使ってみたものの欲しい情報が得られなかった、また、内部のデータソースと連携したいがコネクタがないというケースもあるかと思います。そんなときには独自のコネクタを作ってみるというのも一つの選択肢です。
開発方法については公式のドキュメント *4 にまとめられており、今回はこちらを参考にコネクタの作成方法を紹介します。
開発全体の流れは以下のようになります。
- 環境構築
- 機能実装
- コンフィグの設定&コンテナイメージのビルド
では、流れに沿って開発手順を見ていきましょう。
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. コンフィグの設定&コンテナイメージのビルド
最後にコンフィグの設定とイメージのビルド、デプロイの方法について説明します。
現状のDockerfile
やdocker-compose.yml
, entrypoint.sh
はテンプレートをコピーしただけの状態であるため、適切な内容に修正する必要があります。
変更すべき値はTemplate
やChangeme
といった値で書かれているので、以下のように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オブジェクト |
Dockerfile
やdocker-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
メソッドを追加して、その中で一連の処理を実行しています。
処理の流れは以下のようになっています。
- プラットフォームにエンティティ(File observable)が保存されると同時に、コネクタがそのエンティティIDを受信する
- エンティティIDをもとに、コネクタがプラットフォームに問い合わせハッシュ値を取得する
- ハッシュ値をVirusTotalで検索して関連するIPアドレスを取得する
- IPアドレスをSTIXオブジェクト( IP Address observable *8 *9 )に変換する
- 作成したSTIXオブジェクトをバンドル化してOpenCTIに送信し登録する
最後に、実際に作成したコネクタを動かしてみます。
コンテナイメージをビルドして動かしてみると、起動に成功しプラットフォームと通信ができた時点でコネクタがプラットフォームに登録されます。今回の設定はCONNECTOR_TYPE=INTERNAL_ENRICHMENT
, CONNECTOR_NAME=virustotal_enrichment_connector
であり、コネクタの管理ページを見ると正しく反映されていることがわかります。
次に、FileタイプのObervableをOpenCTIに作成したときに関連するIPアドレスを自動で保存するか試してみます。ブラウザ上から新しく作成してみると、
Observableの作成と同時にコネクタが動作し関連するIPアドレスがOpenCTIに保存されており、作成したコネクタが想定通りの動作が確認できました。
おわりに
OpenCTIのコネクタについての概要と開発方法について紹介しました。また、一例として、外部インテリジェンスサービスから脅威情報を取得してOpenCTIに保存する自作のコネクタについてその実装内容とともに紹介させていただきました。
脅威情報を扱う上で役に立つコネクタが多く公開されているので、ぜひOpenCTIとコネクタを使った脅威情報収集を試してみてください。そして、オリジナルのコネクタを開発する際に本記事が有益な情報を提供できれば幸いです。
*1: https://oasis-open.github.io/cti-documentation/stix/intro.html
*2:https://github.com/OpenCTI-Platform/connectors
*3:https://www.notion.so/OpenCTI-Ecosystem-868329e9fb734fca89692b2ed6087e76
*4:https://www.notion.so/Connector-Development-06b2690697404b5ebc6e3556a1385940
*5:コネクタのタイプによって追加するメソッドが異なります。https://www.notion.so/Connector-Development-06b2690697404b5ebc6e3556a1385940#4b0b0c63683d4b68a52c44dc4a785458
*6: OpenCTIが採用しているデータ形式であるSTIX2.1のオブジェクトの一種。 https://docs.oasis-open.org/cti/stix/v2.1/csprd01/stix-v2.1-csprd01.html#_Toc16070696
*7:https://github.com/nimaike/virustotal_enrich_connector
*8:https://docs.oasis-open.org/cti/stix/v2.1/csprd01/stix-v2.1-csprd01.html#_Toc16070714
*9: https://docs.oasis-open.org/cti/stix/v2.1/csprd01/stix-v2.1-csprd01.html#_Toc16070717