1use std::collections::BTreeMap;
14
15use serde::{Deserialize, Serialize};
16
17fn de_id<'de, D>(deserializer: D) -> std::result::Result<Option<String>, D::Error>
22where
23 D: serde::Deserializer<'de>,
24{
25 let value = Option::<serde_json::Value>::deserialize(deserializer)?;
26 Ok(value.and_then(|v| match v {
27 serde_json::Value::String(s) => Some(s),
28 serde_json::Value::Number(n) => Some(n.to_string()),
29 _ => None,
30 }))
31}
32
33#[derive(Debug, Clone, Deserialize, Default)]
35pub struct Links {
36 #[serde(default)]
38 pub next: Option<String>,
39 #[serde(default)]
41 pub last: Option<String>,
42 #[serde(default, rename = "self")]
44 pub this: Option<String>,
45}
46
47#[derive(Debug, Clone, Deserialize, Default)]
49pub struct Meta {
50 #[serde(flatten)]
52 pub extra: BTreeMap<String, serde_json::Value>,
53}
54
55#[derive(Debug, Clone, Default, Deserialize)]
57#[serde(bound(deserialize = "A: serde::Deserialize<'de> + Default"))]
58pub struct Resource<A> {
59 #[serde(default, deserialize_with = "de_id")]
61 pub id: Option<String>,
62 #[serde(default, rename = "type")]
64 pub kind: Option<String>,
65 #[serde(default)]
67 pub attributes: A,
68 #[serde(default)]
70 pub relationships: serde_json::Value,
71}
72
73#[derive(Debug, Clone, Deserialize)]
79pub struct DataDoc<A> {
80 pub data: A,
82}
83
84#[derive(Debug, Clone, Deserialize)]
86#[serde(bound(deserialize = "A: serde::Deserialize<'de> + Default"))]
87pub struct SingleDoc<A> {
88 pub data: Resource<A>,
90 #[serde(default)]
92 pub links: Links,
93 #[serde(default)]
95 pub meta: Meta,
96}
97
98#[derive(Debug, Clone, Deserialize)]
100#[serde(bound(deserialize = "A: serde::Deserialize<'de> + Default"))]
101pub struct CollectionDoc<A> {
102 #[serde(default)]
104 pub data: Vec<Resource<A>>,
105 #[serde(default)]
107 pub links: Links,
108 #[serde(default)]
110 pub meta: Meta,
111}
112
113#[derive(Debug, Clone)]
115pub struct Page<A> {
116 pub resources: Vec<Resource<A>>,
118 pub next: Option<String>,
120 pub last: Option<String>,
122}
123
124impl<A> Page<A> {
125 pub fn from_doc(doc: CollectionDoc<A>) -> Self {
127 Self {
128 resources: doc.data,
129 next: doc.links.next,
130 last: doc.links.last,
131 }
132 }
133
134 pub fn len(&self) -> usize {
136 self.resources.len()
137 }
138
139 pub fn is_empty(&self) -> bool {
141 self.resources.is_empty()
142 }
143
144 pub fn items(&self) -> impl Iterator<Item = &A> {
146 self.resources.iter().map(|r| &r.attributes)
147 }
148
149 pub fn ids(&self) -> impl Iterator<Item = Option<&str>> {
151 self.resources.iter().map(|r| r.id.as_deref())
152 }
153
154 pub fn into_items(self) -> Vec<A> {
156 self.resources.into_iter().map(|r| r.attributes).collect()
157 }
158}
159
160macro_rules! flexible {
161 ($name:ident { $( $field:ident : $ty:ty ),* $(,)? }) => {
162 #[derive(Debug, Clone, Deserialize, Default)]
163 #[doc = concat!("See the HackerOne API reference for `", stringify!($name), "`.")]
164 pub struct $name {
165 $(
166 #[serde(default)]
167 #[doc = concat!("`", stringify!($field), "`")]
168 pub $field: Option<$ty>,
169 )*
170 #[serde(flatten)]
172 pub extra: BTreeMap<String, serde_json::Value>,
173 }
174 };
175}
176
177flexible!(User {
178 username: String,
179 name: String,
180 email: String,
181 created_at: String,
182 disabled: bool,
183 location: String,
184});
185
186flexible!(Program {
187 handle: String,
188 name: String,
189 state: String,
190 submission_state: String,
191 offers_bounties: bool,
192 policy: String,
193 started_accepting_at: String,
194});
195
196flexible!(StructuredScope {
197 asset_identifier: String,
198 asset_type: String,
199 eligible_for_bounty: bool,
200 eligible_for_submission: bool,
201 max_severity: String,
202 instruction: String,
203 created_at: String,
204});
205
206flexible!(Report {
207 title: String,
208 state: String,
209 created_at: String,
210 updated_at: String,
211 vulnerability_information: String,
212 disclosed_at: String,
213 bounty_awarded_at: String,
214 has_bounty: bool,
215});
216
217flexible!(Weakness {
218 name: String,
219 description: String,
220 external_id: String,
221 created_at: String,
222});
223
224flexible!(Severity {
225 rating: String,
226 score: f64,
227 cvss_vector: String,
228 author_type: String,
229 created_at: String,
230});
231
232flexible!(Hacktivity {
233 title: String,
234 substate: String,
235 url: String,
236 disclosed_at: String,
237 submitted_at: String,
238 disclosed: bool,
239 cve_ids: Vec<String>,
240 cwe: String,
241 severity_rating: String,
242 votes: i64,
243 total_awarded_amount: i64,
244 latest_disclosable_action: String,
245 latest_disclosable_activity_at: String,
246});
247
248flexible!(Earning {
249 amount: f64,
250 created_at: String,
251});
252
253#[derive(Debug, Clone, Default)]
261pub struct Balance {
262 pub balance: Option<f64>,
264 pub currency: Option<String>,
266 pub extra: BTreeMap<String, serde_json::Value>,
268}
269
270fn parse_amount(value: &serde_json::Value) -> Option<f64> {
272 match value {
273 serde_json::Value::Number(n) => n.as_f64(),
274 serde_json::Value::String(s) => s.parse::<f64>().ok(),
275 _ => None,
276 }
277}
278
279impl<'de> Deserialize<'de> for Balance {
280 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
281 where
282 D: serde::Deserializer<'de>,
283 {
284 use serde::de::Error as _;
285 let value = serde_json::Value::deserialize(deserializer)?;
286 let object = match value {
287 serde_json::Value::Object(map) => map,
288 other => {
289 return Err(D::Error::custom(format!(
290 "balance: expected an object, got {other}"
291 )))
292 }
293 };
294
295 let source = match object.get("attributes") {
297 Some(serde_json::Value::Object(attrs)) => attrs.clone(),
298 _ => object,
299 };
300
301 let balance = source.get("balance").and_then(parse_amount);
302 let currency = source
303 .get("currency")
304 .and_then(|v| v.as_str())
305 .map(str::to_string);
306
307 let extra = source
308 .into_iter()
309 .filter(|(k, _)| k != "balance" && k != "currency")
310 .collect();
311
312 Ok(Balance {
313 balance,
314 currency,
315 extra,
316 })
317 }
318}
319
320#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
322#[serde(rename_all = "lowercase")]
323pub enum SeverityRating {
324 None,
326 Low,
328 Medium,
330 High,
332 Critical,
334}
335
336impl SeverityRating {
337 pub fn as_str(self) -> &'static str {
339 match self {
340 SeverityRating::None => "none",
341 SeverityRating::Low => "low",
342 SeverityRating::Medium => "medium",
343 SeverityRating::High => "high",
344 SeverityRating::Critical => "critical",
345 }
346 }
347}
348
349#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
351#[serde(rename_all = "snake_case")]
352pub enum ReportState {
353 New,
355 Triaged,
357 NeedsMoreInfo,
359 Resolved,
361 Informative,
363 NotApplicable,
365 Duplicate,
367 Spam,
369}
370
371impl ReportState {
372 pub fn as_str(self) -> &'static str {
374 match self {
375 ReportState::New => "new",
376 ReportState::Triaged => "triaged",
377 ReportState::NeedsMoreInfo => "needs_more_info",
378 ReportState::Resolved => "resolved",
379 ReportState::Informative => "informative",
380 ReportState::NotApplicable => "not_applicable",
381 ReportState::Duplicate => "duplicate",
382 ReportState::Spam => "spam",
383 }
384 }
385}
386
387#[derive(Debug, Clone, Default, PartialEq)]
411pub struct CreateHackerReport {
412 pub team_handle: String,
414 pub title: String,
416 pub vulnerability_information: String,
418 pub impact: String,
420 pub severity_rating: Option<SeverityRating>,
422 pub weakness_id: Option<u64>,
424 pub structured_scope_id: Option<u64>,
426}
427
428impl CreateHackerReport {
429 pub fn new(team_handle: impl Into<String>, title: impl Into<String>) -> Self {
431 Self {
432 team_handle: team_handle.into(),
433 title: title.into(),
434 ..Default::default()
435 }
436 }
437
438 pub fn vulnerability_information(mut self, text: impl Into<String>) -> Self {
440 self.vulnerability_information = text.into();
441 self
442 }
443
444 pub fn impact(mut self, text: impl Into<String>) -> Self {
446 self.impact = text.into();
447 self
448 }
449
450 pub fn severity(mut self, rating: SeverityRating) -> Self {
452 self.severity_rating = Some(rating);
453 self
454 }
455
456 pub fn weakness_id(mut self, id: u64) -> Self {
458 self.weakness_id = Some(id);
459 self
460 }
461
462 pub fn structured_scope_id(mut self, id: u64) -> Self {
464 self.structured_scope_id = Some(id);
465 self
466 }
467
468 pub fn to_json(&self) -> Result<serde_json::Value, crate::Error> {
470 for (field, value) in [
471 ("team_handle", &self.team_handle),
472 ("title", &self.title),
473 ("vulnerability_information", &self.vulnerability_information),
474 ("impact", &self.impact),
475 ] {
476 if value.trim().is_empty() {
477 return Err(crate::Error::Invalid(format!("report.{field} is required")));
478 }
479 }
480
481 let mut attributes = serde_json::Map::new();
482 attributes.insert("team_handle".into(), serde_json::json!(self.team_handle));
483 attributes.insert("title".into(), serde_json::json!(self.title));
484 attributes.insert(
485 "vulnerability_information".into(),
486 serde_json::json!(self.vulnerability_information),
487 );
488 attributes.insert("impact".into(), serde_json::json!(self.impact));
489 if let Some(rating) = self.severity_rating {
490 attributes.insert("severity_rating".into(), serde_json::json!(rating.as_str()));
491 }
492 if let Some(id) = self.weakness_id {
493 attributes.insert("weakness_id".into(), serde_json::json!(id));
494 }
495 if let Some(id) = self.structured_scope_id {
496 attributes.insert("structured_scope_id".into(), serde_json::json!(id));
497 }
498
499 Ok(serde_json::json!({
500 "data": {
501 "type": "report",
502 "attributes": serde_json::Value::Object(attributes),
503 }
504 }))
505 }
506}
507
508#[derive(Debug, Clone, Default)]
510pub struct PageQuery {
511 pub page_number: Option<u32>,
513 pub page_size: Option<u32>,
515 pub extra: Vec<(String, String)>,
517}
518
519impl PageQuery {
520 pub fn new() -> Self {
522 Self::default()
523 }
524
525 pub fn page(mut self, number: u32, size: u32) -> Self {
527 self.page_number = Some(number);
528 self.page_size = Some(size);
529 self
530 }
531
532 pub fn filter(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
534 self.extra.push((key.into(), value.into()));
535 self
536 }
537
538 pub fn to_pairs(&self) -> Vec<(String, String)> {
540 let mut pairs = Vec::new();
541 if let Some(n) = self.page_number {
542 pairs.push(("page[number]".to_string(), n.to_string()));
543 }
544 if let Some(s) = self.page_size {
545 pairs.push(("page[size]".to_string(), s.to_string()));
546 }
547 pairs.extend(self.extra.iter().cloned());
548 pairs
549 }
550}
551
552#[derive(Debug, Clone, Default)]
557pub struct HacktivityQuery {
558 pub query_string: Option<String>,
560 pub sort: Option<String>,
562 pub page_number: Option<u32>,
564 pub page_size: Option<u32>,
566 pub extra: Vec<(String, String)>,
568}
569
570impl HacktivityQuery {
571 pub fn new() -> Self {
573 Self::default()
574 }
575
576 pub fn query(mut self, query: impl Into<String>) -> Self {
578 self.query_string = Some(query.into());
579 self
580 }
581
582 pub fn sort(mut self, sort: impl Into<String>) -> Self {
584 self.sort = Some(sort.into());
585 self
586 }
587
588 pub fn page(mut self, number: u32, size: u32) -> Self {
590 self.page_number = Some(number);
591 self.page_size = Some(size);
592 self
593 }
594
595 pub fn filter(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
597 self.extra.push((key.into(), value.into()));
598 self
599 }
600
601 pub fn to_pairs(&self) -> Vec<(String, String)> {
603 let mut pairs = Vec::new();
604 if let Some(q) = &self.query_string {
605 pairs.push(("queryString".to_string(), q.clone()));
606 }
607 if let Some(sort) = &self.sort {
608 pairs.push(("sort".to_string(), sort.clone()));
609 }
610 if let Some(n) = self.page_number {
611 pairs.push(("page[number]".to_string(), n.to_string()));
612 }
613 if let Some(s) = self.page_size {
614 pairs.push(("page[size]".to_string(), s.to_string()));
615 }
616 pairs.extend(self.extra.iter().cloned());
617 pairs
618 }
619}
620
621#[derive(Debug, Clone, Default)]
623pub struct ReportQuery {
624 pub states: Vec<String>,
626 pub program: Option<String>,
628 pub sort: Option<String>,
630 pub page_number: Option<u32>,
632 pub page_size: Option<u32>,
634 pub extra: Vec<(String, String)>,
636}
637
638impl ReportQuery {
639 pub fn new() -> Self {
641 Self::default()
642 }
643
644 pub fn state(mut self, state: impl Into<String>) -> Self {
646 self.states.push(state.into());
647 self
648 }
649
650 pub fn program(mut self, handle: impl Into<String>) -> Self {
652 self.program = Some(handle.into());
653 self
654 }
655
656 pub fn sort(mut self, sort: impl Into<String>) -> Self {
658 self.sort = Some(sort.into());
659 self
660 }
661
662 pub fn page(mut self, number: u32, size: u32) -> Self {
664 self.page_number = Some(number);
665 self.page_size = Some(size);
666 self
667 }
668
669 pub fn filter(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
671 self.extra.push((key.into(), value.into()));
672 self
673 }
674
675 pub fn to_pairs(&self) -> Vec<(String, String)> {
677 let mut pairs = Vec::new();
678 for state in &self.states {
679 pairs.push(("filter[state][]".to_string(), state.clone()));
680 }
681 if let Some(program) = &self.program {
682 pairs.push(("filter[program][]".to_string(), program.clone()));
683 }
684 if let Some(sort) = &self.sort {
685 pairs.push(("sort".to_string(), sort.clone()));
686 }
687 if let Some(n) = self.page_number {
688 pairs.push(("page[number]".to_string(), n.to_string()));
689 }
690 if let Some(s) = self.page_size {
691 pairs.push(("page[size]".to_string(), s.to_string()));
692 }
693 pairs.extend(self.extra.iter().cloned());
694 pairs
695 }
696}