Skip to content

feat(email-cases-importer): add connector to import emails as incident-response cases (#7036)#7037

Open
Khidr6G wants to merge 1 commit into
OpenCTI-Platform:masterfrom
Khidr6G:feature/email-cases-importer
Open

feat(email-cases-importer): add connector to import emails as incident-response cases (#7036)#7037
Khidr6G wants to merge 1 commit into
OpenCTI-Platform:masterfrom
Khidr6G:feature/email-cases-importer

Conversation

@Khidr6G

@Khidr6G Khidr6G commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Proposed changes

  • Add a new EXTERNAL_IMPORT connector: Email Cases Importer (external-import/email-cases-importer). It polls a mailbox and creates Incident Response Cases (Case-Incident) from matching emails, writing the email content to the case Content tab and uploading attachments.
  • Multi-protocol mailbox support: IMAP, Microsoft Graph (Office 365), Gmail API, and EWS.
  • Subject filtering (exact / contains / regex) and per-sender rules (author, TLP marking, assignees, participants); thread tracking via provider thread ID, message headers, or normalized subject matching.
  • Password extraction from email bodies and decryption of encrypted ZIP / 7z / XLSX / PDF attachments (extracts unencrypted RAR), including nested archives up to 3 levels.
  • Auto-creation of missing labels, vocabulary values (severity / priority / response type), and author identities.
  • Deterministic STIX IDs generated exclusively via pycti.<Class>.generate_id(...); startup connectivity/credential validation with exit(1) on fatal failure; structured logging via helper.connector_logger; Retry-After-aware 429 backoff and typed errors on non-JSON 200 responses; a fully-failed import cycle marks the OpenCTI Work in_error and preserves state (no silent "success" with zero imports).
  • 260 unit tests covering all four protocol clients, the converter, attachment handlers, settings, and orchestration. Ships config.yml.sample and .env.sample; no hardcoded secrets.

Related issues

Checklist

  • I consider the submitted work as finished
  • I have signed my commits using GPG key.
  • I tested the code for its functionality using different use cases
  • I added/update the relevant documentation (either on github or on notion)
  • Where necessary I refactored code to improve the overall quality

Further comments

  • Tested platform version: OpenCTI >= 7.260715.0 (matches __metadata__/connector_manifest.json).
  • Design notes: email content is rendered to the case Content tab as HTML rather than emitted as separate Note/Email-Message STIX objects; deduplication is by case name + processed-message-id state. Both original (encrypted) and extracted (decrypted) attachments are uploaded to the case.
  • Persistence model — the OpenCTI API, not STIX bundles: the connector writes through the live pycti API (case_incident.create, stix_domain_object.add_file, update_field) rather than emitting STIX 2.1 bundles via send_stix2_bundle. This is a deliberate, required choice: attachment files live on the case Files tab (binary attachments have no STIX representation), email content is appended to the case Content tab on each reply (bundles upsert, not append), and case templates / assignees / participants reference OpenCTI-internal objects with no STIX equivalent. send_stix2_bundle is also asynchronous and cannot return the case's internal ID needed for the immediately-following file and content writes. Idempotency is preserved via deterministic pycti.<Class>.generate_id(...). See the README Behavior section for the full rationale.
  • bsdtar (libarchive-tools) is used for RAR extraction and cannot decrypt password-protected RAR archives — those are uploaded as-is.

Copilot AI review requested due to automatic review settings July 17, 2026 14:57
@Filigran-Automation Filigran-Automation added the community Contribution from the community. label Jul 17, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a new EXTERNAL_IMPORT connector (external-import/email-cases-importer) that polls email inboxes across multiple providers/protocols and creates/updates Incident Response Cases (Case-Incident) in OpenCTI, including attachment extraction/decryption and thread-based grouping.

Changes:

  • Adds connector implementation + protocol clients (IMAP, Microsoft Graph, Gmail, EWS) and shared HTTP robustness helpers.
  • Adds attachment handling pipeline (archives/documents/passthrough) with nested recursion and hashing.
  • Adds extensive unit tests, containerization (Dockerfile/compose), and end-user documentation/config samples.

Reviewed changes

Copilot reviewed 48 out of 50 changed files in this pull request and generated 11 comments.

Show a summary per file
File Description
external-import/email-cases-importer/VERSION Connector version pin.
external-import/email-cases-importer/tests/init.py Marks test package.
external-import/email-cases-importer/tests/conftest.py Test path setup and dependency stubs.
external-import/email-cases-importer/tests/test_attachment_handlers.py Tests handler routing + passthrough behavior.
external-import/email-cases-importer/tests/test_connector.py End-to-end orchestration tests with mocked OpenCTI helper.
external-import/email-cases-importer/tests/test_converter_to_stix.py Tests identity and HTML/content formatting.
external-import/email-cases-importer/tests/test_email_client_factory.py Tests protocol routing and unsupported protocols.
external-import/email-cases-importer/tests/test_ews_client.py Tests EWS parsing + lifecycle (with exchangelib).
external-import/email-cases-importer/tests/test_ews_filter.py Regression tests for correct exchangelib filter construction.
external-import/email-cases-importer/tests/test_gmail_client.py Tests Gmail API client behavior.
external-import/email-cases-importer/tests/test_graph_client.py Tests Microsoft Graph client behavior.
external-import/email-cases-importer/tests/test_handlers_extra.py Tests archive/document handlers + nested recursion edges.
external-import/email-cases-importer/tests/test_http_helpers.py Tests shared HTTP retry + JSON parsing robustness.
external-import/email-cases-importer/tests/test_imap_client.py Tests IMAP client parsing and connection behavior.
external-import/email-cases-importer/tests/test_main.py Tests entrypoint main execution paths.
external-import/email-cases-importer/tests/test_settings.py Tests Pydantic validators/parsers for settings.
external-import/email-cases-importer/tests/test_utils.py Tests string/html/hash utilities.
external-import/email-cases-importer/test-requirements.txt Test dependencies for isolated connector test runs.
external-import/email-cases-importer/src/requirements.txt Runtime dependencies for connector image.
external-import/email-cases-importer/src/main.py Runtime entrypoint wiring settings → helper → connector.
external-import/email-cases-importer/src/email_client/init.py Public email_client exports.
external-import/email-cases-importer/src/email_client/_http.py Shared HTTP helpers (429 backoff + non-JSON 200 handling).
external-import/email-cases-importer/src/email_client/base.py Email message/attachment dataclasses + base client interface.
external-import/email-cases-importer/src/email_client/ews_client.py EWS client implementation.
external-import/email-cases-importer/src/email_client/factory.py Protocol-based email client factory.
external-import/email-cases-importer/src/email_client/gmail_client.py Gmail API client implementation.
external-import/email-cases-importer/src/email_client/graph_client.py Microsoft Graph API client implementation.
external-import/email-cases-importer/src/email_client/imap_client.py IMAP client implementation.
external-import/email-cases-importer/src/connector/init.py Connector package exports.
external-import/email-cases-importer/src/connector/connector.py Main connector orchestration + OpenCTI write paths.
external-import/email-cases-importer/src/connector/converter_to_stix.py Identity + content/description formatting helpers.
external-import/email-cases-importer/src/connector/settings.py Pydantic settings model and validators.
external-import/email-cases-importer/src/connector/utils.py Utility helpers (subject normalization, HTML sanitization, hashing).
external-import/email-cases-importer/src/attachment_handler/init.py Public attachment handler exports.
external-import/email-cases-importer/src/attachment_handler/archive_handler.py ZIP/7z/RAR extraction logic.
external-import/email-cases-importer/src/attachment_handler/base.py Attachment handler interface + ExtractedFile model.
external-import/email-cases-importer/src/attachment_handler/document_handler.py XLSX/PDF handling + optional decryption.
external-import/email-cases-importer/src/attachment_handler/passthrough_handler.py Passthrough handler for non-decrypted files.
external-import/email-cases-importer/src/attachment_handler/registry.py Handler registry + recursion for nested archives.
external-import/email-cases-importer/scripts/test_mailbox_connection.py Standalone mailbox connectivity tester script.
external-import/email-cases-importer/README.md Connector documentation (features/config/behavior).
external-import/email-cases-importer/pyproject.toml Formatter/linter/test config for this connector.
external-import/email-cases-importer/entrypoint.sh Container entrypoint script.
external-import/email-cases-importer/Dockerfile Alpine-based connector image build.
external-import/email-cases-importer/docker-compose.yml Local deployment example.
external-import/email-cases-importer/config.yml.sample Fully commented config sample.
external-import/email-cases-importer/.env.sample Environment variable sample for compose.
external-import/email-cases-importer/.dockerignore Image build context hygiene.
external-import/email-cases-importer/metadata/connector_manifest.json Connector manifest metadata (image/version/support version).

content=content,
severity=severity or self.config.email_cases.default_severity,
priority=priority or self.config.email_cases.default_priority,
createdBy=author_id or self.converter.identity_id,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed in the amended commit

Comment on lines +23 to +27
def __init__(self, config, helper: OpenCTIConnectorHelper):
self.config = config
self.helper = helper
self.converter = ConverterToStix(helper, config)
self.handler_registry = HandlerRegistry()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed in the amended commit

Comment on lines +54 to +65
try:
zf.testzip()
if pwd:
zf.setpassword(pwd)
# Test extraction of first file
if zf.namelist():
zf.read(zf.namelist()[0], pwd=pwd)
used_pwd = pwd
break
except (RuntimeError, zipfile.BadZipFile):
continue

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed in the amended commit

Comment on lines +68 to +70
if since:
iso_since = since.strftime("%Y-%m-%dT%H:%M:%SZ")
filter_parts.append(f"receivedDateTime ge {iso_since}")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed in the amended commit

Comment on lines +163 to +166
if body_data and mime_type == "text/plain":
body_plain = base64.urlsafe_b64decode(body_data).decode(
"utf-8", errors="replace"
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed in the amended commit

Comment on lines +24 to +36
def connect(self) -> None:
from exchangelib import (
DELEGATE,
Account,
Configuration,
Credentials,
)
from exchangelib.protocol import BaseProtocol, NoVerifyHTTPAdapter

if not self._tls_verify:
BaseProtocol.HTTP_ADAPTER_CLS = NoVerifyHTTPAdapter

credentials = Credentials(self._username, self._password)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed in the amended commit

Comment on lines +176 to +185
def build_graph_client(args):
try:
from email_client.graph_client import GraphClient
except ImportError as exc:
print(
f"ERROR: Microsoft Graph client requires 'msal'. Install: "
f"pip install msal ({exc})",
file=sys.stderr,
)
sys.exit(1)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed in the amended commit

|--------|-----------|-----------------|---------|
| ZIP archive | `.zip` | Yes | `zipfile` (stdlib) |
| 7-Zip archive | `.7z` | Yes | `py7zr` |
| RAR archive | `.rar` | Yes | `rarfile` + `unrar` |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed in the amended commit

| CSV | `.csv` | No | `csv` (stdlib) |
| Text | `.txt` | No | stdlib |
| Email | `.eml` | No | `email` (stdlib) |
| Outlook message | `.msg` | No | `extract-msg` |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed in the amended commit

Comment on lines +122 to +125
date_tuple = email.utils.parsedate_to_datetime(msg.get("Date", ""))
if date_tuple.tzinfo is None:
date_tuple = date_tuple.replace(tzinfo=timezone.utc)
in_reply_to = msg.get("In-Reply-To", "")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed in the amended commit

@Khidr6G
Khidr6G force-pushed the feature/email-cases-importer branch from aab4060 to d58346e Compare July 17, 2026 15:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

community Contribution from the community.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(email-cases-importer): add connector to import emails as incident-response cases

5 participants