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
//! A guide to the public REST API for AttackerKB. To generate an API key, navigate to the API tab on your AttackerKB Profile Page.
//!  For more details on the API referer to <https://extensions.rapid7.com/extension/rapid7-attackerkb>
use crate::error::{Error, ErrorResponse, StatusCode};
use crate::pagination::KBResponse;
use crate::v1::query::{AssessmentsParameters, ContributorsParameters, TopicsParameters};
use reqwest::{header, ClientBuilder, RequestBuilder};
use std::fmt::Display;

pub mod error;
pub mod pagination;
pub mod v1;

const BASE_URL: &str = "https://api.attackerkb.com";

/// API client, API token needs to be provided when creating a new instance.
#[derive(Debug, Clone)]
pub struct AttackKBApi {
  base_path: String,
  version: ApiVersion,
  client: reqwest::Client,
}

/// ApiVersion default: v1
#[derive(Debug, Clone)]
pub enum ApiVersion {
  V1,
}

impl Default for ApiVersion {
  fn default() -> Self {
    Self::V1
  }
}

impl Display for ApiVersion {
  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    write!(
      f,
      "{}",
      match self {
        ApiVersion::V1 => String::from("v1"),
      }
    )
  }
}

impl AttackKBApi {
  pub fn new(api_token: Option<impl Into<String>>) -> Result<Self, Error> {
    let mut headers = reqwest::header::HeaderMap::new();
    if let Some(api_token) = api_token {
      let mut auth_value =
        reqwest::header::HeaderValue::from_str(&format!("basic {}", api_token.into()))
          .map_err(|source| Error::InvalidApiToken { source })?;
      auth_value.set_sensitive(true);
      headers.insert(
        header::ACCEPT,
        header::HeaderValue::from_static("application/json"),
      );
      headers.insert(header::AUTHORIZATION, auth_value);
    }
    let api_client = ClientBuilder::new()
      .default_headers(headers)
      .build()
      .map_err(|source| Error::BuildingClient { source })?;
    Ok(AttackKBApi {
      base_path: BASE_URL.to_owned(),
      version: ApiVersion::default(),
      client: api_client,
    })
  }
}

impl AttackKBApi {
  async fn request(&self, request: RequestBuilder) -> Result<KBResponse, Error> {
    let request = request.build()?;
    let resp = self
      .client
      .execute(request)
      .await
      .map_err(|source| Error::RequestFailed { source })?;
    if !resp.status().is_success() {
      return Err(Error::Api {
        error: ErrorResponse {
          message: "not success request".to_string(),
          status: StatusCode::NonZeroU16(resp.status().as_u16()),
        },
      });
    };
    let result = resp
      .json()
      .await
      .map_err(|source| Error::ResponseIo { source })?;
    // let result = serde_json::from_str(&json).unwrap();
    if let KBResponse::Error(err) = &result {
      return Err(Error::Api { error: err.clone() });
    }
    Ok(result)
  }
}

impl AttackKBApi {
  /// Return all topics.
  pub async fn topics(&self, query: &TopicsParameters) -> Result<KBResponse, Error> {
    let u = format!("{}/{}/{}", self.base_path, self.version, "topics");
    self.request(self.client.get(u).query(&query)).await
  }
  /// Return a specific topic.
  pub async fn topic(&self, id: impl Into<String>) -> Result<KBResponse, Error> {
    let u = format!(
      "{}/{}/{}/{}",
      self.base_path,
      self.version,
      "topics",
      id.into()
    );
    self.request(self.client.get(u)).await
  }
  /// Return all assessments.
  pub async fn assessments(&self, query: &AssessmentsParameters) -> Result<KBResponse, Error> {
    let u = format!("{}/{}/{}", self.base_path, self.version, "assessments");
    self.request(self.client.get(u).query(&query)).await
  }
  /// Return a specific assessment.
  pub async fn assessment(&self, id: impl Into<String>) -> Result<KBResponse, Error> {
    let u = format!(
      "{}/{}/{}/{}",
      self.base_path,
      self.version,
      "assessments",
      id.into()
    );
    self.request(self.client.get(u)).await
  }
  /// Return all contributors.
  pub async fn contributors(&self, query: &ContributorsParameters) -> Result<KBResponse, Error> {
    let u = format!("{}/{}/{}", self.base_path, self.version, "contributors");
    self.request(self.client.get(u).query(&query)).await
  }
  /// Return a specific contributor.
  pub async fn contributor(&self, id: impl Into<String>) -> Result<KBResponse, Error> {
    let u = format!(
      "{}/{}/{}/{}",
      self.base_path,
      self.version,
      "contributors",
      id.into()
    );
    self.request(self.client.get(u)).await
  }
}

#[cfg(test)]
mod tests {
  #[test]
  fn it_works() {
    let result = 2 + 2;
    assert_eq!(result, 4);
  }
}