synthient 1.0.0

The official Rust client for the Synthient API: IP and domain intelligence, account details, and real-time and bulk data feeds.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
use std::collections::BTreeMap;

use serde::{Deserialize, Serialize};

use crate::timestamp::Timestamp;

wire_enum! {
    /// The kind of service seen on an address.
    pub enum ProxyType {
        /// A free VPN.
        FreeVpn = "FREE_VPN",
        /// A commercial VPN.
        CommercialVpn = "COMMERCIAL_VPN",
        /// An enterprise VPN.
        EnterpriseVpn = "ENTERPRISE_VPN",
        /// A mobile proxy.
        MobileProxy = "MOBILE_PROXY",
        /// A blockchain-based proxy network.
        BlockchainProxy = "BLOCKCHAIN_PROXY",
        /// A residential proxy.
        ResidentialProxy = "RESIDENTIAL_PROXY",
        /// A public proxy.
        PublicProxy = "PUBLIC_PROXY",
        /// A datacenter proxy.
        DatacenterProxy = "DATACENTER_PROXY",
        /// A Tor node.
        TorNode = "TOR_NODE",
        /// A private relay, such as iCloud Private Relay.
        PrivateRelay = "PRIVATE_RELAY",
        /// A botnet.
        Botnet = "BOTNET",
        /// A search engine crawler.
        SearchEngine = "SEARCH_ENGINE",
        /// An AI crawler.
        AiCrawler = "AI_CRAWLER",
        /// A social media crawler.
        SocialMedia = "SOCIAL_MEDIA",
        /// An uptime monitor.
        UptimeMonitor = "UPTIME_MONITOR",
        /// A link preview fetcher.
        LinkPreview = "LINK_PREVIEW",
        /// An SEO crawler.
        SeoCrawler = "SEO_CRAWLER",
        /// A web archiver.
        WebArchiver = "WEB_ARCHIVER",
        /// A webhook provider.
        WebhookProvider = "WEBHOOK_PROVIDER",
        /// A payment processor.
        PaymentProcessor = "PAYMENT_PROCESSOR",
        /// An internet scanner.
        InternetScanner = "INTERNET_SCANNER",
    }
}

/// A proxy sighting from the proxies feed. The API may omit any field.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct ProxyEvent {
    /// The proxy exit address.
    pub ip: Option<String>,
    /// The proxy service tag.
    pub provider: Option<String>,
    /// The proxy category.
    pub r#type: Option<ProxyType>,
    /// The ISO 3166-1 alpha-2 country of the address.
    pub country_code: Option<String>,
    /// The autonomous system number of the address.
    pub asn: Option<u32>,
    /// When the proxy was seen, in Unix seconds.
    pub timestamp: Option<Timestamp>,
}

/// A VPN or anonymizer IP range from the anonymizers feed. The API may omit
/// any field.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct AnonymizerEvent {
    /// The first address of the range.
    pub range_start: Option<String>,
    /// The last address of the range.
    pub range_end: Option<String>,
    /// The service tag.
    pub provider: Option<String>,
    /// The service category, such as [`ProxyType::CommercialVpn`].
    pub r#type: Option<ProxyType>,
    /// The ISO 3166-1 alpha-2 country of `range_start`.
    pub country_code: Option<String>,
    /// The autonomous system number of `range_start`.
    pub asn: Option<u32>,
    /// When the range was seen, in Unix milliseconds.
    pub timestamp: Option<Timestamp>,
}

/// A torrent observed on the BitTorrent DHT or a tracker. The API may omit
/// any field.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct TorrentEvent {
    /// The hex-encoded SHA-1 info hash.
    pub info_hash: Option<String>,
    /// The torrent name.
    pub name: Option<String>,
    /// The torrent's magnet link.
    pub magnet_uri: Option<String>,
    /// The total size of the torrent's files in bytes.
    pub total_size: Option<i64>,
    /// The piece size in bytes.
    pub piece_length: Option<i64>,
    /// The number of files in the torrent.
    pub file_count: Option<i64>,
    /// The torrent's files.
    pub files: Option<Vec<TorrentFile>>,
    /// The peers seen sharing the torrent.
    pub peers: Option<Vec<TorrentPeer>>,
    /// When the torrent was seen, in Unix seconds.
    pub timestamp: Option<Timestamp>,
}

/// A file in a torrent.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct TorrentFile {
    /// The file path within the torrent.
    pub path: Option<String>,
    /// The file size in bytes.
    pub length: Option<i64>,
}

/// A peer seen sharing a torrent.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct TorrentPeer {
    /// The peer's address.
    pub ip: Option<String>,
    /// The peer's port.
    pub port: Option<u16>,
    /// Where the peer was found, such as `DHT`, `PEX`, or `tracker`.
    pub source: Option<String>,
    /// Whether the peer supports encrypted connections.
    pub encrypted: Option<bool>,
}

/// A JA4T TCP fingerprint sighting. The API may omit any field.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Ja4tEvent {
    /// The address the fingerprint was seen from.
    pub ip: Option<String>,
    /// The JA4T fingerprint.
    pub ja4t: Option<String>,
    /// The service tag of the address.
    pub provider: Option<String>,
    /// The service category, such as [`ProxyType::DatacenterProxy`].
    pub r#type: Option<ProxyType>,
    /// The ISO 3166-1 alpha-2 country of the address.
    pub country_code: Option<String>,
    /// The autonomous system number of the address.
    pub asn: Option<u32>,
    /// When the fingerprint was seen, in Unix seconds.
    pub timestamp: Option<Timestamp>,
}

/// The Helios honeypot sensor that captured an event. The API may omit any
/// field.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct HeliosMeta {
    /// The proxy exit address the traffic arrived through.
    pub proxy_ip: Option<String>,
    /// The sensor that captured the traffic.
    pub server: Option<String>,
    /// The sensor pool.
    pub pool_id: Option<String>,
    /// The proxy service the traffic arrived through.
    pub provider: Option<String>,
}

/// An HTTP request captured by a Helios honeypot.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct HeliosHttpEvent {
    /// When the request was captured, in Unix milliseconds.
    pub timestamp: Timestamp,
    /// The proxy tunnel the request arrived through. Live values use the
    /// full `u64` range.
    pub tunnel_id: u64,
    /// The host the tunnel was opened to.
    pub domain: String,
    /// The destination port.
    pub port: u16,
    /// Always `http`.
    pub protocol: String,
    /// The capturing sensor.
    pub meta: HeliosMeta,
    /// The parsed request.
    pub details: Option<HttpRequestDetails>,
    /// The raw request bytes, which the API sends as base64.
    #[serde(default, with = "base64_bytes")]
    pub raw: Option<Vec<u8>>,
}

/// A parsed HTTP request. The API may omit any field.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct HttpRequestDetails {
    /// The request method.
    pub method: Option<String>,
    /// The request target.
    pub uri: Option<String>,
    /// The HTTP version, such as `HTTP/1.1`.
    pub version: Option<String>,
    /// The request headers.
    pub headers: Option<BTreeMap<String, String>>,
}

/// A TLS ClientHello captured by a Helios honeypot.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct HeliosHttpsEvent {
    /// When the handshake was captured, in Unix milliseconds.
    pub timestamp: Timestamp,
    /// The proxy tunnel the handshake arrived through. Live values use the
    /// full `u64` range.
    pub tunnel_id: u64,
    /// The host the tunnel was opened to.
    pub domain: String,
    /// The destination port.
    pub port: u16,
    /// Always `https`.
    pub protocol: String,
    /// The capturing sensor.
    pub meta: HeliosMeta,
    /// The parsed ClientHello, or `None` if the sensor could not parse the
    /// handshake.
    pub details: Option<TlsClientHello>,
    /// Always `None`, because the sensors do not capture handshake bytes.
    pub raw: Option<String>,
}

/// A TLS ClientHello parsed by a Helios sensor. Fields the sensor could not
/// parse are `None`.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct TlsClientHello {
    /// The record layer version, such as `TLS 1.0`.
    pub record_version: Option<String>,
    /// The ClientHello version, such as `TLS 1.2`.
    pub handshake_version: Option<String>,
    /// The hex-encoded 32-byte client random.
    pub client_random: Option<String>,
    /// The hex-encoded session ID.
    pub session_id: Option<String>,
    /// The length of the session ID in bytes.
    pub session_id_length: Option<u8>,
    /// The offered cipher suites.
    pub cipher_suites: Option<Vec<TlsCodeName>>,
    /// The offered compression methods.
    pub compression_methods: Option<Vec<u8>>,
    /// The extensions in wire order.
    pub extensions: Option<Vec<TlsExtension>>,
    /// The `server_name` extension value.
    pub sni: Option<String>,
    /// The offered application protocols.
    pub alpn: Option<Vec<String>>,
    /// The offered TLS versions.
    pub supported_versions: Option<Vec<String>>,
    /// The offered key exchange groups.
    pub supported_groups: Option<Vec<TlsCodeName>>,
    /// The offered signature algorithms.
    pub signature_algorithms: Option<Vec<TlsCodeName>>,
    /// The offered elliptic curve point formats.
    pub ec_point_formats: Option<Vec<String>>,
    /// The groups the client sent key shares for.
    pub key_share_groups: Option<Vec<String>>,
    /// The offered pre-shared key exchange modes.
    pub psk_key_exchange_modes: Option<Vec<String>>,
    /// The offered certificate compression algorithms.
    pub compress_certificate: Option<Vec<String>>,
    /// Whether the ClientHello contains GREASE values.
    pub has_grease: Option<bool>,
    /// Whether the client offered application settings (ALPS).
    pub application_settings: Option<bool>,
    /// Whether the client offered delegated credentials.
    pub delegated_credentials: Option<bool>,
    /// Whether the client offered encrypt-then-MAC.
    pub encrypt_then_mac: Option<bool>,
    /// Whether the client offered the extended master secret.
    pub extended_master_secret: Option<bool>,
    /// Whether the client offered post-handshake authentication.
    pub post_handshake_auth: Option<bool>,
    /// Whether the client sent renegotiation info.
    pub renegotiation_info: Option<bool>,
    /// Whether the client requested signed certificate timestamps.
    pub signed_certificate_timestamps: Option<bool>,
    /// Whether the client requested OCSP stapling.
    pub status_request: Option<bool>,
}

/// A TLS registry code point and its name. GREASE values are named
/// `GREASE(0x....)`.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct TlsCodeName {
    /// The code point.
    pub code: Option<u16>,
    /// The code point's registered name.
    pub name: Option<String>,
}

/// An extension in a TLS ClientHello.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct TlsExtension {
    /// The extension type.
    pub code: Option<u16>,
    /// The extension's registered name.
    pub name: Option<String>,
    /// The extension body length in bytes.
    pub length: Option<u16>,
}

/// A destination that a Helios honeypot tunnel was asked to reach. Names
/// resolved inside a tunneled DNS session are not visible.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct HeliosDnsEvent {
    /// When the event was captured, in Unix milliseconds.
    pub timestamp: Timestamp,
    /// The proxy tunnel. Live values use the full `u64` range.
    pub tunnel_id: u64,
    /// The host name or address literal the tunnel was opened to.
    pub domain: String,
    /// The destination port.
    pub port: u16,
    /// Always `dns`.
    pub protocol: String,
    /// The capturing sensor.
    pub meta: HeliosMeta,
}

/// An Android Debug Bridge shell command captured by a Helios honeypot. It
/// has no timestamp because the sensor sends none.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct HeliosAdbEvent {
    /// The ADB session as 32 hex characters.
    pub session: String,
    /// The 1-based position of the command within its session.
    pub sequential_id: u64,
    /// The shell command. Invalid UTF-8 bytes are replaced with U+FFFD.
    pub command: String,
    /// The hex-encoded SHA-256 the sensor assigns to the session's payload.
    /// It is not a hash of `command`: every command in a session shares it,
    /// and sessions that replay the same script share it too.
    pub hash: String,
}

mod base64_bytes {
    use base64::Engine;
    use base64::engine::general_purpose::STANDARD;
    use serde::{Deserialize, Deserializer, Serializer};

    pub(super) fn serialize<S: Serializer>(
        bytes: &Option<Vec<u8>>,
        serializer: S,
    ) -> Result<S::Ok, S::Error> {
        match bytes {
            Some(bytes) => serializer.serialize_some(&STANDARD.encode(bytes)),
            None => serializer.serialize_none(),
        }
    }

    pub(super) fn deserialize<'de, D: Deserializer<'de>>(
        deserializer: D,
    ) -> Result<Option<Vec<u8>>, D::Error> {
        let Some(text) = Option::<String>::deserialize(deserializer)? else {
            return Ok(None);
        };
        STANDARD
            .decode(text)
            .map(Some)
            .map_err(serde::de::Error::custom)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn decodes_tunnel_ids_above_i64_max() {
        let line = r#"{"timestamp":1778200137487,"tunnel_id":17257744507466107949,"domain":"example.com","port":443,"protocol":"dns","meta":{}}"#;

        let event: HeliosDnsEvent = serde_json::from_str(line).unwrap();

        assert_eq!(event.tunnel_id, 17_257_744_507_466_107_949);
        assert!(
            serde_json::to_string(&event)
                .unwrap()
                .contains("17257744507466107949")
        );
    }

    #[test]
    fn decodes_base64_raw_requests() {
        let line = r#"{"timestamp":1,"tunnel_id":1,"domain":"d","port":80,"protocol":"http","meta":{},"raw":"R0VUIC8gSFRUUC8xLjENCg=="}"#;
        let event: HeliosHttpEvent = serde_json::from_str(line).unwrap();
        assert_eq!(event.raw.as_deref(), Some(&b"GET / HTTP/1.1\r\n"[..]));

        let missing =
            r#"{"timestamp":1,"tunnel_id":1,"domain":"d","port":80,"protocol":"http","meta":{}}"#;
        let event: HeliosHttpEvent = serde_json::from_str(missing).unwrap();
        assert_eq!(event.raw, None);

        let invalid = r#"{"timestamp":1,"tunnel_id":1,"domain":"d","port":80,"protocol":"http","meta":{},"raw":"not base64!"}"#;
        assert!(serde_json::from_str::<HeliosHttpEvent>(invalid).is_err());
    }

    #[test]
    fn unknown_enum_values_are_kept() {
        let event: ProxyEvent = serde_json::from_str(r#"{"type":"NEW_KIND"}"#).unwrap();

        assert_eq!(event.r#type, Some(ProxyType::Unknown("NEW_KIND".into())));
        assert_eq!(
            serde_json::to_string(&event.r#type).unwrap(),
            r#""NEW_KIND""#
        );
    }

    #[test]
    fn enums_display_with_padding() {
        assert_eq!(format!("[{:<10}]", ProxyType::FreeVpn), "[FREE_VPN  ]");
        assert_eq!(
            format!("[{:>5}]", ProxyType::Unknown("NEW".into())),
            "[  NEW]"
        );
    }

    #[test]
    fn wrong_types_are_rejected() {
        assert!(serde_json::from_str::<ProxyEvent>(r#"{"asn":"13335"}"#).is_err());
        assert!(serde_json::from_str::<ProxyEvent>(r#"{"asn":4294967296}"#).is_err());
        assert!(serde_json::from_str::<ProxyEvent>(r#"{"type":7}"#).is_err());
    }
}