use crate::util::unwrap_try_into;
use chrono::prelude::*;
use derive_builder::Builder;
use failure::Fail;
use reqwest::header::HeaderValue;
use serde::Deserialize;
use std::convert::TryInto;
use crate::datetime::days_ago;
use crate::{GenericResult, SampleHash};
pub struct AlienVaultOTXClient {
apikey: String,
}
impl Default for AlienVaultOTXClient {
fn default() -> Self {
AlienVaultOTXClient {
apikey: std::env::var("OTX_APIKEY")
.expect("please set AlienVault OTX API key to environment var $OTX_APIKEY"),
}
}
}
#[derive(Debug, Fail)]
pub enum AlienVaultOTXError {
#[fail(display = "invalid setting")]
InvalidSetting(String),
#[fail(display = "request failed")]
RequestFailed,
}
#[derive(Debug)]
pub enum QueryType {
General,
Analysis,
}
impl std::fmt::Display for QueryType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
match self {
QueryType::General => write!(f, "general"),
QueryType::Analysis => write!(f, "analysis"),
}
}
}
impl AlienVaultOTXClient {
pub fn new(apikey: String) -> Self {
AlienVaultOTXClient { apikey }
}
pub fn pulses_from(&self, datetime: impl Into<DateTime<Utc>>) -> GenericResult<Vec<Pulse>> {
Ok(PulsesBuilder::default()
.api_key(self.apikey.clone())
.modified_since(datetime.into())
.build()
.map_err(AlienVaultOTXError::InvalidSetting)?
.get_all())
}
pub fn pulses_for(&self, days: i64) -> GenericResult<Vec<Pulse>> {
self.pulses_from(days_ago(days))
}
fn indicator_url(&self, hash: impl AsRef<str>, section: QueryType) -> String {
format!(
"https://otx.alienvault.com/api/v1/indicators/file/{}/{}",
hash.as_ref(),
section
)
}
fn make_get_request(&self, url: impl AsRef<str>) -> reqwest::RequestBuilder {
reqwest::Client::new()
.get(url.as_ref())
.header("X-OTX-API-KEY", self.apikey.as_str())
}
pub fn get_raw_json(
&self,
hash: impl TryInto<SampleHash>,
section: QueryType,
) -> GenericResult<String> {
let hash = unwrap_try_into(hash)?;
let mut res = self
.make_get_request(self.indicator_url(hash, section))
.send()?;
if !res.status().is_success() {
return Err(AlienVaultOTXError::RequestFailed.into());
}
Ok(res.text()?)
}
pub fn query<T>(&self, hash: impl TryInto<SampleHash>, section: QueryType) -> GenericResult<T>
where
T: serde::de::DeserializeOwned,
{
let hash = unwrap_try_into(hash)?;
let mut res = self
.make_get_request(self.indicator_url(hash, section))
.send()?;
if !res.status().is_success() {
return Err(AlienVaultOTXError::RequestFailed.into());
}
Ok(res.json()?)
}
}
#[derive(Builder, Debug)]
pub struct Pulses {
#[builder(
default = "std::env::var(\"OTX_APIKEY\").map_err(|_err| \"could not get OTX APIKEY\".to_owned())?"
)]
api_key: String,
#[builder(default = "50")]
limit: u32,
#[builder(default = "1")]
page: u32,
#[builder(default = "days_ago(7)")]
modified_since: DateTime<Utc>,
#[builder(default = "false")]
has_done: bool,
}
#[derive(Debug, Deserialize)]
pub enum IndicatorType {
IPv4,
IPv6,
#[serde(rename = "domain")]
Domain,
#[serde(rename = "hostname")]
Hostname,
#[serde(rename = "email")]
Email,
URL,
URI,
#[serde(rename = "FileHash-MD5")]
MD5,
#[serde(rename = "FileHash-SHA1")]
SHA1,
#[serde(rename = "FileHash-SHA256")]
SHA256,
#[serde(rename = "FileHash-PEHASH")]
PEHash,
#[serde(rename = "FileHash-IMPHASH")]
IMPHash,
CIDR,
FilePath,
Mutex,
CVE,
YARA,
#[serde(other)]
Unknown,
}
#[derive(Debug, Deserialize)]
pub struct Indicator {
pub id: i64,
pub indicator: String,
pub description: Option<String>,
#[serde(rename = "type")]
pub _type: IndicatorType,
}
#[derive(Debug, Deserialize)]
pub struct Pulse {
pub id: String,
pub name: String,
pub author_name: String,
pub description: Option<String>,
pub created: Option<String>,
pub modified: String,
pub references: Option<Vec<String>>,
pub tags: Option<Vec<String>>,
pub targeted_countries: Vec<String>,
pub indicators: Option<Vec<Indicator>>,
pub revision: Option<i64>,
pub adversary: Option<String>,
}
impl Into<Vec<SampleHash>> for Pulse {
fn into(self) -> Vec<SampleHash> {
self.indicators
.unwrap_or_default()
.into_iter()
.map(|x| SampleHash::new(x.indicator))
.flat_map(|x| x)
.collect()
}
}
impl Pulse {
pub fn get_url(&self) -> String {
format!("https://otx.alienvault.com/pulse/{}", self.id)
}
}
pub fn hashes_in(pulses: Vec<Pulse>) -> Vec<SampleHash> {
pulses
.into_iter()
.map(|x| x.into())
.flat_map(|x: Vec<SampleHash>| x)
.collect()
}
#[derive(Debug, Deserialize)]
pub struct Response {
pub count: i64,
pub next: Option<String>,
pub previous: Option<String>,
pub results: Vec<Pulse>,
}
impl Pulses {
fn request(&mut self) -> GenericResult<Response> {
let res: Response = reqwest::Client::new()
.get(format!(
"https://otx.alienvault.com/api/v1/pulses/subscribed?limit={}&page={}&modified_since={}",
self.limit,
self.page,
self.modified_since.to_rfc3339()).as_str()
)
.header("X-OTX-API-KEY", HeaderValue::from_str(self.api_key.as_str()).unwrap())
.send()?
.json()?;
self.page += 1;
if res.next == None {
self.has_done = true;
}
Ok(res)
}
pub fn get_all(self) -> Vec<Pulse> {
self.flat_map(|x| x).collect()
}
pub fn get_all_hashes(self) -> Vec<SampleHash> {
hashes_in(self.get_all())
}
}
impl Iterator for Pulses {
type Item = Vec<Pulse>;
fn next(&mut self) -> Option<Self::Item> {
if self.has_done {
None
} else {
if self.page != 1 {
std::thread::sleep(std::time::Duration::from_secs(1));
}
self.request().and_then(|x| Ok(x.results)).ok()
}
}
}