Adding Connectors

Guide to implementing new connectors.

Connector Architecture

Connectors follow a trait-based pattern:

Connector Trait (base)
    │
    ├── ThreatIntelConnector
    ├── SIEMConnector
    ├── EDRConnector
    ├── EmailGatewayConnector
    └── TicketingConnector

Implementing a Connector

1. Create the File

touch crates/tw-connectors/src/threat_intel/my_provider.rs

2. Implement Base Trait

#![allow(unused)]
fn main() {
use crate::traits::{Connector, ConnectorError, ConnectorHealth, ConnectorResult};
use async_trait::async_trait;

pub struct MyProviderConnector {
    client: reqwest::Client,
    api_key: String,
    base_url: String,
}

impl MyProviderConnector {
    pub fn new(api_key: String) -> Result<Self, ConnectorError> {
        let client = reqwest::Client::builder()
            .timeout(std::time::Duration::from_secs(30))
            .build()
            .map_err(|e| ConnectorError::Configuration(e.to_string()))?;

        Ok(Self {
            client,
            api_key,
            base_url: "https://api.myprovider.com".to_string(),
        })
    }
}

#[async_trait]
impl Connector for MyProviderConnector {
    fn name(&self) -> &str {
        "my_provider"
    }

    fn connector_type(&self) -> &str {
        "threat_intel"
    }

    async fn health_check(&self) -> ConnectorResult<ConnectorHealth> {
        let response = self.client
            .get(format!("{}/health", self.base_url))
            .header("Authorization", format!("Bearer {}", self.api_key))
            .send()
            .await
            .map_err(|e| ConnectorError::NetworkError(e.to_string()))?;

        if response.status().is_success() {
            Ok(ConnectorHealth::Healthy)
        } else {
            Ok(ConnectorHealth::Unhealthy {
                message: "Health check failed".to_string(),
            })
        }
    }

    async fn test_connection(&self) -> ConnectorResult<bool> {
        match self.health_check().await? {
            ConnectorHealth::Healthy => Ok(true),
            _ => Ok(false),
        }
    }
}
}

3. Implement Specialized Trait

#![allow(unused)]
fn main() {
use crate::traits::{ThreatIntelConnector, ThreatReport, IndicatorType};

#[async_trait]
impl ThreatIntelConnector for MyProviderConnector {
    async fn lookup_hash(&self, hash: &str) -> ConnectorResult<ThreatReport> {
        let response = self.client
            .get(format!("{}/files/{}", self.base_url, hash))
            .header("Authorization", format!("Bearer {}", self.api_key))
            .send()
            .await
            .map_err(|e| ConnectorError::NetworkError(e.to_string()))?;

        if response.status() == reqwest::StatusCode::NOT_FOUND {
            return Ok(ThreatReport {
                indicator: hash.to_string(),
                indicator_type: IndicatorType::FileHash,
                malicious: false,
                confidence: 0.0,
                categories: vec![],
                first_seen: None,
                last_seen: None,
                sources: vec![],
            });
        }

        let data: ApiResponse = response.json().await
            .map_err(|e| ConnectorError::InvalidResponse(e.to_string()))?;

        Ok(self.convert_response(data))
    }

    async fn lookup_url(&self, url: &str) -> ConnectorResult<ThreatReport> {
        // Similar implementation
        todo!()
    }

    async fn lookup_domain(&self, domain: &str) -> ConnectorResult<ThreatReport> {
        // Similar implementation
        todo!()
    }

    async fn lookup_ip(&self, ip: &str) -> ConnectorResult<ThreatReport> {
        // Similar implementation
        todo!()
    }
}
}

4. Add to Module

#![allow(unused)]
fn main() {
// crates/tw-connectors/src/threat_intel/mod.rs
mod my_provider;
pub use my_provider::MyProviderConnector;
}

5. Register in Bridge

#![allow(unused)]
fn main() {
// tw-bridge/src/lib.rs
impl ThreatIntelBridge {
    pub fn new(mode: &str) -> PyResult<Self> {
        let connector: Arc<dyn ThreatIntelConnector + Send + Sync> = match mode {
            "virustotal" => Arc::new(VirusTotalConnector::new(
                std::env::var("TW_VIRUSTOTAL_API_KEY")
                    .map_err(|_| PyErr::new::<pyo3::exceptions::PyValueError, _>(
                        "TW_VIRUSTOTAL_API_KEY not set"
                    ))?
            )?),
            "my_provider" => Arc::new(MyProviderConnector::new(
                std::env::var("TW_MY_PROVIDER_API_KEY")
                    .map_err(|_| PyErr::new::<pyo3::exceptions::PyValueError, _>(
                        "TW_MY_PROVIDER_API_KEY not set"
                    ))?
            )?),
            _ => Arc::new(MockThreatIntelConnector::new("mock")),
        };

        Ok(Self { connector })
    }
}
}

Error Handling

Use appropriate error types:

#![allow(unused)]
fn main() {
pub enum ConnectorError {
    /// Configuration issue
    Configuration(String),

    /// Network/connection error
    NetworkError(String),

    /// Authentication failed
    AuthenticationFailed(String),

    /// Resource not found
    NotFound(String),

    /// Rate limited
    RateLimited { retry_after: Option<Duration> },

    /// Invalid response from service
    InvalidResponse(String),

    /// Request failed
    RequestFailed(String),
}
}

Rate Limiting

Implement rate limiting in your connector:

#![allow(unused)]
fn main() {
use governor::{Quota, RateLimiter};

pub struct MyProviderConnector {
    client: reqwest::Client,
    api_key: String,
    rate_limiter: RateLimiter<...>,
}

impl MyProviderConnector {
    async fn make_request(&self, url: &str) -> ConnectorResult<Response> {
        self.rate_limiter.until_ready().await;

        self.client.get(url)
            .header("Authorization", format!("Bearer {}", self.api_key))
            .send()
            .await
            .map_err(|e| ConnectorError::NetworkError(e.to_string()))
    }
}
}

Testing

Unit Tests

#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
    use super::*;
    use wiremock::{MockServer, Mock, ResponseTemplate};
    use wiremock::matchers::{method, path};

    #[tokio::test]
    async fn test_lookup_hash() {
        let mock_server = MockServer::start().await;

        Mock::given(method("GET"))
            .and(path("/files/abc123"))
            .respond_with(ResponseTemplate::new(200).set_body_json(json!({
                "malicious": true,
                "confidence": 0.95
            })))
            .mount(&mock_server)
            .await;

        let connector = MyProviderConnector::with_base_url(
            "test-key".to_string(),
            mock_server.uri(),
        );

        let result = connector.lookup_hash("abc123").await.unwrap();
        assert!(result.malicious);
    }
}
}

Documentation

Document your connector:

#![allow(unused)]
fn main() {
//! MyProvider threat intelligence connector.
//!
//! # Configuration
//!
//! Set `TW_MY_PROVIDER_API_KEY` environment variable.
//!
//! # Example
//!
//! ```rust
//! let connector = MyProviderConnector::new(api_key)?;
//! let report = connector.lookup_hash("abc123").await?;
//! ```
}