use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
fn de_id<'de, D>(deserializer: D) -> std::result::Result<Option<String>, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = Option::<serde_json::Value>::deserialize(deserializer)?;
Ok(value.and_then(|v| match v {
serde_json::Value::String(s) => Some(s),
serde_json::Value::Number(n) => Some(n.to_string()),
_ => None,
}))
}
#[derive(Debug, Clone, Deserialize, Default)]
pub struct Links {
#[serde(default)]
pub next: Option<String>,
#[serde(default)]
pub last: Option<String>,
#[serde(default, rename = "self")]
pub this: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Default)]
pub struct Meta {
#[serde(flatten)]
pub extra: BTreeMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(bound(deserialize = "A: serde::Deserialize<'de> + Default"))]
pub struct Resource<A> {
#[serde(default, deserialize_with = "de_id")]
pub id: Option<String>,
#[serde(default, rename = "type")]
pub kind: Option<String>,
#[serde(default)]
pub attributes: A,
#[serde(default)]
pub relationships: serde_json::Value,
}
#[derive(Debug, Clone, Deserialize)]
pub struct DataDoc<A> {
pub data: A,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(bound(deserialize = "A: serde::Deserialize<'de> + Default"))]
pub struct SingleDoc<A> {
pub data: Resource<A>,
#[serde(default)]
pub links: Links,
#[serde(default)]
pub meta: Meta,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(bound(deserialize = "A: serde::Deserialize<'de> + Default"))]
pub struct CollectionDoc<A> {
#[serde(default)]
pub data: Vec<Resource<A>>,
#[serde(default)]
pub links: Links,
#[serde(default)]
pub meta: Meta,
}
#[derive(Debug, Clone)]
pub struct Page<A> {
pub resources: Vec<Resource<A>>,
pub next: Option<String>,
pub last: Option<String>,
}
impl<A> Page<A> {
pub fn from_doc(doc: CollectionDoc<A>) -> Self {
Self {
resources: doc.data,
next: doc.links.next,
last: doc.links.last,
}
}
pub fn len(&self) -> usize {
self.resources.len()
}
pub fn is_empty(&self) -> bool {
self.resources.is_empty()
}
pub fn items(&self) -> impl Iterator<Item = &A> {
self.resources.iter().map(|r| &r.attributes)
}
pub fn ids(&self) -> impl Iterator<Item = Option<&str>> {
self.resources.iter().map(|r| r.id.as_deref())
}
pub fn into_items(self) -> Vec<A> {
self.resources.into_iter().map(|r| r.attributes).collect()
}
}
macro_rules! flexible {
($name:ident { $( $field:ident : $ty:ty ),* $(,)? }) => {
#[derive(Debug, Clone, Deserialize, Default)]
#[doc = concat!("See the HackerOne API reference for `", stringify!($name), "`.")]
pub struct $name {
$(
#[serde(default)]
#[doc = concat!("`", stringify!($field), "`")]
pub $field: Option<$ty>,
)*
#[serde(flatten)]
pub extra: BTreeMap<String, serde_json::Value>,
}
};
}
flexible!(User {
username: String,
name: String,
email: String,
created_at: String,
disabled: bool,
location: String,
});
flexible!(Program {
handle: String,
name: String,
state: String,
submission_state: String,
offers_bounties: bool,
policy: String,
started_accepting_at: String,
});
flexible!(StructuredScope {
asset_identifier: String,
asset_type: String,
eligible_for_bounty: bool,
eligible_for_submission: bool,
max_severity: String,
instruction: String,
created_at: String,
});
flexible!(Report {
title: String,
state: String,
created_at: String,
updated_at: String,
vulnerability_information: String,
disclosed_at: String,
bounty_awarded_at: String,
has_bounty: bool,
});
flexible!(Weakness {
name: String,
description: String,
external_id: String,
created_at: String,
});
flexible!(Severity {
rating: String,
score: f64,
cvss_vector: String,
author_type: String,
created_at: String,
});
flexible!(Hacktivity {
title: String,
substate: String,
url: String,
disclosed_at: String,
submitted_at: String,
disclosed: bool,
cve_ids: Vec<String>,
cwe: String,
severity_rating: String,
votes: i64,
total_awarded_amount: i64,
latest_disclosable_action: String,
latest_disclosable_activity_at: String,
});
flexible!(Earning {
amount: f64,
created_at: String,
});
#[derive(Debug, Clone, Default)]
pub struct Balance {
pub balance: Option<f64>,
pub currency: Option<String>,
pub extra: BTreeMap<String, serde_json::Value>,
}
fn parse_amount(value: &serde_json::Value) -> Option<f64> {
match value {
serde_json::Value::Number(n) => n.as_f64(),
serde_json::Value::String(s) => s.parse::<f64>().ok(),
_ => None,
}
}
impl<'de> Deserialize<'de> for Balance {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de::Error as _;
let value = serde_json::Value::deserialize(deserializer)?;
let object = match value {
serde_json::Value::Object(map) => map,
other => {
return Err(D::Error::custom(format!(
"balance: expected an object, got {other}"
)))
}
};
let source = match object.get("attributes") {
Some(serde_json::Value::Object(attrs)) => attrs.clone(),
_ => object,
};
let balance = source.get("balance").and_then(parse_amount);
let currency = source
.get("currency")
.and_then(|v| v.as_str())
.map(str::to_string);
let extra = source
.into_iter()
.filter(|(k, _)| k != "balance" && k != "currency")
.collect();
Ok(Balance {
balance,
currency,
extra,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum SeverityRating {
None,
Low,
Medium,
High,
Critical,
}
impl SeverityRating {
pub fn as_str(self) -> &'static str {
match self {
SeverityRating::None => "none",
SeverityRating::Low => "low",
SeverityRating::Medium => "medium",
SeverityRating::High => "high",
SeverityRating::Critical => "critical",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ReportState {
New,
Triaged,
NeedsMoreInfo,
Resolved,
Informative,
NotApplicable,
Duplicate,
Spam,
}
impl ReportState {
pub fn as_str(self) -> &'static str {
match self {
ReportState::New => "new",
ReportState::Triaged => "triaged",
ReportState::NeedsMoreInfo => "needs_more_info",
ReportState::Resolved => "resolved",
ReportState::Informative => "informative",
ReportState::NotApplicable => "not_applicable",
ReportState::Duplicate => "duplicate",
ReportState::Spam => "spam",
}
}
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct CreateHackerReport {
pub team_handle: String,
pub title: String,
pub vulnerability_information: String,
pub impact: String,
pub severity_rating: Option<SeverityRating>,
pub weakness_id: Option<u64>,
pub structured_scope_id: Option<u64>,
}
impl CreateHackerReport {
pub fn new(team_handle: impl Into<String>, title: impl Into<String>) -> Self {
Self {
team_handle: team_handle.into(),
title: title.into(),
..Default::default()
}
}
pub fn vulnerability_information(mut self, text: impl Into<String>) -> Self {
self.vulnerability_information = text.into();
self
}
pub fn impact(mut self, text: impl Into<String>) -> Self {
self.impact = text.into();
self
}
pub fn severity(mut self, rating: SeverityRating) -> Self {
self.severity_rating = Some(rating);
self
}
pub fn weakness_id(mut self, id: u64) -> Self {
self.weakness_id = Some(id);
self
}
pub fn structured_scope_id(mut self, id: u64) -> Self {
self.structured_scope_id = Some(id);
self
}
pub fn to_json(&self) -> Result<serde_json::Value, crate::Error> {
for (field, value) in [
("team_handle", &self.team_handle),
("title", &self.title),
("vulnerability_information", &self.vulnerability_information),
("impact", &self.impact),
] {
if value.trim().is_empty() {
return Err(crate::Error::Invalid(format!("report.{field} is required")));
}
}
let mut attributes = serde_json::Map::new();
attributes.insert("team_handle".into(), serde_json::json!(self.team_handle));
attributes.insert("title".into(), serde_json::json!(self.title));
attributes.insert(
"vulnerability_information".into(),
serde_json::json!(self.vulnerability_information),
);
attributes.insert("impact".into(), serde_json::json!(self.impact));
if let Some(rating) = self.severity_rating {
attributes.insert("severity_rating".into(), serde_json::json!(rating.as_str()));
}
if let Some(id) = self.weakness_id {
attributes.insert("weakness_id".into(), serde_json::json!(id));
}
if let Some(id) = self.structured_scope_id {
attributes.insert("structured_scope_id".into(), serde_json::json!(id));
}
Ok(serde_json::json!({
"data": {
"type": "report",
"attributes": serde_json::Value::Object(attributes),
}
}))
}
}
#[derive(Debug, Clone, Default)]
pub struct PageQuery {
pub page_number: Option<u32>,
pub page_size: Option<u32>,
pub extra: Vec<(String, String)>,
}
impl PageQuery {
pub fn new() -> Self {
Self::default()
}
pub fn page(mut self, number: u32, size: u32) -> Self {
self.page_number = Some(number);
self.page_size = Some(size);
self
}
pub fn filter(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.extra.push((key.into(), value.into()));
self
}
pub fn to_pairs(&self) -> Vec<(String, String)> {
let mut pairs = Vec::new();
if let Some(n) = self.page_number {
pairs.push(("page[number]".to_string(), n.to_string()));
}
if let Some(s) = self.page_size {
pairs.push(("page[size]".to_string(), s.to_string()));
}
pairs.extend(self.extra.iter().cloned());
pairs
}
}
#[derive(Debug, Clone, Default)]
pub struct HacktivityQuery {
pub query_string: Option<String>,
pub sort: Option<String>,
pub page_number: Option<u32>,
pub page_size: Option<u32>,
pub extra: Vec<(String, String)>,
}
impl HacktivityQuery {
pub fn new() -> Self {
Self::default()
}
pub fn query(mut self, query: impl Into<String>) -> Self {
self.query_string = Some(query.into());
self
}
pub fn sort(mut self, sort: impl Into<String>) -> Self {
self.sort = Some(sort.into());
self
}
pub fn page(mut self, number: u32, size: u32) -> Self {
self.page_number = Some(number);
self.page_size = Some(size);
self
}
pub fn filter(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.extra.push((key.into(), value.into()));
self
}
pub fn to_pairs(&self) -> Vec<(String, String)> {
let mut pairs = Vec::new();
if let Some(q) = &self.query_string {
pairs.push(("queryString".to_string(), q.clone()));
}
if let Some(sort) = &self.sort {
pairs.push(("sort".to_string(), sort.clone()));
}
if let Some(n) = self.page_number {
pairs.push(("page[number]".to_string(), n.to_string()));
}
if let Some(s) = self.page_size {
pairs.push(("page[size]".to_string(), s.to_string()));
}
pairs.extend(self.extra.iter().cloned());
pairs
}
}
#[derive(Debug, Clone, Default)]
pub struct ReportQuery {
pub states: Vec<String>,
pub program: Option<String>,
pub sort: Option<String>,
pub page_number: Option<u32>,
pub page_size: Option<u32>,
pub extra: Vec<(String, String)>,
}
impl ReportQuery {
pub fn new() -> Self {
Self::default()
}
pub fn state(mut self, state: impl Into<String>) -> Self {
self.states.push(state.into());
self
}
pub fn program(mut self, handle: impl Into<String>) -> Self {
self.program = Some(handle.into());
self
}
pub fn sort(mut self, sort: impl Into<String>) -> Self {
self.sort = Some(sort.into());
self
}
pub fn page(mut self, number: u32, size: u32) -> Self {
self.page_number = Some(number);
self.page_size = Some(size);
self
}
pub fn filter(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.extra.push((key.into(), value.into()));
self
}
pub fn to_pairs(&self) -> Vec<(String, String)> {
let mut pairs = Vec::new();
for state in &self.states {
pairs.push(("filter[state][]".to_string(), state.clone()));
}
if let Some(program) = &self.program {
pairs.push(("filter[program][]".to_string(), program.clone()));
}
if let Some(sort) = &self.sort {
pairs.push(("sort".to_string(), sort.clone()));
}
if let Some(n) = self.page_number {
pairs.push(("page[number]".to_string(), n.to_string()));
}
if let Some(s) = self.page_size {
pairs.push(("page[size]".to_string(), s.to_string()));
}
pairs.extend(self.extra.iter().cloned());
pairs
}
}