1use hex;
7use hmac::{Hmac, Mac};
8use reqwest::{Client, multipart};
9use serde::Serialize;
10use sha2::Sha256;
11use std::collections::HashMap;
12use std::time::{SystemTime, UNIX_EPOCH};
13use url::Url;
14
15use crate::{VeracodeConfig, VeracodeError};
16
17type HmacSha256 = Hmac<Sha256>;
19
20#[derive(Clone)]
25pub struct VeracodeClient {
26 config: VeracodeConfig,
27 client: Client,
28}
29
30impl VeracodeClient {
31 pub fn new(config: VeracodeConfig) -> Result<Self, VeracodeError> {
41 let mut client_builder = Client::builder();
42
43 if !config.validate_certificates {
45 client_builder = client_builder
46 .danger_accept_invalid_certs(true)
47 .danger_accept_invalid_hostnames(true);
48 }
49
50 let client = client_builder.build().map_err(VeracodeError::Http)?;
51 Ok(Self { config, client })
52 }
53
54 pub fn base_url(&self) -> &str {
56 &self.config.base_url
57 }
58
59 pub fn config(&self) -> &VeracodeConfig {
61 &self.config
62 }
63
64 pub fn client(&self) -> &Client {
66 &self.client
67 }
68
69 fn generate_hmac_signature(
71 &self,
72 method: &str,
73 url: &str,
74 timestamp: u64,
75 nonce: &str,
76 ) -> Result<String, VeracodeError> {
77 let url_parsed = Url::parse(url)
78 .map_err(|_| VeracodeError::Authentication("Invalid URL".to_string()))?;
79
80 let path_and_query = match url_parsed.query() {
81 Some(query) => format!("{}?{}", url_parsed.path(), query),
82 None => url_parsed.path().to_string(),
83 };
84
85 let host = url_parsed.host_str().unwrap_or("");
86
87 let data = format!(
90 "id={}&host={}&url={}&method={}",
91 self.config.api_id.as_str(),
92 host,
93 path_and_query,
94 method
95 );
96
97 let timestamp_str = timestamp.to_string();
98 let ver_str = "vcode_request_version_1";
99
100 let key_bytes = hex::decode(self.config.api_key.as_str()).map_err(|_| {
102 VeracodeError::Authentication("Invalid API key format - must be hex string".to_string())
103 })?;
104
105 let nonce_bytes = hex::decode(nonce)
106 .map_err(|_| VeracodeError::Authentication("Invalid nonce format".to_string()))?;
107
108 let mut mac1 = HmacSha256::new_from_slice(&key_bytes)
110 .map_err(|_| VeracodeError::Authentication("Failed to create HMAC".to_string()))?;
111 mac1.update(&nonce_bytes);
112 let hashed_nonce = mac1.finalize().into_bytes();
113
114 let mut mac2 = HmacSha256::new_from_slice(&hashed_nonce)
116 .map_err(|_| VeracodeError::Authentication("Failed to create HMAC".to_string()))?;
117 mac2.update(timestamp_str.as_bytes());
118 let hashed_timestamp = mac2.finalize().into_bytes();
119
120 let mut mac3 = HmacSha256::new_from_slice(&hashed_timestamp)
122 .map_err(|_| VeracodeError::Authentication("Failed to create HMAC".to_string()))?;
123 mac3.update(ver_str.as_bytes());
124 let hashed_ver_str = mac3.finalize().into_bytes();
125
126 let mut mac4 = HmacSha256::new_from_slice(&hashed_ver_str)
128 .map_err(|_| VeracodeError::Authentication("Failed to create HMAC".to_string()))?;
129 mac4.update(data.as_bytes());
130 let signature = mac4.finalize().into_bytes();
131
132 Ok(hex::encode(signature).to_lowercase())
134 }
135
136 pub fn generate_auth_header(&self, method: &str, url: &str) -> Result<String, VeracodeError> {
138 let timestamp = SystemTime::now()
139 .duration_since(UNIX_EPOCH)
140 .unwrap()
141 .as_millis() as u64; let nonce_bytes: [u8; 16] = rand::random();
145 let nonce = hex::encode(nonce_bytes);
146
147 let signature = self.generate_hmac_signature(method, url, timestamp, &nonce)?;
148
149 Ok(format!(
150 "VERACODE-HMAC-SHA-256 id={},ts={},nonce={},sig={}",
151 self.config.api_id.as_str(),
152 timestamp,
153 nonce,
154 signature
155 ))
156 }
157
158 pub async fn get(
169 &self,
170 endpoint: &str,
171 query_params: Option<&[(String, String)]>,
172 ) -> Result<reqwest::Response, VeracodeError> {
173 let mut url = format!("{}{}", self.config.base_url, endpoint);
174
175 if let Some(params) = query_params {
176 if !params.is_empty() {
177 url.push('?');
178 url.push_str(
179 ¶ms
180 .iter()
181 .map(|(k, v)| format!("{k}={v}"))
182 .collect::<Vec<_>>()
183 .join("&"),
184 );
185 }
186 }
187
188 let auth_header = self.generate_auth_header("GET", &url)?;
189
190 let response = self
191 .client
192 .get(&url)
193 .header("Authorization", auth_header)
194 .header("Content-Type", "application/json")
195 .send()
196 .await?;
197
198 Ok(response)
199 }
200
201 pub async fn post<T: Serialize>(
212 &self,
213 endpoint: &str,
214 body: Option<&T>,
215 ) -> Result<reqwest::Response, VeracodeError> {
216 let url = format!("{}{}", self.config.base_url, endpoint);
217 let auth_header = self.generate_auth_header("POST", &url)?;
218
219 let mut request = self
220 .client
221 .post(&url)
222 .header("Authorization", auth_header)
223 .header("Content-Type", "application/json");
224
225 if let Some(body) = body {
226 request = request.json(body);
227 }
228
229 let response = request.send().await?;
230 Ok(response)
231 }
232
233 pub async fn put<T: Serialize>(
244 &self,
245 endpoint: &str,
246 body: Option<&T>,
247 ) -> Result<reqwest::Response, VeracodeError> {
248 let url = format!("{}{}", self.config.base_url, endpoint);
249 let auth_header = self.generate_auth_header("PUT", &url)?;
250
251 let mut request = self
252 .client
253 .put(&url)
254 .header("Authorization", auth_header)
255 .header("Content-Type", "application/json");
256
257 if let Some(body) = body {
258 request = request.json(body);
259 }
260
261 let response = request.send().await?;
262 Ok(response)
263 }
264
265 pub async fn delete(&self, endpoint: &str) -> Result<reqwest::Response, VeracodeError> {
275 let url = format!("{}{}", self.config.base_url, endpoint);
276 let auth_header = self.generate_auth_header("DELETE", &url)?;
277
278 let response = self
279 .client
280 .delete(&url)
281 .header("Authorization", auth_header)
282 .header("Content-Type", "application/json")
283 .send()
284 .await?;
285
286 Ok(response)
287 }
288
289 pub async fn handle_response(
301 response: reqwest::Response,
302 ) -> Result<reqwest::Response, VeracodeError> {
303 if !response.status().is_success() {
304 let status = response.status();
305 let error_text = response.text().await?;
306 return Err(VeracodeError::InvalidResponse(format!(
307 "HTTP {status}: {error_text}"
308 )));
309 }
310 Ok(response)
311 }
312
313 pub async fn get_with_query(
326 &self,
327 endpoint: &str,
328 query_params: Option<Vec<(String, String)>>,
329 ) -> Result<reqwest::Response, VeracodeError> {
330 let query_slice = query_params.as_deref();
331 let response = self.get(endpoint, query_slice).await?;
332 Self::handle_response(response).await
333 }
334
335 pub async fn post_with_response<T: Serialize>(
346 &self,
347 endpoint: &str,
348 body: Option<&T>,
349 ) -> Result<reqwest::Response, VeracodeError> {
350 let response = self.post(endpoint, body).await?;
351 Self::handle_response(response).await
352 }
353
354 pub async fn put_with_response<T: Serialize>(
365 &self,
366 endpoint: &str,
367 body: Option<&T>,
368 ) -> Result<reqwest::Response, VeracodeError> {
369 let response = self.put(endpoint, body).await?;
370 Self::handle_response(response).await
371 }
372
373 pub async fn delete_with_response(
383 &self,
384 endpoint: &str,
385 ) -> Result<reqwest::Response, VeracodeError> {
386 let response = self.delete(endpoint).await?;
387 Self::handle_response(response).await
388 }
389
390 pub async fn get_paginated(
405 &self,
406 endpoint: &str,
407 base_query_params: Option<Vec<(String, String)>>,
408 page_size: Option<u32>,
409 ) -> Result<String, VeracodeError> {
410 let size = page_size.unwrap_or(500);
411 let mut page = 0;
412 let mut all_items = Vec::new();
413 let mut page_info = None;
414
415 loop {
416 let mut query_params = base_query_params.clone().unwrap_or_default();
417 query_params.push(("page".to_string(), page.to_string()));
418 query_params.push(("size".to_string(), size.to_string()));
419
420 let response = self.get_with_query(endpoint, Some(query_params)).await?;
421 let response_text = response.text().await?;
422
423 if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(&response_text) {
425 if let Some(embedded) = json_value.get("_embedded") {
427 if let Some(items_array) =
428 embedded.as_object().and_then(|obj| obj.values().next())
429 {
430 if let Some(items) = items_array.as_array() {
431 if items.is_empty() {
432 break; }
434 all_items.extend(items.clone());
435 }
436 }
437 } else if let Some(items) = json_value.as_array() {
438 if items.is_empty() {
440 break;
441 }
442 all_items.extend(items.clone());
443 } else {
444 return Ok(response_text);
446 }
447
448 if let Some(page_obj) = json_value.get("page") {
450 page_info = Some(page_obj.clone());
451 if let (Some(current), Some(total)) = (
452 page_obj.get("number").and_then(|n| n.as_u64()),
453 page_obj.get("totalPages").and_then(|n| n.as_u64()),
454 ) {
455 if current + 1 >= total {
456 break; }
458 }
459 }
460 } else {
461 return Ok(response_text);
463 }
464
465 page += 1;
466
467 if page > 100 {
469 break;
470 }
471 }
472
473 let combined_response = if let Some(page_info) = page_info {
475 serde_json::json!({
477 "_embedded": {
478 "roles": all_items },
480 "page": page_info
481 })
482 } else {
483 serde_json::Value::Array(all_items)
485 };
486
487 Ok(combined_response.to_string())
488 }
489
490 pub async fn get_with_params(
501 &self,
502 endpoint: &str,
503 params: &[(&str, &str)],
504 ) -> Result<reqwest::Response, VeracodeError> {
505 let url = format!("{}{}", self.config.base_url, endpoint);
506 let mut request_url =
507 Url::parse(&url).map_err(|e| VeracodeError::InvalidConfig(e.to_string()))?;
508
509 if !params.is_empty() {
511 let mut query_pairs = request_url.query_pairs_mut();
512 for (key, value) in params {
513 query_pairs.append_pair(key, value);
514 }
515 }
516
517 let auth_header = self.generate_auth_header("GET", request_url.as_str())?;
518
519 let response = self
520 .client
521 .get(request_url)
522 .header("Authorization", auth_header)
523 .header("User-Agent", "Veracode Rust Client")
524 .send()
525 .await?;
526
527 Ok(response)
528 }
529
530 pub async fn post_form(
541 &self,
542 endpoint: &str,
543 params: &[(&str, &str)],
544 ) -> Result<reqwest::Response, VeracodeError> {
545 let url = format!("{}{}", self.config.base_url, endpoint);
546
547 let mut form_data = Vec::new();
549 for (key, value) in params {
550 form_data.push((key.to_string(), value.to_string()));
551 }
552
553 let auth_header = self.generate_auth_header("POST", &url)?;
554
555 let response = self
556 .client
557 .post(&url)
558 .header("Authorization", auth_header)
559 .header("User-Agent", "Veracode Rust Client")
560 .form(&form_data)
561 .send()
562 .await?;
563
564 Ok(response)
565 }
566
567 pub async fn upload_file_multipart(
581 &self,
582 endpoint: &str,
583 params: HashMap<&str, &str>,
584 file_field_name: &str,
585 filename: &str,
586 file_data: Vec<u8>,
587 ) -> Result<reqwest::Response, VeracodeError> {
588 let url = format!("{}{}", self.config.base_url, endpoint);
589
590 let mut form = multipart::Form::new();
592
593 for (key, value) in params {
595 form = form.text(key.to_string(), value.to_string());
596 }
597
598 let part = multipart::Part::bytes(file_data)
600 .file_name(filename.to_string())
601 .mime_str("application/octet-stream")
602 .map_err(|e| VeracodeError::InvalidConfig(e.to_string()))?;
603
604 form = form.part(file_field_name.to_string(), part);
605
606 let auth_header = self.generate_auth_header("POST", &url)?;
607
608 let response = self
609 .client
610 .post(&url)
611 .header("Authorization", auth_header)
612 .header("User-Agent", "Veracode Rust Client")
613 .multipart(form)
614 .send()
615 .await?;
616
617 Ok(response)
618 }
619
620 pub async fn upload_file_multipart_put(
634 &self,
635 url: &str,
636 file_field_name: &str,
637 filename: &str,
638 file_data: Vec<u8>,
639 additional_headers: Option<HashMap<&str, &str>>,
640 ) -> Result<reqwest::Response, VeracodeError> {
641 let part = multipart::Part::bytes(file_data)
643 .file_name(filename.to_string())
644 .mime_str("application/octet-stream")
645 .map_err(|e| VeracodeError::InvalidConfig(e.to_string()))?;
646
647 let form = multipart::Form::new().part(file_field_name.to_string(), part);
648
649 let auth_header = self.generate_auth_header("PUT", url)?;
650
651 let mut request = self
652 .client
653 .put(url)
654 .header("Authorization", auth_header)
655 .header("User-Agent", "Veracode Rust Client")
656 .multipart(form);
657
658 if let Some(headers) = additional_headers {
660 for (key, value) in headers {
661 request = request.header(key, value);
662 }
663 }
664
665 let response = request.send().await?;
666 Ok(response)
667 }
668
669 pub async fn upload_file_with_query_params(
686 &self,
687 endpoint: &str,
688 query_params: &[(&str, &str)],
689 file_field_name: &str,
690 filename: &str,
691 file_data: Vec<u8>,
692 ) -> Result<reqwest::Response, VeracodeError> {
693 let mut url = format!("{}{}", self.config.base_url, endpoint);
695
696 if !query_params.is_empty() {
697 url.push('?');
698 let encoded_params: Vec<String> = query_params
699 .iter()
700 .map(|(key, value)| {
701 format!(
702 "{}={}",
703 urlencoding::encode(key),
704 urlencoding::encode(value)
705 )
706 })
707 .collect();
708 url.push_str(&encoded_params.join("&"));
709 }
710
711 let part = multipart::Part::bytes(file_data)
713 .file_name(filename.to_string())
714 .mime_str("application/octet-stream")
715 .map_err(|e| VeracodeError::InvalidConfig(e.to_string()))?;
716
717 let form = multipart::Form::new().part(file_field_name.to_string(), part);
718
719 let auth_header = self.generate_auth_header("POST", &url)?;
720
721 let response = self
722 .client
723 .post(&url)
724 .header("Authorization", auth_header)
725 .header("User-Agent", "Veracode Rust Client")
726 .multipart(form)
727 .send()
728 .await?;
729
730 Ok(response)
731 }
732
733 pub async fn post_with_query_params(
747 &self,
748 endpoint: &str,
749 query_params: &[(&str, &str)],
750 ) -> Result<reqwest::Response, VeracodeError> {
751 let mut url = format!("{}{}", self.config.base_url, endpoint);
753
754 if !query_params.is_empty() {
755 url.push('?');
756 let encoded_params: Vec<String> = query_params
757 .iter()
758 .map(|(key, value)| {
759 format!(
760 "{}={}",
761 urlencoding::encode(key),
762 urlencoding::encode(value)
763 )
764 })
765 .collect();
766 url.push_str(&encoded_params.join("&"));
767 }
768
769 let auth_header = self.generate_auth_header("POST", &url)?;
770
771 let response = self
772 .client
773 .post(&url)
774 .header("Authorization", auth_header)
775 .header("User-Agent", "Veracode Rust Client")
776 .send()
777 .await?;
778
779 Ok(response)
780 }
781
782 pub async fn get_with_query_params(
796 &self,
797 endpoint: &str,
798 query_params: &[(&str, &str)],
799 ) -> Result<reqwest::Response, VeracodeError> {
800 let mut url = format!("{}{}", self.config.base_url, endpoint);
802
803 if !query_params.is_empty() {
804 url.push('?');
805 let encoded_params: Vec<String> = query_params
806 .iter()
807 .map(|(key, value)| {
808 format!(
809 "{}={}",
810 urlencoding::encode(key),
811 urlencoding::encode(value)
812 )
813 })
814 .collect();
815 url.push_str(&encoded_params.join("&"));
816 }
817
818 let auth_header = self.generate_auth_header("GET", &url)?;
819
820 let response = self
821 .client
822 .get(&url)
823 .header("Authorization", auth_header)
824 .header("User-Agent", "Veracode Rust Client")
825 .send()
826 .await?;
827
828 Ok(response)
829 }
830
831 pub async fn upload_large_file_chunked<F>(
848 &self,
849 endpoint: &str,
850 query_params: &[(&str, &str)],
851 file_path: &str,
852 content_type: Option<&str>,
853 progress_callback: Option<F>,
854 ) -> Result<reqwest::Response, VeracodeError>
855 where
856 F: Fn(u64, u64, f64) + Send + Sync,
857 {
858 use std::fs::File;
859 use std::io::{Read, Seek, SeekFrom};
860
861 let mut url = format!("{}{}", self.config.base_url, endpoint);
863
864 if !query_params.is_empty() {
865 url.push('?');
866 let encoded_params: Vec<String> = query_params
867 .iter()
868 .map(|(key, value)| {
869 format!(
870 "{}={}",
871 urlencoding::encode(key),
872 urlencoding::encode(value)
873 )
874 })
875 .collect();
876 url.push_str(&encoded_params.join("&"));
877 }
878
879 let mut file = File::open(file_path)
881 .map_err(|e| VeracodeError::InvalidConfig(format!("Failed to open file: {e}")))?;
882
883 let file_size = file
884 .metadata()
885 .map_err(|e| VeracodeError::InvalidConfig(format!("Failed to get file size: {e}")))?
886 .len();
887
888 const MAX_FILE_SIZE: u64 = 2 * 1024 * 1024 * 1024; if file_size > MAX_FILE_SIZE {
891 return Err(VeracodeError::InvalidConfig(format!(
892 "File size ({file_size} bytes) exceeds maximum limit of {MAX_FILE_SIZE} bytes"
893 )));
894 }
895
896 file.seek(SeekFrom::Start(0))
898 .map_err(|e| VeracodeError::InvalidConfig(format!("Failed to seek file: {e}")))?;
899
900 let mut file_data = Vec::with_capacity(file_size as usize);
901 file.read_to_end(&mut file_data)
902 .map_err(|e| VeracodeError::InvalidConfig(format!("Failed to read file: {e}")))?;
903
904 let auth_header = self.generate_auth_header("POST", &url)?;
906
907 let request = self
909 .client
910 .post(&url)
911 .header("Authorization", auth_header)
912 .header("User-Agent", "Veracode Rust Client")
913 .header(
914 "Content-Type",
915 content_type.unwrap_or("binary/octet-stream"),
916 )
917 .header("Content-Length", file_size.to_string())
918 .body(file_data);
919
920 if let Some(callback) = progress_callback {
922 callback(file_size, file_size, 100.0);
923 }
924
925 let response = request.send().await?;
926 Ok(response)
927 }
928
929 pub async fn upload_file_binary(
945 &self,
946 endpoint: &str,
947 query_params: &[(&str, &str)],
948 file_data: Vec<u8>,
949 content_type: &str,
950 ) -> Result<reqwest::Response, VeracodeError> {
951 let mut url = format!("{}{}", self.config.base_url, endpoint);
953
954 if !query_params.is_empty() {
955 url.push('?');
956 let encoded_params: Vec<String> = query_params
957 .iter()
958 .map(|(key, value)| {
959 format!(
960 "{}={}",
961 urlencoding::encode(key),
962 urlencoding::encode(value)
963 )
964 })
965 .collect();
966 url.push_str(&encoded_params.join("&"));
967 }
968
969 let auth_header = self.generate_auth_header("POST", &url)?;
970
971 let response = self
972 .client
973 .post(&url)
974 .header("Authorization", auth_header)
975 .header("User-Agent", "Veracode Rust Client")
976 .header("Content-Type", content_type)
977 .header("Content-Length", file_data.len().to_string())
978 .body(file_data)
979 .send()
980 .await?;
981
982 Ok(response)
983 }
984}