Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions external-import/stairwell/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
**/__pycache__
**/*.pyc
**/.pytest_cache
**/.venv
src/tests
src/config.yml
36 changes: 36 additions & 0 deletions external-import/stairwell/.env.test.sample
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Source this file before running the connector locally:
# cp .env.test.sample .env.test
# # edit values
# set -a; source .env.test; set +a
# python3 main.py
#
# Or with explicit exports (if not using set -a):
# source .env.test

# --- OpenCTI ---
export OPENCTI_URL="http://localhost:8080"
export OPENCTI_TOKEN="REPLACE_WITH_ADMIN_TOKEN_FROM_OPENCTI_DOTENV"

# --- Connector ---
export CONNECTOR_ID="REPLACE_WITH_UUIDV4" # generate with: uuidgen | tr A-Z a-z
export CONNECTOR_TYPE="EXTERNAL_IMPORT"
export CONNECTOR_NAME="Stairwell Import (manual test)"
export CONNECTOR_SCOPE="stairwell"
export CONNECTOR_LOG_LEVEL="info"
export CONNECTOR_DURATION_PERIOD="PT1H" # ISO-8601 schedule interval (long, so the loop won't fire twice during a manual test)

# --- Stairwell ---
export STAIRWELL_API_TOKEN="REPLACE_WITH_STAIRWELL_API_TOKEN"
export STAIRWELL_API_BASE_URL="https://app.stairwell.com"
export STAIRWELL_ORGANIZATION_ID="" # optional, for rate-limit boost
export STAIRWELL_USER_ID="" # optional, for rate-limit boost

# --- Import behavior (small values for fast smoke testing) ---
export STAIRWELL_IMPORT_TLP="green"
export STAIRWELL_IMPORT_FIRST_RUN_WINDOW="P7D" # backfill window on first run
export STAIRWELL_IMPORT_MAX_INDICATORS=5 # cap for manual testing; raise for real use
export STAIRWELL_IMPORT_PAGE_SIZE=20
export STAIRWELL_IMPORT_INDICATOR_VALIDITY_DAYS=90
export STAIRWELL_IMPORT_MIN_BUCKET="HIGH" # LOW | MEDIUM | HIGH | MALICIOUS
export STAIRWELL_IMPORT_SCOPE="environment" # environment (your tenant) | global
export STAIRWELL_IMPORT_WRAPPER="grouping" # grouping | report
19 changes: 19 additions & 0 deletions external-import/stairwell/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
FROM python:3.12-alpine
ENV CONNECTOR_TYPE=EXTERNAL_IMPORT

# Copy the connector
COPY src /opt/opencti-connector-stairwell-import

# Install Python modules
# hadolint ignore=DL3003
RUN apk update && apk upgrade && \
apk --no-cache add git build-base libmagic libffi-dev libxml2-dev libxslt-dev

RUN cd /opt/opencti-connector-stairwell-import && \
pip3 install --no-cache-dir -r requirements.txt && \
apk del git build-base

# Expose and entrypoint
COPY entrypoint.sh /
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
92 changes: 92 additions & 0 deletions external-import/stairwell/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# OpenCTI Stairwell Connector (External Import)

External-import connector that periodically pulls high-confidence Stairwell
indicators (hashes, domains, IPs, URLs) and ingests them into OpenCTI as
Indicators with related observables, wrapped in a Grouping or Report.

## Requirements

- OpenCTI Platform >= 6.8.12
- A Stairwell API token (Bearer auth)

## Configuration

Configuration is loaded via the `connectors-sdk` settings layer: from
`src/config.yml` if present, otherwise from environment variables.
Environment variables override the file.

### OpenCTI

| Parameter | config.yml | Env var | Required | Default | Description |
|---|---|---|---|---|---|
| OpenCTI URL | `opencti.url` | `OPENCTI_URL` | yes | — | OpenCTI platform URL |
| OpenCTI token | `opencti.token` | `OPENCTI_TOKEN` | yes | — | OpenCTI admin token |

### Connector

| Parameter | config.yml | Env var | Required | Default | Description |
|---|---|---|---|---|---|
| Connector ID | `connector.id` | `CONNECTOR_ID` | yes | — | UUIDv4 unique to this instance |
| Connector type | `connector.type` | `CONNECTOR_TYPE` | yes | `EXTERNAL_IMPORT` | Must be `EXTERNAL_IMPORT` |
| Connector name | `connector.name` | `CONNECTOR_NAME` | yes | `Stairwell Import` | Display name in OpenCTI |
| Connector scope | `connector.scope` | `CONNECTOR_SCOPE` | yes | `stairwell` | Free-form import scope tag |
| Log level | `connector.log_level` | `CONNECTOR_LOG_LEVEL` | no | `error` | `debug`, `info`, `warn`, `error` |
| Duration period | `connector.duration_period` | `CONNECTOR_DURATION_PERIOD` | no | `P1D` | ISO 8601 interval between runs (scheduling) |

### Stairwell

| Parameter | config.yml | Env var | Required | Default | Description |
|---|---|---|---|---|---|
| API token | `stairwell.api_token` | `STAIRWELL_API_TOKEN` | yes | — | Stairwell API token |
| API base URL | `stairwell.api_base_url` | `STAIRWELL_API_BASE_URL` | no | `https://app.stairwell.com` | Override for staging |
| Organization ID | `stairwell.organization_id` | `STAIRWELL_ORGANIZATION_ID` | no | — | Adds rate-limit header |
| User ID | `stairwell.user_id` | `STAIRWELL_USER_ID` | no | — | Adds rate-limit header |
| Import TLP | `stairwell.import_tlp` | `STAIRWELL_IMPORT_TLP` | no | `green` | `clear`, `green`, `amber`, `amber+strict`, `red` |
| First-run window | `stairwell.import_first_run_window` | `STAIRWELL_IMPORT_FIRST_RUN_WINDOW` | no | `P1D` | ISO 8601 duration to backfill on first run |
| Max indicators | `stairwell.import_max_indicators` | `STAIRWELL_IMPORT_MAX_INDICATORS` | no | `1000` | Cap per run |
| Page size | `stairwell.import_page_size` | `STAIRWELL_IMPORT_PAGE_SIZE` | no | `100` | API page size |
| Indicator validity | `stairwell.import_indicator_validity_days` | `STAIRWELL_IMPORT_INDICATOR_VALIDITY_DAYS` | no | `90` | `valid_until` offset |
| Min bucket | `stairwell.import_min_bucket` | `STAIRWELL_IMPORT_MIN_BUCKET` | no | `HIGH` | `LOW`, `MEDIUM`, `HIGH`, `MALICIOUS` |
| Import scope | `stairwell.import_scope` | `STAIRWELL_IMPORT_SCOPE` | no | `environment` | `environment` (your tenant) or `global` |
| Wrapper | `stairwell.import_wrapper` | `STAIRWELL_IMPORT_WRAPPER` | no | `grouping` | `grouping` or `report` |

## Behavior

- **Periodic poll.** Scheduling is owned by the OpenCTI connector helper
(`schedule_process`) on the `CONNECTOR_DURATION_PERIOD` interval; each run
opens a work item so it appears in the OpenCTI work log. The first-run
backfill window prevents re-importing the full history.
- **CEL filtering.** Min-bucket and time cutoff are pushed to Stairwell as a
CEL expression so the API only returns relevant rows.
- **Per-run wrapper.** All ingested objects are grouped under a single
Grouping (default) or Report SDO so reviewers can see exactly what landed in
one batch.
- **Indicator validity.** Each Indicator gets `valid_from` = ingest time,
`valid_until` = `valid_from + import_indicator_validity_days`.

## Installation

### Via Docker

```bash
docker build -t opencti/connector-stairwell-import:latest .
```

Then add the service from `docker-compose.yml` to your OpenCTI deployment.

### Local development

```bash
cd src
pip install -r requirements.txt
cp config.yml.sample config.yml # edit values
python3 main.py
```

## Tests

```bash
cd src
pip install -r requirements.txt -r tests/test-requirements.txt
PYTHONPATH=. pytest tests/
```
27 changes: 27 additions & 0 deletions external-import/stairwell/__metadata__/connector_manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"title": "Stairwell Import",
"slug": "stairwell-import",
"description": "The Stairwell external-import connector periodically pulls high-confidence malicious indicators from Stairwell's global file corpus into OpenCTI. On a configurable schedule it queries Stairwell MalEval verdicts for newly-seen malicious files and emits STIX Indicators together with their related observables (file hashes, IP addresses, domain names, and URLs), wrapped in a Grouping or Report so every run is auditable. Filtering by MalEval probability bucket, time window, and environment/global scope is configurable, enabling proactive threat hunting against fresh, vendor-attributed intelligence.",
"short_description": "Periodically imports high-confidence malicious file, IP, domain, and URL indicators from Stairwell's global corpus (MalEval verdicts) into OpenCTI for proactive threat hunting.",
"logo": "external-import/stairwell/__metadata__/logo.png",
"use_cases": [
"Detection & Response Enablement",
"Adversary & Campaign Insights"
],
"solution_categories": [
"Threat Intelligence Feed"
],
"license_type": "Commercial",
"contact": "support@stairwell.com",
"verified": true,
"last_verified_date": null,
"playbook_supported": false,
"max_confidence_level": 80,
"support_version": ">=6.8.12",
"subscription_link": "https://stairwell.com",
"source_code": "https://github.com/OpenCTI-Platform/connectors/tree/master/external-import/stairwell",
"manager_supported": false,
"container_version": "rolling",
"container_image": "opencti/connector-stairwell-import",
"container_type": "EXTERNAL_IMPORT"
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
25 changes: 25 additions & 0 deletions external-import/stairwell/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
services:
connector-stairwell-import:
image: opencti/connector-stairwell-import:rolling
environment:
- OPENCTI_URL=http://opencti:8080
- OPENCTI_TOKEN=${OPENCTI_ADMIN_TOKEN}
- CONNECTOR_ID=${CONNECTOR_STAIRWELL_IMPORT_ID}
- CONNECTOR_TYPE=EXTERNAL_IMPORT
- CONNECTOR_NAME=Stairwell Import
- CONNECTOR_SCOPE=stairwell
- CONNECTOR_LOG_LEVEL=error
- CONNECTOR_DURATION_PERIOD=P1D
- STAIRWELL_API_TOKEN=${STAIRWELL_API_TOKEN}
- STAIRWELL_API_BASE_URL=https://app.stairwell.com
- STAIRWELL_ORGANIZATION_ID=${STAIRWELL_ORGANIZATION_ID:-}
- STAIRWELL_USER_ID=${STAIRWELL_USER_ID:-}
- STAIRWELL_IMPORT_TLP=green
- STAIRWELL_IMPORT_FIRST_RUN_WINDOW=P1D
- STAIRWELL_IMPORT_MAX_INDICATORS=1000
- STAIRWELL_IMPORT_PAGE_SIZE=100
- STAIRWELL_IMPORT_INDICATOR_VALIDITY_DAYS=90
- STAIRWELL_IMPORT_MIN_BUCKET=HIGH
- STAIRWELL_IMPORT_SCOPE=environment
- STAIRWELL_IMPORT_WRAPPER=grouping
restart: always
7 changes: 7 additions & 0 deletions external-import/stairwell/entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#!/bin/sh

# Go to the right directory
cd /opt/opencti-connector-stairwell-import

# Launch the connector
python3 main.py
25 changes: 25 additions & 0 deletions external-import/stairwell/src/config.yml.sample
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
opencti:
url: 'http://localhost:8080'
token: 'ChangeMe'

connector:
type: 'EXTERNAL_IMPORT'
id: 'ChangeMe'
name: 'Stairwell Import' # optional (default: 'Stairwell Import')
scope: 'stairwell'
log_level: 'error' # optional (default: 'error')
duration_period: 'P1D' # optional, ISO-8601 interval between two runs (default: 'P1D')

stairwell:
api_token: 'ChangeMe'
api_base_url: 'https://app.stairwell.com' # optional
organization_id: '' # optional, adds a rate-limit header
user_id: '' # optional, adds a rate-limit header
import_tlp: 'green' # optional, one of: clear, white, green, amber, amber+strict, red (default: 'green')
import_first_run_window: 'P1D' # optional, ISO-8601 backfill window on first run (default: 'P1D')
import_max_indicators: 1000 # optional (default: 1000)
import_page_size: 100 # optional (default: 100)
import_indicator_validity_days: 90 # optional (default: 90)
import_min_bucket: 'HIGH' # optional, one of: LOW, MEDIUM, HIGH, MALICIOUS (default: 'HIGH')
import_scope: 'environment' # optional, 'environment' or 'global' (default: 'environment')
import_wrapper: 'grouping' # optional, 'grouping' or 'report' (default: 'grouping')
Empty file.
68 changes: 68 additions & 0 deletions external-import/stairwell/src/connector/connector.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
from __future__ import annotations

from pycti import OpenCTIConnectorHelper

from connector.import_filter import parse_iso_duration
from connector.import_runner import ImportRunner
from connector.settings import ConnectorSettings
from connector.stairwell import StairwellClient


class StairwellImportConnector:
"""External-import connector: periodically pulls Stairwell MalEval indicators.

Scheduling and work registration are owned by the OpenCTI connector helper:
`run()` registers `process_message` with `schedule_process`, and each run
opens a work item (`initiate_work` / `to_processed`) so the ingestion shows
up in the OpenCTI work log.
"""

def __init__(
self, config: ConnectorSettings, helper: OpenCTIConnectorHelper
) -> None:
self.config = config
self.helper = helper

s = config.stairwell
self.client = StairwellClient(
api_token=s.api_token,
base_url=str(s.api_base_url),
organization_id=s.organization_id or None,
user_id=s.user_id or None,
)
self.runner = ImportRunner(
helper=helper,
client=self.client,
first_run_window=parse_iso_duration(s.import_first_run_window),
max_indicators=s.import_max_indicators,
page_size=s.import_page_size,
min_bucket=s.import_min_bucket,
scope_environment=(s.import_scope.lower() == "environment"),
wrapper=s.import_wrapper,
tlp=s.import_tlp,
indicator_validity_days=s.import_indicator_validity_days,
)

def process_message(self) -> None:
friendly_name = "Stairwell import"
work_id = self.helper.api.work.initiate_work(
self.helper.connect_id, friendly_name
)
try:
message = self.runner.run(work_id=work_id)
except Exception as err: # noqa: BLE001
self.helper.connector_logger.error(
"[CONNECTOR] Stairwell import run failed",
{"error": str(err)},
)
message = f"Run failed: {err}"
self.helper.api.work.to_processed(work_id, message)
self.helper.connector_logger.info(
"[CONNECTOR] Stairwell import run finished", {"message": message}
)

def run(self) -> None:
self.helper.schedule_process(
message_callback=self.process_message,
duration_period=self.config.connector.duration_period.total_seconds(),
)
Loading
Loading