1use std::collections::BTreeMap;
14
15use serde::{Deserialize, Serialize};
16
17#[derive(Debug, Clone, Deserialize, Default)]
19pub struct Links {
20 #[serde(default)]
22 pub next: Option<String>,
23 #[serde(default)]
25 pub last: Option<String>,
26 #[serde(default, rename = "self")]
28 pub this: Option<String>,
29}
30
31#[derive(Debug, Clone, Deserialize, Default)]
33pub struct Meta {
34 #[serde(flatten)]
36 pub extra: BTreeMap<String, serde_json::Value>,
37}
38
39#[derive(Debug, Clone, Default, Deserialize)]
41#[serde(bound(deserialize = "A: serde::Deserialize<'de> + Default"))]
42pub struct Resource<A> {
43 #[serde(default)]
45 pub id: Option<String>,
46 #[serde(default, rename = "type")]
48 pub kind: Option<String>,
49 #[serde(default)]
51 pub attributes: A,
52 #[serde(default)]
54 pub relationships: serde_json::Value,
55}
56
57#[derive(Debug, Clone, Deserialize)]
59#[serde(bound(deserialize = "A: serde::Deserialize<'de> + Default"))]
60pub struct SingleDoc<A> {
61 pub data: Resource<A>,
63 #[serde(default)]
65 pub links: Links,
66 #[serde(default)]
68 pub meta: Meta,
69}
70
71#[derive(Debug, Clone, Deserialize)]
73#[serde(bound(deserialize = "A: serde::Deserialize<'de> + Default"))]
74pub struct CollectionDoc<A> {
75 #[serde(default)]
77 pub data: Vec<Resource<A>>,
78 #[serde(default)]
80 pub links: Links,
81 #[serde(default)]
83 pub meta: Meta,
84}
85
86#[derive(Debug, Clone)]
88pub struct Page<A> {
89 pub resources: Vec<Resource<A>>,
91 pub next: Option<String>,
93 pub last: Option<String>,
95}
96
97impl<A> Page<A> {
98 pub fn from_doc(doc: CollectionDoc<A>) -> Self {
100 Self {
101 resources: doc.data,
102 next: doc.links.next,
103 last: doc.links.last,
104 }
105 }
106
107 pub fn len(&self) -> usize {
109 self.resources.len()
110 }
111
112 pub fn is_empty(&self) -> bool {
114 self.resources.is_empty()
115 }
116
117 pub fn items(&self) -> impl Iterator<Item = &A> {
119 self.resources.iter().map(|r| &r.attributes)
120 }
121
122 pub fn ids(&self) -> impl Iterator<Item = Option<&str>> {
124 self.resources.iter().map(|r| r.id.as_deref())
125 }
126
127 pub fn into_items(self) -> Vec<A> {
129 self.resources.into_iter().map(|r| r.attributes).collect()
130 }
131}
132
133macro_rules! flexible {
134 ($name:ident { $( $field:ident : $ty:ty ),* $(,)? }) => {
135 #[derive(Debug, Clone, Deserialize, Default)]
136 #[doc = concat!("See the HackerOne API reference for `", stringify!($name), "`.")]
137 pub struct $name {
138 $(
139 #[serde(default)]
140 #[doc = concat!("`", stringify!($field), "`")]
141 pub $field: Option<$ty>,
142 )*
143 #[serde(flatten)]
145 pub extra: BTreeMap<String, serde_json::Value>,
146 }
147 };
148}
149
150flexible!(User {
151 username: String,
152 name: String,
153 email: String,
154 created_at: String,
155 disabled: bool,
156 location: String,
157});
158
159flexible!(Program {
160 handle: String,
161 name: String,
162 state: String,
163 submission_state: String,
164 offers_bounties: bool,
165 policy: String,
166 started_accepting_at: String,
167});
168
169flexible!(StructuredScope {
170 asset_identifier: String,
171 asset_type: String,
172 eligible_for_bounty: bool,
173 eligible_for_submission: bool,
174 max_severity: String,
175 instruction: String,
176 created_at: String,
177});
178
179flexible!(Report {
180 title: String,
181 state: String,
182 created_at: String,
183 updated_at: String,
184 vulnerability_information: String,
185 disclosed_at: String,
186 bounty_awarded_at: String,
187 has_bounty: bool,
188});
189
190flexible!(Weakness {
191 name: String,
192 description: String,
193 external_id: String,
194 created_at: String,
195});
196
197flexible!(Severity {
198 rating: String,
199 score: f64,
200 cvss_vector: String,
201 author_type: String,
202 created_at: String,
203});
204
205#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
207#[serde(rename_all = "lowercase")]
208pub enum SeverityRating {
209 None,
211 Low,
213 Medium,
215 High,
217 Critical,
219}
220
221impl SeverityRating {
222 pub fn as_str(self) -> &'static str {
224 match self {
225 SeverityRating::None => "none",
226 SeverityRating::Low => "low",
227 SeverityRating::Medium => "medium",
228 SeverityRating::High => "high",
229 SeverityRating::Critical => "critical",
230 }
231 }
232}
233
234#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
236#[serde(rename_all = "snake_case")]
237pub enum ReportState {
238 New,
240 Triaged,
242 NeedsMoreInfo,
244 Resolved,
246 Informative,
248 NotApplicable,
250 Duplicate,
252 Spam,
254}
255
256impl ReportState {
257 pub fn as_str(self) -> &'static str {
259 match self {
260 ReportState::New => "new",
261 ReportState::Triaged => "triaged",
262 ReportState::NeedsMoreInfo => "needs_more_info",
263 ReportState::Resolved => "resolved",
264 ReportState::Informative => "informative",
265 ReportState::NotApplicable => "not_applicable",
266 ReportState::Duplicate => "duplicate",
267 ReportState::Spam => "spam",
268 }
269 }
270}
271
272#[derive(Debug, Clone, Default)]
275pub struct CreateReport {
276 pub program: String,
278 pub title: String,
280 pub vulnerability_information: Option<String>,
282 pub impact: Option<String>,
284 pub severity_rating: Option<SeverityRating>,
286 pub weakness_id: Option<String>,
288 pub structured_scope_id: Option<String>,
290 pub custom_fields: BTreeMap<String, serde_json::Value>,
292}
293
294impl CreateReport {
295 pub fn new(program: impl Into<String>, title: impl Into<String>) -> Self {
297 Self {
298 program: program.into(),
299 title: title.into(),
300 ..Default::default()
301 }
302 }
303
304 pub fn vulnerability_information(mut self, text: impl Into<String>) -> Self {
306 self.vulnerability_information = Some(text.into());
307 self
308 }
309
310 pub fn impact(mut self, text: impl Into<String>) -> Self {
312 self.impact = Some(text.into());
313 self
314 }
315
316 pub fn severity(mut self, rating: SeverityRating) -> Self {
318 self.severity_rating = Some(rating);
319 self
320 }
321
322 pub fn weakness(mut self, id: impl Into<String>) -> Self {
324 self.weakness_id = Some(id.into());
325 self
326 }
327
328 pub fn structured_scope(mut self, id: impl Into<String>) -> Self {
330 self.structured_scope_id = Some(id.into());
331 self
332 }
333
334 pub fn custom_field(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
336 self.custom_fields.insert(key.into(), value);
337 self
338 }
339
340 pub fn to_json(&self) -> Result<serde_json::Value, crate::Error> {
342 if self.program.trim().is_empty() {
343 return Err(crate::Error::Invalid("report.program is required".into()));
344 }
345 if self.title.trim().is_empty() {
346 return Err(crate::Error::Invalid("report.title is required".into()));
347 }
348
349 let mut attributes = serde_json::Map::new();
350 attributes.insert("title".into(), serde_json::json!(self.title));
351 if let Some(v) = &self.vulnerability_information {
352 attributes.insert("vulnerability_information".into(), serde_json::json!(v));
353 }
354 if let Some(v) = &self.impact {
355 attributes.insert("impact".into(), serde_json::json!(v));
356 }
357 if let Some(rating) = self.severity_rating {
358 attributes.insert("severity_rating".into(), serde_json::json!(rating.as_str()));
359 }
360 for (key, value) in &self.custom_fields {
361 attributes.insert(key.clone(), value.clone());
362 }
363
364 let mut relationships = serde_json::Map::new();
365 let program_id = self.program.trim_start_matches('@');
366 relationships.insert(
367 "program".into(),
368 serde_json::json!({ "data": { "type": "program", "id": program_id } }),
369 );
370 if let Some(id) = &self.weakness_id {
371 relationships.insert(
372 "weakness".into(),
373 serde_json::json!({ "data": { "type": "weakness", "id": id } }),
374 );
375 }
376 if let Some(id) = &self.structured_scope_id {
377 relationships.insert(
378 "structured_scope".into(),
379 serde_json::json!({ "data": { "type": "structured-scope", "id": id } }),
380 );
381 }
382
383 Ok(serde_json::json!({
384 "data": {
385 "type": "report",
386 "attributes": serde_json::Value::Object(attributes),
387 "relationships": serde_json::Value::Object(relationships),
388 }
389 }))
390 }
391}
392
393#[derive(Debug, Clone, Default)]
395pub struct ReportQuery {
396 pub states: Vec<String>,
398 pub program: Option<String>,
400 pub sort: Option<String>,
402 pub page_number: Option<u32>,
404 pub page_size: Option<u32>,
406 pub extra: Vec<(String, String)>,
408}
409
410impl ReportQuery {
411 pub fn new() -> Self {
413 Self::default()
414 }
415
416 pub fn state(mut self, state: impl Into<String>) -> Self {
418 self.states.push(state.into());
419 self
420 }
421
422 pub fn program(mut self, handle: impl Into<String>) -> Self {
424 self.program = Some(handle.into());
425 self
426 }
427
428 pub fn sort(mut self, sort: impl Into<String>) -> Self {
430 self.sort = Some(sort.into());
431 self
432 }
433
434 pub fn page(mut self, number: u32, size: u32) -> Self {
436 self.page_number = Some(number);
437 self.page_size = Some(size);
438 self
439 }
440
441 pub fn filter(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
443 self.extra.push((key.into(), value.into()));
444 self
445 }
446
447 pub fn to_pairs(&self) -> Vec<(String, String)> {
449 let mut pairs = Vec::new();
450 for state in &self.states {
451 pairs.push(("filter[state][]".to_string(), state.clone()));
452 }
453 if let Some(program) = &self.program {
454 pairs.push(("filter[program][]".to_string(), program.clone()));
455 }
456 if let Some(sort) = &self.sort {
457 pairs.push(("sort".to_string(), sort.clone()));
458 }
459 if let Some(n) = self.page_number {
460 pairs.push(("page[number]".to_string(), n.to_string()));
461 }
462 if let Some(s) = self.page_size {
463 pairs.push(("page[size]".to_string(), s.to_string()));
464 }
465 pairs.extend(self.extra.iter().cloned());
466 pairs
467 }
468}