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 std::time::{SystemTime, UNIX_EPOCH};
7use std::collections::HashMap;
8use hmac::{Hmac, Mac};
9use sha2::Sha256;
10use hex;
11use reqwest::{Client, multipart};
12use url::Url;
13use serde::Serialize;
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(&self, method: &str, url: &str, timestamp: u64, nonce: &str) -> Result<String, VeracodeError> {
71        let url_parsed = Url::parse(url)
72            .map_err(|_| VeracodeError::Authentication("Invalid URL".to_string()))?;
73        
74        let path_and_query = match url_parsed.query() {
75            Some(query) => format!("{}?{}", url_parsed.path(), query),
76            None => url_parsed.path().to_string(),
77        };
78
79        let host = url_parsed.host_str().unwrap_or("");
80
81        // Based on the official Veracode JavaScript implementation:
82        // var data = `id=${id}&host=${host}&url=${url}&method=${method}`;
83        let data = format!("id={}&host={}&url={}&method={}", 
84                          self.config.api_id, 
85                          host, 
86                          path_and_query, 
87                          method);
88
89        let timestamp_str = timestamp.to_string();
90        let ver_str = "vcode_request_version_1";
91
92        // Convert hex strings to bytes
93        let key_bytes = hex::decode(&self.config.api_key)
94            .map_err(|_| VeracodeError::Authentication("Invalid API key format - must be hex string".to_string()))?;
95        
96        let nonce_bytes = hex::decode(nonce)
97            .map_err(|_| VeracodeError::Authentication("Invalid nonce format".to_string()))?;
98
99        // Step 1: HMAC(nonce, key)
100        let mut mac1 = HmacSha256::new_from_slice(&key_bytes)
101            .map_err(|_| VeracodeError::Authentication("Failed to create HMAC".to_string()))?;
102        mac1.update(&nonce_bytes);
103        let hashed_nonce = mac1.finalize().into_bytes();
104
105        // Step 2: HMAC(timestamp, hashed_nonce)
106        let mut mac2 = HmacSha256::new_from_slice(&hashed_nonce)
107            .map_err(|_| VeracodeError::Authentication("Failed to create HMAC".to_string()))?;
108        mac2.update(timestamp_str.as_bytes());
109        let hashed_timestamp = mac2.finalize().into_bytes();
110
111        // Step 3: HMAC(ver_str, hashed_timestamp)
112        let mut mac3 = HmacSha256::new_from_slice(&hashed_timestamp)
113            .map_err(|_| VeracodeError::Authentication("Failed to create HMAC".to_string()))?;
114        mac3.update(ver_str.as_bytes());
115        let hashed_ver_str = mac3.finalize().into_bytes();
116
117        // Step 4: HMAC(data, hashed_ver_str)
118        let mut mac4 = HmacSha256::new_from_slice(&hashed_ver_str)
119            .map_err(|_| VeracodeError::Authentication("Failed to create HMAC".to_string()))?;
120        mac4.update(data.as_bytes());
121        let signature = mac4.finalize().into_bytes();
122
123        // Return the hex-encoded signature (lowercase)
124        Ok(hex::encode(signature).to_lowercase())
125    }
126
127    /// Generate authorization header for HMAC authentication
128    pub fn generate_auth_header(&self, method: &str, url: &str) -> Result<String, VeracodeError> {
129        let timestamp = SystemTime::now()
130            .duration_since(UNIX_EPOCH)
131            .unwrap()
132            .as_millis() as u64; // Use milliseconds like JavaScript
133
134        // Generate a 16-byte random nonce and convert to hex string
135        let nonce_bytes: [u8; 16] = rand::random();
136        let nonce = hex::encode(nonce_bytes);
137        
138        let signature = self.generate_hmac_signature(method, url, timestamp, &nonce)?;
139
140        Ok(format!("VERACODE-HMAC-SHA-256 id={},ts={},nonce={},sig={}",
141                   self.config.api_id, timestamp, nonce, signature))
142    }
143
144    /// Make a GET request to the specified endpoint.
145    ///
146    /// # Arguments
147    ///
148    /// * `endpoint` - The API endpoint path (e.g., "/appsec/v1/applications")
149    /// * `query_params` - Optional query parameters as key-value pairs
150    ///
151    /// # Returns
152    ///
153    /// A `Result` containing the HTTP response.
154    pub async fn get(&self, endpoint: &str, query_params: Option<&[(String, String)]>) -> Result<reqwest::Response, VeracodeError> {
155        let mut url = format!("{}{}", self.config.base_url, endpoint);
156        
157        if let Some(params) = query_params {
158            if !params.is_empty() {
159                url.push('?');
160                url.push_str(&params.iter()
161                    .map(|(k, v)| format!("{k}={v}"))
162                    .collect::<Vec<_>>()
163                    .join("&"));
164            }
165        }
166
167        let auth_header = self.generate_auth_header("GET", &url)?;
168        
169        let response = self.client
170            .get(&url)
171            .header("Authorization", auth_header)
172            .header("Content-Type", "application/json")
173            .send()
174            .await?;
175
176        Ok(response)
177    }
178
179    /// Make a POST request to the specified endpoint.
180    ///
181    /// # Arguments
182    ///
183    /// * `endpoint` - The API endpoint path (e.g., "/appsec/v1/applications")
184    /// * `body` - Optional request body that implements Serialize
185    ///
186    /// # Returns
187    ///
188    /// A `Result` containing the HTTP response.
189    pub async fn post<T: Serialize>(&self, endpoint: &str, body: Option<&T>) -> Result<reqwest::Response, VeracodeError> {
190        let url = format!("{}{}", self.config.base_url, endpoint);
191        let auth_header = self.generate_auth_header("POST", &url)?;
192        
193        let mut request = self.client
194            .post(&url)
195            .header("Authorization", auth_header)
196            .header("Content-Type", "application/json");
197
198        if let Some(body) = body {
199            request = request.json(body);
200        }
201
202        let response = request.send().await?;
203        Ok(response)
204    }
205
206    /// Make a PUT request to the specified endpoint.
207    ///
208    /// # Arguments
209    ///
210    /// * `endpoint` - The API endpoint path (e.g., "/appsec/v1/applications/guid")
211    /// * `body` - Optional request body that implements Serialize
212    ///
213    /// # Returns
214    ///
215    /// A `Result` containing the HTTP response.
216    pub async fn put<T: Serialize>(&self, endpoint: &str, body: Option<&T>) -> Result<reqwest::Response, VeracodeError> {
217        let url = format!("{}{}", self.config.base_url, endpoint);
218        let auth_header = self.generate_auth_header("PUT", &url)?;
219        
220        let mut request = self.client
221            .put(&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 DELETE request to the specified endpoint.
234    ///
235    /// # Arguments
236    ///
237    /// * `endpoint` - The API endpoint path (e.g., "/appsec/v1/applications/guid")
238    ///
239    /// # Returns
240    ///
241    /// A `Result` containing the HTTP response.
242    pub async fn delete(&self, endpoint: &str) -> Result<reqwest::Response, VeracodeError> {
243        let url = format!("{}{}", self.config.base_url, endpoint);
244        let auth_header = self.generate_auth_header("DELETE", &url)?;
245        
246        let response = self.client
247            .delete(&url)
248            .header("Authorization", auth_header)
249            .header("Content-Type", "application/json")
250            .send()
251            .await?;
252
253        Ok(response)
254    }
255
256    /// Helper method to handle common response processing.
257    ///
258    /// Checks if the response is successful and returns an error if not.
259    ///
260    /// # Arguments
261    ///
262    /// * `response` - The HTTP response to check
263    ///
264    /// # Returns
265    ///
266    /// A `Result` containing the response if successful, or an error if not.
267    pub async fn handle_response(response: reqwest::Response) -> Result<reqwest::Response, VeracodeError> {
268        if !response.status().is_success() {
269            let status = response.status();
270            let error_text = response.text().await?;
271            return Err(VeracodeError::InvalidResponse(
272                format!("HTTP {status}: {error_text}")
273            ));
274        }
275        Ok(response)
276    }
277
278    /// Make a GET request with full URL construction and query parameter handling.
279    ///
280    /// This is a higher-level method that builds the full URL and handles query parameters.
281    ///
282    /// # Arguments
283    ///
284    /// * `endpoint` - The API endpoint path
285    /// * `query_params` - Optional query parameters
286    ///
287    /// # Returns
288    ///
289    /// A `Result` containing the HTTP response, pre-processed for success/failure.
290    pub async fn get_with_query(&self, endpoint: &str, query_params: Option<Vec<(String, String)>>) -> Result<reqwest::Response, VeracodeError> {
291        let query_slice = query_params.as_deref();
292        let response = self.get(endpoint, query_slice).await?;
293        Self::handle_response(response).await
294    }
295
296    /// Make a POST request with automatic response handling.
297    ///
298    /// # Arguments
299    ///
300    /// * `endpoint` - The API endpoint path
301    /// * `body` - Optional request body
302    ///
303    /// # Returns
304    ///
305    /// A `Result` containing the HTTP response, pre-processed for success/failure.
306    pub async fn post_with_response<T: Serialize>(&self, endpoint: &str, body: Option<&T>) -> Result<reqwest::Response, VeracodeError> {
307        let response = self.post(endpoint, body).await?;
308        Self::handle_response(response).await
309    }
310
311    /// Make a PUT request with automatic response handling.
312    ///
313    /// # Arguments
314    ///
315    /// * `endpoint` - The API endpoint path
316    /// * `body` - Optional request body
317    ///
318    /// # Returns
319    ///
320    /// A `Result` containing the HTTP response, pre-processed for success/failure.
321    pub async fn put_with_response<T: Serialize>(&self, endpoint: &str, body: Option<&T>) -> Result<reqwest::Response, VeracodeError> {
322        let response = self.put(endpoint, body).await?;
323        Self::handle_response(response).await
324    }
325
326    /// Make a DELETE request with automatic response handling.
327    ///
328    /// # Arguments
329    ///
330    /// * `endpoint` - The API endpoint path
331    ///
332    /// # Returns
333    ///
334    /// A `Result` containing the HTTP response, pre-processed for success/failure.
335    pub async fn delete_with_response(&self, endpoint: &str) -> Result<reqwest::Response, VeracodeError> {
336        let response = self.delete(endpoint).await?;
337        Self::handle_response(response).await
338    }
339
340    /// Make paginated GET requests to collect all results.
341    ///
342    /// This method automatically handles pagination by making multiple requests
343    /// and combining all results into a single response.
344    ///
345    /// # Arguments
346    ///
347    /// * `endpoint` - The API endpoint path
348    /// * `base_query_params` - Base query parameters (non-pagination)
349    /// * `page_size` - Number of items per page (default: 500)
350    ///
351    /// # Returns
352    ///
353    /// A `Result` containing all paginated results as a single response body string.
354    pub async fn get_paginated(&self, endpoint: &str, base_query_params: Option<Vec<(String, String)>>, page_size: Option<u32>) -> Result<String, VeracodeError> {
355        let size = page_size.unwrap_or(500);
356        let mut page = 0;
357        let mut all_items = Vec::new();
358        let mut page_info = None;
359
360        loop {
361            let mut query_params = base_query_params.clone().unwrap_or_default();
362            query_params.push(("page".to_string(), page.to_string()));
363            query_params.push(("size".to_string(), size.to_string()));
364
365            let response = self.get_with_query(endpoint, Some(query_params)).await?;
366            let response_text = response.text().await?;
367
368            // Try to parse as JSON to extract items and pagination info
369            if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(&response_text) {
370                // Handle embedded response format
371                if let Some(embedded) = json_value.get("_embedded") {
372                    if let Some(items_array) = embedded.as_object().and_then(|obj| obj.values().next()) {
373                        if let Some(items) = items_array.as_array() {
374                            if items.is_empty() {
375                                break; // No more items
376                            }
377                            all_items.extend(items.clone());
378                        }
379                    }
380                } else if let Some(items) = json_value.as_array() {
381                    // Handle direct array response
382                    if items.is_empty() {
383                        break;
384                    }
385                    all_items.extend(items.clone());
386                } else {
387                    // Single page response, return as-is
388                    return Ok(response_text);
389                }
390
391                // Check pagination info
392                if let Some(page_obj) = json_value.get("page") {
393                    page_info = Some(page_obj.clone());
394                    if let (Some(current), Some(total)) = (
395                        page_obj.get("number").and_then(|n| n.as_u64()),
396                        page_obj.get("totalPages").and_then(|n| n.as_u64())
397                    ) {
398                        if current + 1 >= total {
399                            break; // Last page reached
400                        }
401                    }
402                }
403            } else {
404                // Not JSON or parsing failed, return single response
405                return Ok(response_text);
406            }
407
408            page += 1;
409            
410            // Safety check to prevent infinite loops
411            if page > 100 {
412                break;
413            }
414        }
415
416        // Reconstruct response with all items
417        let combined_response = if let Some(page_info) = page_info {
418            // Use embedded format
419            serde_json::json!({
420                "_embedded": {
421                    "roles": all_items // This key might need to be dynamic
422                },
423                "page": page_info
424            })
425        } else {
426            // Use direct array format
427            serde_json::Value::Array(all_items)
428        };
429
430        Ok(combined_response.to_string())
431    }
432
433    /// Make a GET request with query parameters
434    ///
435    /// # Arguments
436    ///
437    /// * `endpoint` - The API endpoint to call
438    /// * `params` - Query parameters as a slice of tuples
439    ///
440    /// # Returns
441    ///
442    /// A `Result` containing the response or an error.
443    pub async fn get_with_params(&self, endpoint: &str, params: &[(&str, &str)]) -> Result<reqwest::Response, VeracodeError> {
444        let url = format!("{}{}", self.config.base_url, endpoint);
445        let mut request_url = Url::parse(&url).map_err(|e| VeracodeError::InvalidConfig(e.to_string()))?;
446        
447        // Add query parameters
448        if !params.is_empty() {
449            let mut query_pairs = request_url.query_pairs_mut();
450            for (key, value) in params {
451                query_pairs.append_pair(key, value);
452            }
453        }
454
455        let auth_header = self.generate_auth_header("GET", request_url.as_str())?;
456        
457        let response = self.client
458            .get(request_url)
459            .header("Authorization", auth_header)
460            .header("User-Agent", "Veracode Rust Client")
461            .send()
462            .await?;
463
464        Ok(response)
465    }
466
467    /// Make a POST request with form data
468    ///
469    /// # Arguments
470    ///
471    /// * `endpoint` - The API endpoint to call
472    /// * `params` - Form parameters as a slice of tuples
473    ///
474    /// # Returns
475    ///
476    /// A `Result` containing the response or an error.
477    pub async fn post_form(&self, endpoint: &str, params: &[(&str, &str)]) -> Result<reqwest::Response, VeracodeError> {
478        let url = format!("{}{}", self.config.base_url, endpoint);
479        
480        // Build form data
481        let mut form_data = Vec::new();
482        for (key, value) in params {
483            form_data.push((key.to_string(), value.to_string()));
484        }
485
486        let auth_header = self.generate_auth_header("POST", &url)?;
487        
488        let response = self.client
489            .post(&url)
490            .header("Authorization", auth_header)
491            .header("User-Agent", "Veracode Rust Client")
492            .form(&form_data)
493            .send()
494            .await?;
495
496        Ok(response)
497    }
498
499    /// Upload a file using multipart form data
500    ///
501    /// # Arguments
502    ///
503    /// * `endpoint` - The API endpoint to call
504    /// * `params` - Additional form parameters
505    /// * `file_field_name` - Name of the file field
506    /// * `filename` - Name of the file
507    /// * `file_data` - File data as bytes
508    ///
509    /// # Returns
510    ///
511    /// A `Result` containing the response or an error.
512    pub async fn upload_file_multipart(
513        &self,
514        endpoint: &str,
515        params: HashMap<&str, &str>,
516        file_field_name: &str,
517        filename: &str,
518        file_data: Vec<u8>,
519    ) -> Result<reqwest::Response, VeracodeError> {
520        let url = format!("{}{}", self.config.base_url, endpoint);
521        
522        // Build multipart form
523        let mut form = multipart::Form::new();
524        
525        // Add regular form fields
526        for (key, value) in params {
527            form = form.text(key.to_string(), value.to_string());
528        }
529        
530        // Add file
531        let part = multipart::Part::bytes(file_data)
532            .file_name(filename.to_string())
533            .mime_str("application/octet-stream")
534            .map_err(|e| VeracodeError::InvalidConfig(e.to_string()))?;
535        
536        form = form.part(file_field_name.to_string(), part);
537
538        let auth_header = self.generate_auth_header("POST", &url)?;
539        
540        let response = self.client
541            .post(&url)
542            .header("Authorization", auth_header)
543            .header("User-Agent", "Veracode Rust Client")
544            .multipart(form)
545            .send()
546            .await?;
547
548        Ok(response)
549    }
550
551    /// Upload a file using multipart form data with PUT method (for pipeline scans)
552    ///
553    /// # Arguments
554    ///
555    /// * `url` - The full URL to upload to
556    /// * `file_field_name` - Name of the file field
557    /// * `filename` - Name of the file
558    /// * `file_data` - File data as bytes
559    /// * `additional_headers` - Additional headers to include
560    ///
561    /// # Returns
562    ///
563    /// A `Result` containing the response or an error.
564    pub async fn upload_file_multipart_put(
565        &self,
566        url: &str,
567        file_field_name: &str,
568        filename: &str,
569        file_data: Vec<u8>,
570        additional_headers: Option<HashMap<&str, &str>>,
571    ) -> Result<reqwest::Response, VeracodeError> {
572        // Build multipart form
573        let part = multipart::Part::bytes(file_data)
574            .file_name(filename.to_string())
575            .mime_str("application/octet-stream")
576            .map_err(|e| VeracodeError::InvalidConfig(e.to_string()))?;
577        
578        let form = multipart::Form::new().part(file_field_name.to_string(), part);
579
580        let auth_header = self.generate_auth_header("PUT", url)?;
581        
582        let mut request = self.client
583            .put(url)
584            .header("Authorization", auth_header)
585            .header("User-Agent", "Veracode Rust Client")
586            .multipart(form);
587
588        // Add any additional headers
589        if let Some(headers) = additional_headers {
590            for (key, value) in headers {
591                request = request.header(key, value);
592            }
593        }
594
595        let response = request.send().await?;
596        Ok(response)
597    }
598
599    /// Upload a file with query parameters (like Java implementation)
600    ///
601    /// This method mimics the Java API wrapper's approach where parameters
602    /// are added to the query string and the file is uploaded separately.
603    ///
604    /// # Arguments
605    ///
606    /// * `endpoint` - The API endpoint to call
607    /// * `query_params` - Query parameters as key-value pairs
608    /// * `file_field_name` - Name of the file field
609    /// * `filename` - Name of the file
610    /// * `file_data` - File data as bytes
611    ///
612    /// # Returns
613    ///
614    /// A `Result` containing the response or an error.
615    pub async fn upload_file_with_query_params(
616        &self,
617        endpoint: &str,
618        query_params: &[(&str, &str)],
619        file_field_name: &str,
620        filename: &str,
621        file_data: Vec<u8>,
622    ) -> Result<reqwest::Response, VeracodeError> {
623        // Build URL with query parameters (URL encoded)
624        let mut url = format!("{}/{}", self.config.base_url, endpoint);
625        
626        if !query_params.is_empty() {
627            url.push('?');
628            let encoded_params: Vec<String> = query_params
629                .iter()
630                .map(|(key, value)| {
631                    format!("{}={}", 
632                        urlencoding::encode(key), 
633                        urlencoding::encode(value))
634                })
635                .collect();
636            url.push_str(&encoded_params.join("&"));
637        }
638        
639        // Build multipart form with only the file
640        let part = multipart::Part::bytes(file_data)
641            .file_name(filename.to_string())
642            .mime_str("application/octet-stream")
643            .map_err(|e| VeracodeError::InvalidConfig(e.to_string()))?;
644        
645        let form = multipart::Form::new().part(file_field_name.to_string(), part);
646
647        let auth_header = self.generate_auth_header("POST", &url)?;
648        
649        let response = self.client
650            .post(&url)
651            .header("Authorization", auth_header)
652            .header("User-Agent", "Veracode Rust Client")
653            .multipart(form)
654            .send()
655            .await?;
656
657        Ok(response)
658    }
659
660    /// Make a POST request with query parameters (like Java implementation for XML API)
661    ///
662    /// This method mimics the Java API wrapper's approach for POST operations
663    /// where parameters are added to the query string rather than form data.
664    ///
665    /// # Arguments
666    ///
667    /// * `endpoint` - The API endpoint to call
668    /// * `query_params` - Query parameters as key-value pairs
669    ///
670    /// # Returns
671    ///
672    /// A `Result` containing the response or an error.
673    pub async fn post_with_query_params(
674        &self,
675        endpoint: &str,
676        query_params: &[(&str, &str)],
677    ) -> Result<reqwest::Response, VeracodeError> {
678        // Build URL with query parameters (URL encoded)
679        let mut url = format!("{}/{}", self.config.base_url, endpoint);
680        
681        if !query_params.is_empty() {
682            url.push('?');
683            let encoded_params: Vec<String> = query_params
684                .iter()
685                .map(|(key, value)| {
686                    format!("{}={}", 
687                        urlencoding::encode(key), 
688                        urlencoding::encode(value))
689                })
690                .collect();
691            url.push_str(&encoded_params.join("&"));
692        }
693
694        let auth_header = self.generate_auth_header("POST", &url)?;
695        
696        let response = self.client
697            .post(&url)
698            .header("Authorization", auth_header)
699            .header("User-Agent", "Veracode Rust Client")
700            .send()
701            .await?;
702
703        Ok(response)
704    }
705
706    /// Make a GET request with query parameters (like Java implementation for XML API)
707    ///
708    /// This method mimics the Java API wrapper's approach for GET operations
709    /// where parameters are added to the query string.
710    ///
711    /// # Arguments
712    ///
713    /// * `endpoint` - The API endpoint to call
714    /// * `query_params` - Query parameters as key-value pairs
715    ///
716    /// # Returns
717    ///
718    /// A `Result` containing the response or an error.
719    pub async fn get_with_query_params(
720        &self,
721        endpoint: &str,
722        query_params: &[(&str, &str)],
723    ) -> Result<reqwest::Response, VeracodeError> {
724        // Build URL with query parameters (URL encoded)
725        let mut url = format!("{}/{}", self.config.base_url, endpoint);
726        
727        if !query_params.is_empty() {
728            url.push('?');
729            let encoded_params: Vec<String> = query_params
730                .iter()
731                .map(|(key, value)| {
732                    format!("{}={}", 
733                        urlencoding::encode(key), 
734                        urlencoding::encode(value))
735                })
736                .collect();
737            url.push_str(&encoded_params.join("&"));
738        }
739
740        let auth_header = self.generate_auth_header("GET", &url)?;
741        
742        let response = self.client
743            .get(&url)
744            .header("Authorization", auth_header)
745            .header("User-Agent", "Veracode Rust Client")
746            .send()
747            .await?;
748
749        Ok(response)
750    }
751
752    /// Upload a large file using chunked streaming (for uploadlargefile.do)
753    ///
754    /// This method implements chunked upload functionality similar to the Java API wrapper.
755    /// It uploads files in chunks and provides progress tracking capabilities.
756    ///
757    /// # Arguments
758    ///
759    /// * `endpoint` - The API endpoint to call  
760    /// * `query_params` - Query parameters as key-value pairs
761    /// * `file_path` - Path to the file to upload
762    /// * `content_type` - Content type for the file (default: binary/octet-stream)
763    /// * `progress_callback` - Optional callback for progress tracking
764    ///
765    /// # Returns
766    ///
767    /// A `Result` containing the response or an error.
768    pub async fn upload_large_file_chunked<F>(
769        &self,
770        endpoint: &str,
771        query_params: &[(&str, &str)],
772        file_path: &str,
773        content_type: Option<&str>,
774        progress_callback: Option<F>,
775    ) -> Result<reqwest::Response, VeracodeError>
776    where
777        F: Fn(u64, u64, f64) + Send + Sync,
778    {
779        use std::fs::File;
780        use std::io::{Read, Seek, SeekFrom};
781        
782        // Build URL with query parameters
783        let mut url = format!("{}/{}", self.config.base_url, endpoint);
784        
785        if !query_params.is_empty() {
786            url.push('?');
787            let encoded_params: Vec<String> = query_params
788                .iter()
789                .map(|(key, value)| {
790                    format!("{}={}", 
791                        urlencoding::encode(key), 
792                        urlencoding::encode(value))
793                })
794                .collect();
795            url.push_str(&encoded_params.join("&"));
796        }
797
798        // Open file and get size
799        let mut file = File::open(file_path)
800            .map_err(|e| VeracodeError::InvalidConfig(format!("Failed to open file: {e}")))?;
801        
802        let file_size = file.metadata()
803            .map_err(|e| VeracodeError::InvalidConfig(format!("Failed to get file size: {e}")))?
804            .len();
805
806        // Check file size limit (2GB for uploadlargefile.do)
807        const MAX_FILE_SIZE: u64 = 2 * 1024 * 1024 * 1024; // 2GB
808        if file_size > MAX_FILE_SIZE {
809            return Err(VeracodeError::InvalidConfig(
810                format!("File size ({} bytes) exceeds maximum limit of {} bytes", file_size, MAX_FILE_SIZE)
811            ));
812        }
813
814        // Read entire file for now (can be optimized to streaming later)
815        file.seek(SeekFrom::Start(0))
816            .map_err(|e| VeracodeError::InvalidConfig(format!("Failed to seek file: {e}")))?;
817        
818        let mut file_data = Vec::with_capacity(file_size as usize);
819        file.read_to_end(&mut file_data)
820            .map_err(|e| VeracodeError::InvalidConfig(format!("Failed to read file: {e}")))?;
821
822        // Generate auth header
823        let auth_header = self.generate_auth_header("POST", &url)?;
824
825        // Create request with streaming body
826        let request = self.client
827            .post(&url)
828            .header("Authorization", auth_header)
829            .header("User-Agent", "Veracode Rust Client")
830            .header("Content-Type", content_type.unwrap_or("binary/octet-stream"))
831            .header("Content-Length", file_size.to_string())
832            .body(file_data);
833
834        // Track progress if callback provided
835        if let Some(callback) = progress_callback {
836            callback(file_size, file_size, 100.0);
837        }
838
839        let response = request.send().await?;
840        Ok(response)
841    }
842
843    /// Upload a file with binary data (optimized for uploadlargefile.do)
844    ///
845    /// This method uploads a file as raw binary data without multipart encoding,
846    /// which is the expected format for the uploadlargefile.do endpoint.
847    ///
848    /// # Arguments
849    ///
850    /// * `endpoint` - The API endpoint to call
851    /// * `query_params` - Query parameters as key-value pairs  
852    /// * `file_data` - File data as bytes
853    /// * `content_type` - Content type for the file
854    ///
855    /// # Returns
856    ///
857    /// A `Result` containing the response or an error.
858    pub async fn upload_file_binary(
859        &self,
860        endpoint: &str,
861        query_params: &[(&str, &str)],
862        file_data: Vec<u8>,
863        content_type: &str,
864    ) -> Result<reqwest::Response, VeracodeError> {
865        // Build URL with query parameters
866        let mut url = format!("{}/{}", self.config.base_url, endpoint);
867        
868        if !query_params.is_empty() {
869            url.push('?');
870            let encoded_params: Vec<String> = query_params
871                .iter()
872                .map(|(key, value)| {
873                    format!("{}={}", 
874                        urlencoding::encode(key), 
875                        urlencoding::encode(value))
876                })
877                .collect();
878            url.push_str(&encoded_params.join("&"));
879        }
880
881        let auth_header = self.generate_auth_header("POST", &url)?;
882        
883        let response = self.client
884            .post(&url)
885            .header("Authorization", auth_header)
886            .header("User-Agent", "Veracode Rust Client")
887            .header("Content-Type", content_type)
888            .header("Content-Length", file_data.len().to_string())
889            .body(file_data)
890            .send()
891            .await?;
892
893        Ok(response)
894    }
895}