cargo_pants/
client.rs

1// Copyright 2019 Glenn Mohre, 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.
14use std::collections::HashMap;
15
16use reqwest::blocking::Client;
17use reqwest::header::{HeaderMap, HeaderValue, USER_AGENT};
18use tracing::debug;
19use url::Url;
20
21use crate::{coordinate::Coordinate, package::Package};
22
23const PRODUCTION_API_BASE: &str = "https://ossindex.sonatype.org/api/v3/";
24
25pub struct OSSIndexClient {
26    url_maker: UrlMaker,
27}
28
29struct UrlMaker {
30    api_base: String,
31    api_key: String,
32}
33
34impl OSSIndexClient {
35    pub fn new(key: String) -> OSSIndexClient {
36        #[cfg(not(test))]
37        let ossindex_api_base = PRODUCTION_API_BASE;
38
39        #[cfg(test)]
40        let ossindex_api_base = &mockito::server_url();
41
42        debug!("Value for ossindex_api_base: {}", ossindex_api_base);
43
44        let url_maker = UrlMaker::new(ossindex_api_base.to_owned(), key);
45
46        OSSIndexClient { url_maker }
47    }
48
49    fn construct_headers(&self) -> HeaderMap {
50        const VERSION: &'static str = env!("CARGO_PKG_VERSION");
51
52        let mut headers = HeaderMap::new();
53        headers.insert(
54            USER_AGENT,
55            HeaderValue::from_str(&format!("cargo-pants/{}", VERSION)).expect(&format!(
56                "version could not be converted to a header: {}",
57                VERSION
58            )),
59        );
60        headers
61    }
62
63    pub fn post_coordinates(&self, purls: Vec<Package>) -> Vec<Coordinate> {
64        let url = self.url_maker.component_report_url();
65        let coordinates: Vec<Coordinate> =
66            self.post_json(url.to_string(), purls).unwrap_or_default();
67        return coordinates;
68    }
69
70    fn post_json(
71        &self,
72        url: String,
73        packages: Vec<Package>,
74    ) -> Result<Vec<Coordinate>, reqwest::Error> {
75        // TODO: The purl parsing should move into it's own function or builder, etc...
76        let mut purls: HashMap<String, Vec<String>> = HashMap::new();
77
78        purls.insert(
79            "coordinates".to_string(),
80            packages.iter().map(|x| x.as_purl()).collect(),
81        );
82        let client = Client::new();
83
84        let response = client
85            .post(&url)
86            .json(&purls)
87            .headers(self.construct_headers())
88            .send()?;
89
90        response.json()
91    }
92}
93
94impl UrlMaker {
95    pub fn new(api_base: String, api_key: String) -> UrlMaker {
96        UrlMaker { api_base, api_key }
97    }
98
99    fn build_url(&self, path: &str) -> Result<Url, url::ParseError> {
100        let mut url = Url::parse(&self.api_base)?.join(path)?;
101        url.query_pairs_mut()
102            .append_pair(&"api_key".to_string(), &self.api_key);
103        Ok(url)
104    }
105
106    pub fn component_report_url(&self) -> Url {
107        self.build_url("component-report").expect(&format!(
108            "Could not construct component-report URL {}",
109            self.api_base
110        ))
111    }
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117    use env_logger::builder;
118    use mockito::mock;
119
120    fn init_logger() {
121        let _ = builder().is_test(true).try_init();
122    }
123
124    #[test]
125    fn new_ossindexclient() {
126        let key = String::from("ALL_YOUR_KEY");
127        let client = OSSIndexClient::new(key);
128        assert_eq!(client.url_maker.api_key, "ALL_YOUR_KEY");
129    }
130
131    #[test]
132    fn new_urlmaker() {
133        let api_base = "https://allyourbase.api/api/v3/";
134        let api_key = "ALL_YOUR_KEY";
135        let urlmaker = UrlMaker::new(api_base.to_string(), api_key.to_string());
136        assert_eq!(urlmaker.api_base, api_base);
137        assert_eq!(urlmaker.api_key, api_key);
138    }
139
140    #[test]
141    fn component_report_url_with_empty_apikey() {
142        let api_base = "https://allyourbase.api/api/v3/";
143        let api_key = "";
144        let urlmaker = UrlMaker::new(api_base.to_string(), api_key.to_string());
145        let report_url = urlmaker.component_report_url();
146        assert_eq!(
147            report_url.as_str(),
148            "https://allyourbase.api/api/v3/component-report?api_key="
149        );
150    }
151
152    #[test]
153    fn test_parse_bytes_as_value() {
154        let raw_json: &[u8] = r##"{
155            "coordinates": "pkg:pypi/rust@0.1.1",
156            "description": "Ribo-Seq Unit Step Transformation",
157            "reference": "https://ossindex.sonatype.org/component/pkg:pypi/rust@0.1.1",
158            "vulnerabilities": [],
159            "source": "registry+https://github.com/rust-lang/crates.io-index"
160        }"##
161        .as_bytes();
162        let value: serde_json::Value =
163            serde_json::from_slice(raw_json).expect("Failed to parse JSON");
164        assert_eq!(value["coordinates"], "pkg:pypi/rust@0.1.1");
165        assert_eq!(value["description"], "Ribo-Seq Unit Step Transformation");
166    }
167
168    fn test_package_data() -> Package {
169        let package_data = r##"{
170            "name": "claxon",
171            "version": "0.3.0",
172            "package_id": ""
173        }"##
174        .as_bytes();
175        serde_json::from_slice::<Package>(package_data).expect("Failed to parse package data")
176    }
177
178    #[test]
179    fn test_post_json() {
180        init_logger();
181
182        let raw_json: &[u8] = r##"{
183            "coordinates": "pkg:cargo/claxon@0.3.0",
184            "description": "A FLAC decoding library",
185            "reference": "https://ossindex.sonatype.org/component/pkg:cargo/claxon@0.3.0",
186            "vulnerabilities": 
187                [
188                    {
189                        "title": "CWE-200: Information Exposure",
190                        "description": "An information exposure is the intentional or unintentional disclosure of information to an actor that is not explicitly authorized to have access to that information.",
191                        "cvssScore": 4.3,
192                        "cvssVector": "CVSS:3.0/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N",
193                        "reference": "https://ossindex.sonatype.org/vuln/bd1aacf1-bc91-441d-aaf8-44f40513200d"
194                    }
195                ],
196            "source": "registry+https://github.com/rust-lang/crates.io-index"
197            }"##.as_bytes();
198        let packages: Vec<Package> = vec![test_package_data()];
199        let mock = mock("POST", "/component-report?api_key=ALL_YOUR_KEY")
200            .with_header("CONTENT_TYPE", "application/json")
201            .with_body(raw_json)
202            .create();
203
204        {
205            let key = String::from("ALL_YOUR_KEY");
206            let client = OSSIndexClient::new(key);
207            client.post_coordinates(packages);
208        }
209        mock.assert();
210    }
211
212    fn test_component_report_json() -> &'static [u8] {
213        return r##"[{
214            "coordinates": "pkg:cargo/claxon@0.3.0",
215            "description": "A FLAC decoding library",
216            "reference": "https://ossindex.sonatype.org/component/pkg:cargo/claxon@0.3.0",
217            "vulnerabilities": [{
218                "id": "bd1aacf1-bc91-441d-aaf8-44f40513200d",
219                "title": "CWE-200: Information Exposure",
220                "description": "An information exposure is the intentional or unintentional disclosure of information to an actor that is not explicitly authorized to have access to that information.",
221                "cvssScore": 4.3,
222                "cvssVector": "CVSS:3.0/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N",
223                "cwe": "CWE-200",
224                "reference": "https://ossindex.sonatype.org/vuln/bd1aacf1-bc91-441d-aaf8-44f40513200d"
225            }]
226            }, {
227            "coordinates": "pkg:cargo/arrayfire@3.5.0",
228            "description": "ArrayFire is a high performance software library for parallel computing with an easy-to-use API. Its array based function set makes parallel programming simple. ArrayFire's multiple backends (CUDA, OpenCL and native CPU) make it platform independent and highly portable. A few lines of code in ArrayFire can replace dozens of lines of parallel computing code, saving you valuable time and lowering development costs. This crate provides Rust bindings for ArrayFire library.",
229            "reference": "https://ossindex.sonatype.org/component/pkg:cargo/arrayfire@3.5.0",
230            "vulnerabilities": [{
231                "id": "bb99215c-ee5f-4539-98a5-f1257429c3a0",
232                "title": "CWE-119: Improper Restriction of Operations within the Bounds of a Memory Buffer",
233                "description": "The software performs operations on a memory buffer, but it can read from or write to a memory location that is outside of the intended boundary of the buffer.",
234                "cvssScore": 8.6,
235                "cvssVector": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:H",
236                "cwe": "CWE-119",
237                "reference": "https://ossindex.sonatype.org/vuln/bb99215c-ee5f-4539-98a5-f1257429c3a0"
238            }]
239        }]"##.as_bytes();
240    }
241}