Skip to main content

cargo_pants/
iq.rs

1// Copyright 2021 Sonatype.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use 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// POLICY REPORT
130
131#[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}