1use reqwest::blocking::Client;
16use reqwest::StatusCode;
17use reqwest::Url;
18use std::error::Error;
19use std::fmt;
20use std::thread;
21use std::time::Duration;
22use tracing::trace;
23
24#[derive(Default, Debug, Clone, PartialEq, serde_derive::Serialize, serde_derive::Deserialize)]
25#[serde(rename_all = "camelCase")]
26pub struct ApplicationResponse {
27 pub applications: Vec<Application>,
28}
29
30#[derive(Default, Debug, Clone, PartialEq, serde_derive::Serialize, serde_derive::Deserialize)]
31#[serde(rename_all = "camelCase")]
32pub struct ApplicationTag {
33 pub id: String,
34 pub tag_id: String,
35 pub application_id: String,
36}
37
38#[derive(Default, Debug, Clone, PartialEq, serde_derive::Serialize, serde_derive::Deserialize)]
39#[serde(rename_all = "camelCase")]
40pub struct SbomSubmitResult {
41 pub status_url: String,
42}
43
44#[derive(Default, Debug, Clone, PartialEq, serde_derive::Serialize, serde_derive::Deserialize)]
45#[serde(rename_all = "camelCase")]
46pub struct StatusURLResult {
47 pub policy_action: String,
48 pub report_html_url: String,
49 pub report_pdf_url: String,
50 pub report_data_url: String,
51 pub embeddable_report_html_url: String,
52 pub is_error: bool,
53 pub error_message: Option<String>,
54 pub components_affected: ComponentsAffected,
55 pub open_policy_violations: OpenPolicyViolations,
56 pub grandfathered_policy_violations: i64,
57}
58
59#[derive(Default, Debug, Clone, PartialEq, serde_derive::Serialize, serde_derive::Deserialize)]
60#[serde(rename_all = "camelCase")]
61pub struct ComponentsAffected {
62 pub critical: i64,
63 pub severe: i64,
64 pub moderate: i64,
65}
66
67#[derive(Default, Debug, Clone, PartialEq, serde_derive::Serialize, serde_derive::Deserialize)]
68#[serde(rename_all = "camelCase")]
69pub struct OpenPolicyViolations {
70 pub critical: i64,
71 pub severe: i64,
72 pub moderate: i64,
73}
74
75#[derive(Default, Debug, Clone, PartialEq, serde_derive::Serialize, serde_derive::Deserialize)]
76#[serde(rename_all = "camelCase")]
77pub struct RawReportResults {
78 pub components: Vec<Component>,
79}
80
81#[derive(Default, Debug, Clone, PartialEq, serde_derive::Serialize, serde_derive::Deserialize)]
82#[serde(rename_all = "camelCase")]
83pub struct LicenseData {
84 pub declared_licenses: Vec<DeclaredLicense>,
85 pub observed_licenses: Vec<ObservedLicense>,
86 pub effective_licenses: Vec<EffectiveLicense>,
87 pub overridden_licenses: Vec<::serde_json::Value>,
88 pub status: String,
89}
90
91#[derive(Default, Debug, Clone, PartialEq, serde_derive::Serialize, serde_derive::Deserialize)]
92#[serde(rename_all = "camelCase")]
93pub struct DeclaredLicense {
94 pub license_id: String,
95 pub license_name: String,
96}
97
98#[derive(Default, Debug, Clone, PartialEq, serde_derive::Serialize, serde_derive::Deserialize)]
99#[serde(rename_all = "camelCase")]
100pub struct ObservedLicense {
101 pub license_id: String,
102 pub license_name: String,
103}
104
105#[derive(Default, Debug, Clone, PartialEq, serde_derive::Serialize, serde_derive::Deserialize)]
106#[serde(rename_all = "camelCase")]
107pub struct EffectiveLicense {
108 pub license_id: String,
109 pub license_name: String,
110}
111
112#[derive(Default, Debug, Clone, PartialEq, serde_derive::Serialize, serde_derive::Deserialize)]
113#[serde(rename_all = "camelCase")]
114pub struct SecurityData {
115 pub security_issues: Vec<SecurityIssue>,
116}
117
118#[derive(Default, Debug, Clone, PartialEq, serde_derive::Serialize, serde_derive::Deserialize)]
119#[serde(rename_all = "camelCase")]
120pub struct SecurityIssue {
121 pub source: String,
122 pub reference: String,
123 pub severity: f64,
124 pub status: String,
125 pub url: String,
126 pub threat_category: String,
127}
128
129#[derive(Default, Debug, Clone, PartialEq, serde_derive::Serialize, serde_derive::Deserialize)]
132#[serde(rename_all = "camelCase")]
133pub struct PolicyReportResult {
134 pub report_time: i64,
135 pub report_title: String,
136 pub commit_hash: Option<String>,
137 pub initiator: String,
138 pub application: Application,
139 pub counts: Counts,
140 pub components: Vec<Component>,
141}
142
143#[derive(Default, Debug, Clone, PartialEq, serde_derive::Serialize, serde_derive::Deserialize)]
144#[serde(rename_all = "camelCase")]
145pub struct Application {
146 pub id: String,
147 pub public_id: String,
148 pub name: String,
149 pub organization_id: Option<String>,
150 pub contact_user_name: Option<String>,
151 pub application_tags: Option<Vec<ApplicationTag>>,
152}
153
154#[derive(Default, Debug, Clone, PartialEq, serde_derive::Serialize, serde_derive::Deserialize)]
155#[serde(rename_all = "camelCase")]
156pub struct Counts {
157 pub partially_matched_component_count: i64,
158 pub exactly_matched_component_count: i64,
159 pub total_component_count: i64,
160 pub grandfathered_policy_violation_count: i64,
161}
162
163#[derive(Default, Debug, Clone, PartialEq, serde_derive::Serialize, serde_derive::Deserialize)]
164#[serde(rename_all = "camelCase")]
165pub struct Component {
166 pub hash: String,
167 pub match_state: String,
168 pub component_identifier: ComponentIdentifier,
169 pub package_url: String,
170 pub proprietary: bool,
171 pub pathnames: Vec<String>,
172 pub dependency_data: Option<DependencyData>,
173 pub violations: Option<Vec<Violation>>,
174 pub display_name: Option<String>,
175}
176
177#[derive(Default, Debug, Clone, PartialEq, serde_derive::Serialize, serde_derive::Deserialize)]
178#[serde(rename_all = "camelCase")]
179pub struct ComponentIdentifier {
180 pub format: String,
181 pub coordinates: Coordinates,
182}
183
184#[derive(Default, Debug, Clone, PartialEq, serde_derive::Serialize, serde_derive::Deserialize)]
185#[serde(rename_all = "camelCase")]
186pub struct Coordinates {
187 pub artifact_id: Option<String>,
188 pub name: Option<String>,
189 pub group_id: Option<String>,
190 pub version: String,
191 pub extension: Option<String>,
192 pub classifier: Option<String>,
193}
194
195#[derive(Default, Debug, Clone, PartialEq, serde_derive::Serialize, serde_derive::Deserialize)]
196#[serde(rename_all = "camelCase")]
197pub struct DependencyData {
198 pub direct_dependency: bool,
199 pub inner_source: bool,
200 #[serde(default)]
201 pub inner_source_data: Vec<InnerSourceDaum>,
202 pub parent_component_purls: Option<Vec<String>>,
203}
204
205#[derive(Default, Debug, Clone, PartialEq, serde_derive::Serialize, serde_derive::Deserialize)]
206#[serde(rename_all = "camelCase")]
207pub struct InnerSourceDaum {
208 pub owner_application_name: String,
209 pub owner_application_id: String,
210 pub inner_source_component_purl: Option<String>,
211}
212
213#[derive(Default, Debug, Clone, PartialEq, serde_derive::Serialize, serde_derive::Deserialize)]
214#[serde(rename_all = "camelCase")]
215pub struct Violation {
216 pub policy_id: String,
217 pub policy_name: String,
218 pub policy_threat_category: String,
219 pub policy_threat_level: i64,
220 pub policy_violation_id: String,
221 pub waived: bool,
222 pub grandfathered: bool,
223 pub constraints: Vec<Constraint>,
224}
225
226#[derive(Default, Debug, Clone, PartialEq, serde_derive::Serialize, serde_derive::Deserialize)]
227#[serde(rename_all = "camelCase")]
228pub struct Constraint {
229 pub constraint_id: String,
230 pub constraint_name: String,
231 pub conditions: Vec<Condition>,
232}
233
234#[derive(Default, Debug, Clone, PartialEq, serde_derive::Serialize, serde_derive::Deserialize)]
235#[serde(rename_all = "camelCase")]
236pub struct Condition {
237 pub condition_summary: String,
238 pub condition_reason: String,
239}
240
241pub struct ReportResults {
242 pub url_results: StatusURLResult,
243 pub data_results: RawReportResults,
244 pub policy_report_results: PolicyReportResult,
245}
246
247#[derive(Debug)]
248struct PollingError(String);
249
250impl fmt::Display for PollingError {
251 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
252 write!(f, "Too many polling attempts occurred: {}", self.0)
253 }
254}
255
256impl Error for PollingError {}
257
258#[derive(Debug)]
259struct GeneralError(String);
260
261impl fmt::Display for GeneralError {
262 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
263 write!(
264 f,
265 "A general error occurred talking to Nexus IQ Server: {}",
266 self.0
267 )
268 }
269}
270
271impl Error for GeneralError {}
272
273pub struct IQClient {
274 pub server: String,
275 user: String,
276 token: String,
277 stage: String,
278 application: String,
279 attempts: u32,
280}
281
282impl IQClient {
283 pub fn new(
284 mut server: String,
285 user: String,
286 token: String,
287 stage: String,
288 application: String,
289 attempts: u32,
290 ) -> IQClient {
291 if server.ends_with("/") {
292 server = server.trim_end_matches("/").to_string();
293 }
294 IQClient {
295 server,
296 user,
297 token,
298 stage,
299 application,
300 attempts,
301 }
302 }
303
304 pub fn audit_with_iq_server(&self, sbom: String) -> Result<ReportResults, Box<dyn Error>> {
305 let app = &self.application;
306 let internal_application_id = match self.get_internal_application_id(app.to_string()) {
307 Ok(internal_application_id) => internal_application_id,
308 Err(e) => return Err(Box::new(e)),
309 };
310
311 if internal_application_id.applications.len() == 0 {
312 return Err(Box::new(GeneralError(format!("could not get internal application id for public application id: {}. perhaps you ({}) lack permissions on this application?", app.to_string(), &self.user.to_string()).into())));
313 }
314 let internal_app = &internal_application_id.applications[0].id;
315
316 let status_url = match self.submit_to_third_party_api(internal_app.to_string(), sbom) {
317 Ok(status_url) => status_url,
318 Err(e) => return Err(Box::new(e)),
319 };
320
321 let mut i = 0;
322 let mut last_err: String = String::new();
323 let status_url_string = &status_url.status_url;
324
325 loop {
326 if i > self.attempts {
327 break;
328 };
329
330 let result = self.poll_status_url(status_url_string.to_string());
331
332 if result.is_ok() {
333 let res = result.unwrap();
334 let data = self.get_raw_report_results(res.report_data_url.clone());
335 let policy = self.get_policy_report_results(res.report_data_url.clone());
336
337 return if data.is_ok() {
338 let combined_results: ReportResults = ReportResults {
339 data_results: data.unwrap(),
340 policy_report_results: policy.unwrap(),
341 url_results: res,
342 };
343 Ok(combined_results)
344 } else {
345 Err(Box::new(GeneralError(data.unwrap_err().to_string())))
346 };
347 }
348 if result.is_err() {
349 let res_err = result.unwrap_err();
350 if res_err.is_status() {
351 match res_err.status().unwrap() {
352 StatusCode::NOT_FOUND => {
353 i = i + 1;
354
355 thread::sleep(Duration::from_secs(1));
356 continue;
357 }
358 _ => break,
359 }
360 } else {
361 last_err = format!("{:#?}", res_err);
362 trace!(
363 "Poll response result err: {:#?}, attempt: {}, of: {}",
364 last_err,
365 i,
366 self.attempts
367 );
368 }
369 }
370 i += 1;
371 }
372
373 let msg = format!(
374 "attempts: {}, max attempts: {}, last error: {}",
375 i, self.attempts, last_err
376 );
377 return Err(Box::new(PollingError(msg)));
378 }
379
380 fn get_internal_application_id(
381 &self,
382 application: String,
383 ) -> Result<ApplicationResponse, reqwest::Error> {
384 let client = Client::new();
385
386 let url = Url::parse(&format!(
387 "{}{}{}",
388 &self.server, "/api/v2/applications?publicId=", &application
389 ))
390 .unwrap();
391
392 let res = client
393 .get(url)
394 .basic_auth(&self.user.to_string(), Some(&self.token.to_string()))
395 .send()?;
396
397 return res.json();
398 }
399
400 fn submit_to_third_party_api(
401 &self,
402 internal_application_id: String,
403 sbom: String,
404 ) -> Result<SbomSubmitResult, reqwest::Error> {
405 let client = Client::new();
406
407 let url = Url::parse(&format!(
408 "{}{}{}{}{}",
409 &self.server,
410 "/api/v2/scan/applications/",
411 internal_application_id,
412 "/sources/cargo-pants?stageId=",
413 &self.stage
414 ))
415 .unwrap();
416
417 let res = client
418 .post(url)
419 .basic_auth(&self.user.to_string(), Some(&self.token.to_string()))
420 .body(sbom.clone())
421 .header("Content-Type", "application/xml")
422 .send()?;
423
424 return res.json();
425 }
426
427 fn poll_status_url(&self, status_url: String) -> Result<StatusURLResult, reqwest::Error> {
428 let client = Client::new();
429
430 let url_string = format!("{}/{}", &self.server, &status_url);
431 let url = Url::parse(&url_string).unwrap();
432 trace!("status url: {}", url);
433
434 let res = client
435 .get(url)
436 .basic_auth(&self.user.to_string(), Some(&self.token.to_string()))
437 .send()?;
438
439 return res.json();
440 }
441
442 fn get_raw_report_results(
443 &self,
444 report_url: String,
445 ) -> Result<RawReportResults, reqwest::Error> {
446 let client = Client::new();
447
448 let url_string = format!("{}/{}", &self.server, &report_url);
449 let url = Url::parse(&url_string).unwrap();
450 let res = client
451 .get(url)
452 .basic_auth(&self.user.to_string(), Some(&self.token.to_string()))
453 .send()?;
454
455 return res.json();
456 }
457
458 fn get_policy_report_results(
459 &self,
460 report_url: String,
461 ) -> Result<PolicyReportResult, reqwest::Error> {
462 let client = Client::new();
463
464 let url_string = format!(
465 "{}/{}",
466 &self.server,
467 &report_url.replace("/raw", "/policy")
468 );
469 let url = Url::parse(&url_string).unwrap();
470
471 let res = client
472 .get(url)
473 .basic_auth(&self.user.to_string(), Some(&self.token.to_string()))
474 .send()?;
475
476 return res.json();
477 }
478}
479
480#[cfg(test)]
481mod tests {
482 use super::*;
483 use mockito::mock;
484
485 #[test]
486 fn new_iqclient() {
487 let iq_server_url: &str = "iqServerURL";
488 let user: &str = "iqUser";
489 let token: &str = "iqToken";
490 let stage: &str = "iqStage";
491 let app_id: &str = "iqAppId";
492 let client = IQClient::new(
493 iq_server_url.to_string(),
494 user.to_string(),
495 token.to_string(),
496 stage.to_string(),
497 app_id.to_string(),
498 1,
499 );
500 assert_eq!(client.server, iq_server_url.to_string());
501 assert_eq!(client.user, user.to_string());
502 assert_eq!(client.token, token.to_string());
503 assert_eq!(client.stage, stage.to_string());
504 assert_eq!(client.application, app_id.to_string());
505 assert_eq!(client.attempts, 1);
506 }
507
508 #[test]
509 fn test_get_internal_app_id_one_application() {
510 let public_app_id = "iqPublicApplicationId".to_string();
511 let mock_path = format!("/api/v2/applications?publicId={}", public_app_id);
512
513 let raw_json: &str = r#"{
514 "applications": [
515 {
516 "id": "4bb67dcfc86344e3a483832f8c496419",
517 "publicId": "iqPublicApplicationId",
518 "name": "MyApplicationName"
519 }
520 ]
521 }"#;
522 let mock = mock("GET", mock_path.as_str())
523 .with_header("CONTENT_TYPE", "application/json")
524 .with_body(raw_json)
525 .create();
526 {
527 let mock_server = &mockito::server_url();
528 let client = IQClient::new(
529 mock_server.parse().unwrap(),
530 "".to_string(),
531 "".to_string(),
532 "".to_string(),
533 public_app_id.to_string(),
534 0,
535 );
536 let internal_application_id = client
537 .get_internal_application_id(public_app_id.to_string())
538 .expect("Failed to retrieve application ID");
539
540 assert_eq!(internal_application_id.applications.len(), 1);
541 assert_eq!(
542 internal_application_id.applications[0].public_id,
543 public_app_id
544 );
545 }
546 mock.assert();
547 }
548
549 #[test]
550 fn test_get_internal_app_id_when_empty() {
551 let public_app_id = "iqPublicApplicationId".to_string();
552 let mock_path = format!("/api/v2/applications?publicId={}", public_app_id);
553
554 let raw_json: &str = r#"{
555 "applications": [
556 ]
557 }"#;
558 let mock = mock("GET", mock_path.as_str())
559 .with_header("CONTENT_TYPE", "application/json")
560 .with_body(raw_json)
561 .create();
562 {
563 let mock_server = &mockito::server_url();
564 let client = IQClient::new(
565 mock_server.parse().unwrap(),
566 "".to_string(),
567 "".to_string(),
568 "".to_string(),
569 public_app_id.to_string(),
570 0,
571 );
572 let internal_application_id = client
573 .get_internal_application_id(public_app_id.to_string())
574 .expect("Failed to retrieve application ID");
575
576 assert_eq!(internal_application_id.applications.len(), 0);
577 }
578 mock.assert();
579 }
580
581 #[test]
582 fn test_audit_with_iq_server_when_applications_empty() {
583 let public_app_id = "iqPublicApplicationId".to_string();
584 let mock_path = format!("/api/v2/applications?publicId={}", public_app_id);
585
586 let raw_json: &str = r#"{
587 "applications": [
588 ]
589 }"#;
590 let mock = mock("GET", mock_path.as_str())
591 .with_header("CONTENT_TYPE", "application/json")
592 .with_body(raw_json)
593 .create();
594 {
595 let mock_server = &mockito::server_url();
596 let client = IQClient::new(
597 mock_server.parse().unwrap(),
598 "".to_string(),
599 "".to_string(),
600 "".to_string(),
601 public_app_id.to_string(),
602 0,
603 );
604 let actual_error = client.audit_with_iq_server("".to_string()).err().unwrap();
605 assert!(format!("{}", actual_error).contains(
606 "A general error occurred talking to Nexus IQ Server: could not get internal application id for public application id: "));
607 }
608 mock.assert();
609 }
610}