veracode_platform/
client.rs

1//! Core Veracode API client implementation.
2//!
3//! This module contains the foundational client for making authenticated requests
4//! to the Veracode API, including HMAC authentication and HTTP request handling.
5
6use 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
17// Type aliases for HMAC
18type HmacSha256 = Hmac<Sha256>;
19
20/// Core Veracode API client.
21///
22/// This struct provides the foundational HTTP client with HMAC authentication
23/// for making requests to any Veracode API endpoint.
24#[derive(Clone)]
25pub struct VeracodeClient {
26    config: VeracodeConfig,
27    client: Client,
28}
29
30impl VeracodeClient {
31    /// Create a new Veracode API client.
32    ///
33    /// # Arguments
34    ///
35    /// * `config` - Configuration containing API credentials and settings
36    ///
37    /// # Returns
38    ///
39    /// A new `VeracodeClient` instance ready to make API calls.
40    pub fn new(config: VeracodeConfig) -> Result<Self, VeracodeError> {
41        let mut client_builder = Client::builder();
42
43        // Use the certificate validation setting from config
44        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    /// Get the base URL for API requests.
55    pub fn base_url(&self) -> &str {
56        &self.config.base_url
57    }
58
59    /// Get access to the configuration
60    pub fn config(&self) -> &VeracodeConfig {
61        &self.config
62    }
63
64    /// Get access to the underlying reqwest client
65    pub fn client(&self) -> &Client {
66        &self.client
67    }
68
69    /// Generate HMAC signature for authentication based on official Veracode JavaScript implementation
70    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        // Based on the official Veracode JavaScript implementation:
88        // var data = `id=${id}&host=${host}&url=${url}&method=${method}`;
89        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        // Convert hex strings to bytes
101        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        // Step 1: HMAC(nonce, key)
109        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        // Step 2: HMAC(timestamp, hashed_nonce)
115        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        // Step 3: HMAC(ver_str, hashed_timestamp)
121        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        // Step 4: HMAC(data, hashed_ver_str)
127        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        // Return the hex-encoded signature (lowercase)
133        Ok(hex::encode(signature).to_lowercase())
134    }
135
136    /// Generate authorization header for HMAC authentication
137    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; // Use milliseconds like JavaScript
142
143        // Generate a 16-byte random nonce and convert to hex string
144        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    /// Make a GET request to the specified endpoint.
159    ///
160    /// # Arguments
161    ///
162    /// * `endpoint` - The API endpoint path (e.g., "/appsec/v1/applications")
163    /// * `query_params` - Optional query parameters as key-value pairs
164    ///
165    /// # Returns
166    ///
167    /// A `Result` containing the HTTP response.
168    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                    &params
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    /// Make a POST request to the specified endpoint.
202    ///
203    /// # Arguments
204    ///
205    /// * `endpoint` - The API endpoint path (e.g., "/appsec/v1/applications")
206    /// * `body` - Optional request body that implements Serialize
207    ///
208    /// # Returns
209    ///
210    /// A `Result` containing the HTTP response.
211    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    /// Make a PUT request to the specified endpoint.
234    ///
235    /// # Arguments
236    ///
237    /// * `endpoint` - The API endpoint path (e.g., "/appsec/v1/applications/guid")
238    /// * `body` - Optional request body that implements Serialize
239    ///
240    /// # Returns
241    ///
242    /// A `Result` containing the HTTP response.
243    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    /// Make a DELETE request to the specified endpoint.
266    ///
267    /// # Arguments
268    ///
269    /// * `endpoint` - The API endpoint path (e.g., "/appsec/v1/applications/guid")
270    ///
271    /// # Returns
272    ///
273    /// A `Result` containing the HTTP response.
274    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    /// Helper method to handle common response processing.
290    ///
291    /// Checks if the response is successful and returns an error if not.
292    ///
293    /// # Arguments
294    ///
295    /// * `response` - The HTTP response to check
296    ///
297    /// # Returns
298    ///
299    /// A `Result` containing the response if successful, or an error if not.
300    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    /// Make a GET request with full URL construction and query parameter handling.
314    ///
315    /// This is a higher-level method that builds the full URL and handles query parameters.
316    ///
317    /// # Arguments
318    ///
319    /// * `endpoint` - The API endpoint path
320    /// * `query_params` - Optional query parameters
321    ///
322    /// # Returns
323    ///
324    /// A `Result` containing the HTTP response, pre-processed for success/failure.
325    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    /// Make a POST request with automatic response handling.
336    ///
337    /// # Arguments
338    ///
339    /// * `endpoint` - The API endpoint path
340    /// * `body` - Optional request body
341    ///
342    /// # Returns
343    ///
344    /// A `Result` containing the HTTP response, pre-processed for success/failure.
345    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    /// Make a PUT request with automatic response handling.
355    ///
356    /// # Arguments
357    ///
358    /// * `endpoint` - The API endpoint path
359    /// * `body` - Optional request body
360    ///
361    /// # Returns
362    ///
363    /// A `Result` containing the HTTP response, pre-processed for success/failure.
364    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    /// Make a DELETE request with automatic response handling.
374    ///
375    /// # Arguments
376    ///
377    /// * `endpoint` - The API endpoint path
378    ///
379    /// # Returns
380    ///
381    /// A `Result` containing the HTTP response, pre-processed for success/failure.
382    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    /// Make paginated GET requests to collect all results.
391    ///
392    /// This method automatically handles pagination by making multiple requests
393    /// and combining all results into a single response.
394    ///
395    /// # Arguments
396    ///
397    /// * `endpoint` - The API endpoint path
398    /// * `base_query_params` - Base query parameters (non-pagination)
399    /// * `page_size` - Number of items per page (default: 500)
400    ///
401    /// # Returns
402    ///
403    /// A `Result` containing all paginated results as a single response body string.
404    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            // Try to parse as JSON to extract items and pagination info
424            if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(&response_text) {
425                // Handle embedded response format
426                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; // No more items
433                            }
434                            all_items.extend(items.clone());
435                        }
436                    }
437                } else if let Some(items) = json_value.as_array() {
438                    // Handle direct array response
439                    if items.is_empty() {
440                        break;
441                    }
442                    all_items.extend(items.clone());
443                } else {
444                    // Single page response, return as-is
445                    return Ok(response_text);
446                }
447
448                // Check pagination info
449                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; // Last page reached
457                        }
458                    }
459                }
460            } else {
461                // Not JSON or parsing failed, return single response
462                return Ok(response_text);
463            }
464
465            page += 1;
466
467            // Safety check to prevent infinite loops
468            if page > 100 {
469                break;
470            }
471        }
472
473        // Reconstruct response with all items
474        let combined_response = if let Some(page_info) = page_info {
475            // Use embedded format
476            serde_json::json!({
477                "_embedded": {
478                    "roles": all_items // This key might need to be dynamic
479                },
480                "page": page_info
481            })
482        } else {
483            // Use direct array format
484            serde_json::Value::Array(all_items)
485        };
486
487        Ok(combined_response.to_string())
488    }
489
490    /// Make a GET request with query parameters
491    ///
492    /// # Arguments
493    ///
494    /// * `endpoint` - The API endpoint to call
495    /// * `params` - Query parameters as a slice of tuples
496    ///
497    /// # Returns
498    ///
499    /// A `Result` containing the response or an error.
500    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        // Add query parameters
510        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    /// Make a POST request with form data
531    ///
532    /// # Arguments
533    ///
534    /// * `endpoint` - The API endpoint to call
535    /// * `params` - Form parameters as a slice of tuples
536    ///
537    /// # Returns
538    ///
539    /// A `Result` containing the response or an error.
540    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        // Build form data
548        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    /// Upload a file using multipart form data
568    ///
569    /// # Arguments
570    ///
571    /// * `endpoint` - The API endpoint to call
572    /// * `params` - Additional form parameters
573    /// * `file_field_name` - Name of the file field
574    /// * `filename` - Name of the file
575    /// * `file_data` - File data as bytes
576    ///
577    /// # Returns
578    ///
579    /// A `Result` containing the response or an error.
580    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        // Build multipart form
591        let mut form = multipart::Form::new();
592
593        // Add regular form fields
594        for (key, value) in params {
595            form = form.text(key.to_string(), value.to_string());
596        }
597
598        // Add file
599        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    /// Upload a file using multipart form data with PUT method (for pipeline scans)
621    ///
622    /// # Arguments
623    ///
624    /// * `url` - The full URL to upload to
625    /// * `file_field_name` - Name of the file field
626    /// * `filename` - Name of the file
627    /// * `file_data` - File data as bytes
628    /// * `additional_headers` - Additional headers to include
629    ///
630    /// # Returns
631    ///
632    /// A `Result` containing the response or an error.
633    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        // Build multipart form
642        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        // Add any additional headers
659        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    /// Upload a file with query parameters (like Java implementation)
670    ///
671    /// This method mimics the Java API wrapper's approach where parameters
672    /// are added to the query string and the file is uploaded separately.
673    ///
674    /// # Arguments
675    ///
676    /// * `endpoint` - The API endpoint to call
677    /// * `query_params` - Query parameters as key-value pairs
678    /// * `file_field_name` - Name of the file field
679    /// * `filename` - Name of the file
680    /// * `file_data` - File data as bytes
681    ///
682    /// # Returns
683    ///
684    /// A `Result` containing the response or an error.
685    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        // Build URL with query parameters (URL encoded)
694        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        // Build multipart form with only the file
712        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    /// Make a POST request with query parameters (like Java implementation for XML API)
734    ///
735    /// This method mimics the Java API wrapper's approach for POST operations
736    /// where parameters are added to the query string rather than form data.
737    ///
738    /// # Arguments
739    ///
740    /// * `endpoint` - The API endpoint to call
741    /// * `query_params` - Query parameters as key-value pairs
742    ///
743    /// # Returns
744    ///
745    /// A `Result` containing the response or an error.
746    pub async fn post_with_query_params(
747        &self,
748        endpoint: &str,
749        query_params: &[(&str, &str)],
750    ) -> Result<reqwest::Response, VeracodeError> {
751        // Build URL with query parameters (URL encoded)
752        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    /// Make a GET request with query parameters (like Java implementation for XML API)
783    ///
784    /// This method mimics the Java API wrapper's approach for GET operations
785    /// where parameters are added to the query string.
786    ///
787    /// # Arguments
788    ///
789    /// * `endpoint` - The API endpoint to call
790    /// * `query_params` - Query parameters as key-value pairs
791    ///
792    /// # Returns
793    ///
794    /// A `Result` containing the response or an error.
795    pub async fn get_with_query_params(
796        &self,
797        endpoint: &str,
798        query_params: &[(&str, &str)],
799    ) -> Result<reqwest::Response, VeracodeError> {
800        // Build URL with query parameters (URL encoded)
801        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    /// Upload a large file using chunked streaming (for uploadlargefile.do)
832    ///
833    /// This method implements chunked upload functionality similar to the Java API wrapper.
834    /// It uploads files in chunks and provides progress tracking capabilities.
835    ///
836    /// # Arguments
837    ///
838    /// * `endpoint` - The API endpoint to call  
839    /// * `query_params` - Query parameters as key-value pairs
840    /// * `file_path` - Path to the file to upload
841    /// * `content_type` - Content type for the file (default: binary/octet-stream)
842    /// * `progress_callback` - Optional callback for progress tracking
843    ///
844    /// # Returns
845    ///
846    /// A `Result` containing the response or an error.
847    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        // Build URL with query parameters
862        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        // Open file and get size
880        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        // Check file size limit (2GB for uploadlargefile.do)
889        const MAX_FILE_SIZE: u64 = 2 * 1024 * 1024 * 1024; // 2GB
890        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        // Read entire file for now (can be optimized to streaming later)
897        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        // Generate auth header
905        let auth_header = self.generate_auth_header("POST", &url)?;
906
907        // Create request with streaming body
908        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        // Track progress if callback provided
921        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    /// Upload a file with binary data (optimized for uploadlargefile.do)
930    ///
931    /// This method uploads a file as raw binary data without multipart encoding,
932    /// which is the expected format for the uploadlargefile.do endpoint.
933    ///
934    /// # Arguments
935    ///
936    /// * `endpoint` - The API endpoint to call
937    /// * `query_params` - Query parameters as key-value pairs  
938    /// * `file_data` - File data as bytes
939    /// * `content_type` - Content type for the file
940    ///
941    /// # Returns
942    ///
943    /// A `Result` containing the response or an error.
944    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        // Build URL with query parameters
952        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}