veracode_platform/
pipeline.rs

1//! Pipeline Scan API functionality for scanning applications with static analysis.
2//!
3//! This module provides functionality to interact with the Veracode Pipeline Scan API,
4//! allowing you to submit applications for static analysis and retrieve scan results.
5
6use serde::{Deserialize, Serialize};
7
8use crate::{VeracodeClient, VeracodeError};
9
10/// Error types specific to pipeline scan operations
11#[derive(Debug, thiserror::Error)]
12pub enum PipelineError {
13    #[error("Pipeline scan not found")]
14    ScanNotFound,
15    #[error("Permission denied: {0}")]
16    PermissionDenied(String),
17    #[error("Invalid request: {0}")]
18    InvalidRequest(String),
19    #[error("Scan timeout")]
20    ScanTimeout,
21    #[error("Scan findings not ready yet - try again later")]
22    FindingsNotReady,
23    #[error("Application not found: {0}")]
24    ApplicationNotFound(String),
25    #[error("Multiple applications found with name '{0}'. Please check the application name and ensure it uniquely identifies a single application.")]
26    MultipleApplicationsFound(String),
27    #[error("API error: {0}")]
28    ApiError(#[from] VeracodeError),
29    #[error("HTTP error: {0}")]
30    Http(#[from] reqwest::Error),
31}
32
33/// Pipeline scan development stage
34#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
35#[serde(rename_all = "UPPERCASE")]
36pub enum DevStage {
37    Development,
38    Testing,
39    Release,
40}
41
42/// Pipeline scan stage/status
43#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
44#[serde(rename_all = "UPPERCASE")]
45pub enum ScanStage {
46    Create,
47    Upload,
48    Start,
49    Details,
50    Findings,
51}
52
53/// Pipeline scan execution status  
54#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
55#[serde(rename_all = "UPPERCASE")]
56pub enum ScanStatus {
57    Pending,
58    Uploading,
59    Started,
60    Success,
61    Failure,
62    Cancelled,
63    Timeout,
64    #[serde(rename = "USER_TIMEOUT")]
65    UserTimeout,
66}
67
68impl ScanStatus {
69    /// Check if the scan completed successfully
70    pub fn is_successful(&self) -> bool {
71        matches!(self, ScanStatus::Success)
72    }
73
74    /// Check if the scan failed or was terminated
75    pub fn is_failed(&self) -> bool {
76        matches!(self, 
77            ScanStatus::Failure |
78            ScanStatus::Cancelled |
79            ScanStatus::Timeout |
80            ScanStatus::UserTimeout
81        )
82    }
83
84    /// Check if the scan is still in progress
85    pub fn is_in_progress(&self) -> bool {
86        matches!(self, 
87            ScanStatus::Pending |
88            ScanStatus::Uploading |
89            ScanStatus::Started
90        )
91    }
92}
93
94impl std::fmt::Display for ScanStatus {
95    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
96        match self {
97            ScanStatus::Pending => write!(f, "PENDING"),
98            ScanStatus::Uploading => write!(f, "UPLOADING"),
99            ScanStatus::Started => write!(f, "STARTED"),
100            ScanStatus::Success => write!(f, "SUCCESS"),
101            ScanStatus::Failure => write!(f, "FAILURE"),
102            ScanStatus::Cancelled => write!(f, "CANCELLED"),
103            ScanStatus::Timeout => write!(f, "TIMEOUT"),
104            ScanStatus::UserTimeout => write!(f, "USER_TIMEOUT"),
105        }
106    }
107}
108
109/// Security finding severity levels
110#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
111pub enum Severity {
112    #[serde(rename = "0")]
113    Informational,
114    #[serde(rename = "1")]
115    VeryLow,
116    #[serde(rename = "2")]
117    Low,
118    #[serde(rename = "3")]
119    Medium,
120    #[serde(rename = "4")]
121    High,
122    #[serde(rename = "5")]
123    VeryHigh,
124}
125
126/// Source file information for a finding
127#[derive(Debug, Clone, Serialize, Deserialize)]
128pub struct SourceFile {
129    /// File path where the issue was found
130    pub file: String,
131    /// Function name (may be null)
132    pub function_name: Option<String>,
133    /// Function prototype
134    pub function_prototype: String,
135    /// Line number in the file
136    pub line: u32,
137    /// Qualified function name
138    pub qualified_function_name: String,
139    /// Scope information
140    pub scope: String,
141}
142
143/// Files information containing source file details
144#[derive(Debug, Clone, Serialize, Deserialize)]
145pub struct FindingFiles {
146    /// Source file information
147    pub source_file: SourceFile,
148}
149
150/// Stack dump information
151#[derive(Debug, Clone, Serialize, Deserialize)]
152pub struct StackDumps {
153    /// Array of stack dumps (optional - can be missing when stack_dumps is empty object)
154    pub stack_dump: Option<Vec<serde_json::Value>>,
155}
156
157/// Security finding/issue from a pipeline scan
158#[derive(Debug, Clone, Serialize, Deserialize)]
159pub struct Finding {
160    /// CWE (Common Weakness Enumeration) ID as string
161    pub cwe_id: String,
162    /// Detailed message with HTML formatting
163    pub display_text: String,
164    /// File and location information
165    pub files: FindingFiles,
166    /// Flaw details link for accessing detailed vulnerability information (optional)
167    pub flaw_details_link: Option<String>,
168    /// Grade of defect (e.g., "B")
169    pub gob: String,
170    /// Issue ID for tracking
171    pub issue_id: u32,
172    /// Type of security issue
173    pub issue_type: String,
174    /// Issue type identifier
175    pub issue_type_id: String,
176    /// Severity level (0-5)
177    pub severity: u32,
178    /// Stack dump information (optional)
179    pub stack_dumps: Option<StackDumps>,
180    /// Short title/summary of the issue
181    pub title: String,
182}
183
184/// Complete findings response from the API
185#[derive(Debug, Clone, Serialize, Deserialize)]
186pub struct FindingsResponse {
187    /// HAL navigation links
188    #[serde(rename = "_links")]
189    pub links: Option<serde_json::Value>,
190    /// Scan ID
191    pub scan_id: String,
192    /// Current scan status
193    pub scan_status: ScanStatus,
194    /// Scan message
195    pub message: String,
196    /// List of modules scanned
197    pub modules: Vec<String>,
198    /// Number of modules
199    pub modules_count: u32,
200    /// List of security findings
201    pub findings: Vec<Finding>,
202    /// Selected modules
203    pub selected_modules: Vec<String>,
204    /// Stack dump information (optional)
205    pub stack_dump: Option<serde_json::Value>,
206}
207
208/// Legacy Finding struct for backwards compatibility
209/// Converts from the new Finding format to a simpler structure
210#[derive(Debug, Clone, Serialize, Deserialize)]
211pub struct LegacyFinding {
212    /// File path where the issue was found
213    pub file: String,
214    /// Line number in the file
215    pub line: u32,
216    /// Type of security issue
217    pub issue_type: String,
218    /// Severity level (0-5)
219    pub severity: u32,
220    /// Descriptive message about the issue (HTML stripped)
221    pub message: String,
222    /// CWE (Common Weakness Enumeration) ID
223    pub cwe_id: u32,
224    /// Optional link to more details
225    pub details_link: Option<String>,
226    /// Issue ID for tracking
227    pub issue_id: Option<String>,
228    /// OWASP category if applicable
229    pub owasp_category: Option<String>,
230    /// SANS category if applicable
231    pub sans_category: Option<String>,
232}
233
234impl Finding {
235    /// Convert to legacy format for backwards compatibility
236    pub fn to_legacy(&self) -> LegacyFinding {
237        LegacyFinding {
238            file: self.files.source_file.file.clone(),
239            line: self.files.source_file.line,
240            issue_type: self.issue_type.clone(),
241            severity: self.severity,
242            message: strip_html_tags(&self.display_text),
243            cwe_id: self.cwe_id.parse().unwrap_or(0),
244            details_link: None,
245            issue_id: Some(self.issue_id.to_string()),
246            owasp_category: None,
247            sans_category: None,
248        }
249    }
250}
251
252/// Strip HTML tags from display text to get plain text message
253fn strip_html_tags(html: &str) -> String {
254    // Simple HTML tag removal without regex dependency
255    let mut result = String::new();
256    let mut in_tag = false;
257    
258    for ch in html.chars() {
259        match ch {
260            '<' => in_tag = true,
261            '>' => in_tag = false,
262            _ if !in_tag => result.push(ch),
263            _ => {}
264        }
265    }
266    
267    // Clean up extra whitespace
268    result.split_whitespace().collect::<Vec<&str>>().join(" ")
269}
270
271/// Pipeline scan request for creating a new scan
272#[derive(Debug, Clone, Serialize, Deserialize)]
273pub struct CreateScanRequest {
274    /// Name of the binary/artifact being scanned (MANDATORY)
275    #[serde(skip_serializing_if = "never_skip_string")]
276    pub binary_name: String,
277    /// Size of the binary in bytes (MANDATORY)
278    #[serde(skip_serializing_if = "never_skip_u64")]
279    pub binary_size: u64,
280    /// SHA-256 hash of the binary (MANDATORY)
281    #[serde(skip_serializing_if = "never_skip_string")]
282    pub binary_hash: String,
283    /// Project name
284    pub project_name: String,
285    /// Project URI (optional)
286    #[serde(skip_serializing_if = "Option::is_none")]
287    pub project_uri: Option<String>,
288    /// Development stage
289    pub dev_stage: DevStage,
290    /// Application ID (optional, for linking to existing Veracode app)
291    #[serde(skip_serializing_if = "Option::is_none")]
292    pub app_id: Option<String>,
293    /// Project reference/branch/commit (optional)
294    #[serde(skip_serializing_if = "Option::is_none")]
295    pub project_ref: Option<String>,
296    /// Scan timeout in minutes (optional)
297    #[serde(skip_serializing_if = "Option::is_none")]
298    pub scan_timeout: Option<u32>,
299    /// Plugin version (automatically set)
300    #[serde(skip_serializing_if = "Option::is_none")]
301    pub plugin_version: Option<String>,
302    /// Emit stack dump flag (optional)
303    #[serde(skip_serializing_if = "Option::is_none")]
304    pub emit_stack_dump: Option<String>,
305    /// Include specific modules (optional)
306    #[serde(skip_serializing_if = "Option::is_none")]
307    pub include_modules: Option<String>,
308}
309
310/// Helper function to never skip String fields (ensures mandatory fields are always included)
311fn never_skip_string(_: &String) -> bool {
312    false
313}
314
315/// Helper function to never skip u64 fields (ensures mandatory fields are always included)
316fn never_skip_u64(_: &u64) -> bool {
317    false
318}
319
320/// Result of scan creation containing scan ID and _links for operations
321#[derive(Debug, Clone)]
322pub struct ScanCreationResult {
323    /// The scan ID
324    pub scan_id: String,
325    /// Upload URI from _links.upload.href 
326    pub upload_uri: Option<String>,
327    /// Details URI from _links.details.href
328    pub details_uri: Option<String>,
329    /// Start URI from _links.start.href
330    pub start_uri: Option<String>,
331    /// Cancel URI from _links.cancel.href
332    pub cancel_uri: Option<String>,
333    /// Expected number of upload segments
334    pub expected_segments: Option<u32>,
335}
336
337/// Pipeline scan configuration
338#[derive(Debug, Clone, Serialize, Deserialize)]
339pub struct ScanConfig {
340    /// Maximum timeout in minutes (default: 60)
341    pub timeout: Option<u32>,
342    /// Include low severity findings
343    pub include_low_severity: Option<bool>,
344    /// Maximum number of findings to return
345    pub max_findings: Option<u32>,
346}
347
348/// Pipeline scan details/status
349#[derive(Debug, Clone, Serialize, Deserialize)]
350pub struct Scan {
351    /// Unique scan ID
352    pub scan_id: String,
353    /// Current scan status (UPLOADING, VERIFYING, RUNNING, RESULTS_READY, etc.)
354    pub scan_status: ScanStatus,
355    /// API version
356    pub api_version: f64,
357    /// Application ID (may be null)
358    pub app_id: Option<String>,
359    /// Project name
360    pub project_name: String,
361    /// Project URI
362    pub project_uri: Option<String>,
363    /// Project reference
364    pub project_ref: Option<String>,
365    /// Commit hash
366    pub commit_hash: Option<String>,
367    /// Development stage
368    pub dev_stage: String,
369    /// Binary name being scanned
370    pub binary_name: String,
371    /// Binary size in bytes
372    pub binary_size: u64,
373    /// Binary SHA-256 hash
374    pub binary_hash: String,
375    /// Expected number of binary segments
376    pub binary_segments_expected: u32,
377    /// Number of binary segments uploaded
378    pub binary_segments_uploaded: u32,
379    /// Scan timeout in minutes
380    pub scan_timeout: Option<u32>,
381    /// Scan duration in minutes (can be fractional)
382    pub scan_duration: Option<f64>,
383    /// Results size (can be fractional)
384    pub results_size: Option<f64>,
385    /// Status message
386    pub message: Option<String>,
387    /// Scan creation time
388    pub created: String,
389    /// Last changed time
390    pub changed: String,
391    /// Modules information
392    pub modules: Vec<serde_json::Value>,
393    /// Selected modules
394    pub selected_modules: Vec<serde_json::Value>,
395    /// Display modules
396    pub display_modules: Vec<serde_json::Value>,
397    /// Display selected modules
398    pub display_selected_modules: Vec<serde_json::Value>,
399    /// Links for navigation (HAL format)
400    #[serde(rename = "_links")]
401    pub links: Option<serde_json::Value>,
402}
403
404/// Pipeline scan results
405#[derive(Debug, Clone, Serialize, Deserialize)]
406pub struct ScanResults {
407    /// Scan metadata
408    pub scan: Scan,
409    /// List of security findings
410    pub findings: Vec<Finding>,
411    /// Findings summary by severity
412    pub summary: FindingsSummary,
413    /// Security standards compliance
414    pub standards: SecurityStandards,
415}
416
417/// Summary of findings by severity
418#[derive(Debug, Clone, Serialize, Deserialize)]
419pub struct FindingsSummary {
420    /// Number of Very High severity findings
421    pub very_high: u32,
422    /// Number of High severity findings
423    pub high: u32,
424    /// Number of Medium severity findings
425    pub medium: u32,
426    /// Number of Low severity findings
427    pub low: u32,
428    /// Number of Very Low severity findings
429    pub very_low: u32,
430    /// Number of Informational findings
431    pub informational: u32,
432    /// Total number of findings
433    pub total: u32,
434}
435
436/// Security standards compliance information
437#[derive(Debug, Clone, Serialize, Deserialize)]
438pub struct SecurityStandards {
439    /// OWASP compliance
440    pub owasp: Option<StandardCompliance>,
441    /// SANS compliance
442    pub sans: Option<StandardCompliance>,
443    /// PCI compliance
444    pub pci: Option<StandardCompliance>,
445    /// CWE categories
446    pub cwe: Option<StandardCompliance>,
447}
448
449/// Compliance information for a security standard
450#[derive(Debug, Clone, Serialize, Deserialize)]
451pub struct StandardCompliance {
452    /// Total number of applicable rules
453    pub total_rules: u32,
454    /// Number of rules violated
455    pub violations: u32,
456    /// Compliance percentage (0-100)
457    pub compliance_score: f64,
458    /// List of violated rule IDs
459    pub violated_rules: Vec<String>,
460}
461
462/// Pipeline Scan API client
463pub struct PipelineApi {
464    client: VeracodeClient,
465    debug: bool,
466}
467
468impl PipelineApi {
469    /// Create a new Pipeline API client
470    pub fn new(client: VeracodeClient) -> Self {
471        Self { client, debug: false }
472    }
473
474    /// Create a new Pipeline API client with debug enabled
475    pub fn new_with_debug(client: VeracodeClient, debug: bool) -> Self {
476        Self { client, debug }
477    }
478
479    /// Get the pipeline scan v1 base URL for file uploads
480    fn get_pipeline_base_url(&self) -> String {
481        if self.client.config().base_url.contains("api.veracode.com") {
482            "https://api.veracode.com/pipeline_scan/v1".to_string()
483        } else {
484            // For other environments, use the configured base URL with pipeline_scan/v1 path
485            format!("{}/pipeline_scan/v1", self.client.config().base_url.trim_end_matches('/'))
486        }
487    }
488
489    /// Look up application ID by application name
490    ///
491    /// # Arguments
492    ///
493    /// * `app_name` - The name of the application to search for
494    ///
495    /// # Returns
496    ///
497    /// A `Result` containing the application ID as a string if found
498    pub async fn lookup_app_id_by_name(&self, app_name: &str) -> Result<String, PipelineError> {
499        let applications = self.client.search_applications_by_name(app_name).await?;
500        
501        match applications.len() {
502            0 => Err(PipelineError::ApplicationNotFound(app_name.to_string())),
503            1 => Ok(applications[0].id.to_string()),
504            _ => {
505                // Print the found applications to help the user
506                eprintln!("❌ Found {} applications matching '{}':", applications.len(), app_name);
507                for (i, app) in applications.iter().enumerate() {
508                    if let Some(ref profile) = app.profile {
509                        eprintln!("   {}. ID: {} - Name: '{}'", i + 1, app.id, profile.name);
510                    } else {
511                        eprintln!("   {}. ID: {} - GUID: {}", i + 1, app.id, app.guid);
512                    }
513                }
514                eprintln!("💡 Please provide a more specific application name that matches exactly one application.");
515                Err(PipelineError::MultipleApplicationsFound(app_name.to_string()))
516            }
517        }
518    }
519
520    /// Create a new pipeline scan with automatic app_id lookup
521    ///
522    /// # Arguments
523    ///
524    /// * `request` - Scan creation request with binary details
525    /// * `app_name` - Optional application name to look up app_id automatically
526    ///
527    /// # Returns
528    ///
529    /// A `Result` containing the scan details if successful
530    pub async fn create_scan_with_app_lookup(&self, mut request: CreateScanRequest, app_name: Option<&str>) -> Result<ScanCreationResult, PipelineError> {
531        // Look up app_id if app_name is provided
532        if let Some(name) = app_name {
533            if request.app_id.is_none() {
534                let app_id = self.lookup_app_id_by_name(name).await?;
535                request.app_id = Some(app_id);
536                println!("✅ Found application '{}' with ID: {}", name, request.app_id.as_ref().unwrap());
537            }
538        }
539        
540        self.create_scan(request).await
541    }
542
543    /// Create a new pipeline scan
544    ///
545    /// # Arguments
546    ///
547    /// * `request` - Scan creation request with binary details
548    ///
549    /// # Returns
550    ///
551    /// A `Result` containing the scan details if successful
552    pub async fn create_scan(&self, mut request: CreateScanRequest) -> Result<ScanCreationResult, PipelineError> {
553        // Set plugin version to match Java implementation (from MANIFEST.MF)
554        if request.plugin_version.is_none() {
555            request.plugin_version = Some("25.2.0-0".to_string());
556        }
557        
558        // Set default scan timeout to 30 minutes if not supplied
559        if request.scan_timeout.is_none() {
560            request.scan_timeout = Some(30);
561        }
562        
563        // Generate auth header for debugging
564        let endpoint = "/pipeline_scan/v1/scans";
565        let _full_url = format!("{}{}", self.client.config().base_url, endpoint);
566                
567        let response = self.client
568            .post_with_response("/pipeline_scan/v1/scans", Some(&request))
569            .await?;
570
571        let response_text = response.text().await?;
572        
573        // Parse response to extract scan ID and _links (using actual API response structure)
574        if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(&response_text) {
575            // Extract scan ID
576            let scan_id = json_value.get("scan_id")
577                .and_then(|id| id.as_str())
578                .ok_or_else(|| PipelineError::InvalidRequest("Missing scan_id in response".to_string()))?
579                .to_string();
580            
581            // Extract all useful URIs from _links
582            let links = json_value.get("_links");
583            
584            let upload_uri = links
585                .and_then(|links| links.get("upload"))
586                .and_then(|upload| upload.get("href"))
587                .and_then(|href| href.as_str())
588                .map(|s| s.to_string());
589            
590            let details_uri = links
591                .and_then(|links| links.get("details"))
592                .and_then(|details| details.get("href"))
593                .and_then(|href| href.as_str())
594                .map(|s| s.to_string());
595            
596            let start_uri = links
597                .and_then(|links| links.get("start"))
598                .and_then(|start| start.get("href"))
599                .and_then(|href| href.as_str())
600                .map(|s| s.to_string());
601            
602            let cancel_uri = links
603                .and_then(|links| links.get("cancel"))
604                .and_then(|cancel| cancel.get("href"))
605                .and_then(|href| href.as_str())
606                .map(|s| s.to_string());
607            
608            // Extract expected segments
609            let expected_segments = json_value.get("binary_segments_expected")
610                .and_then(|segments| segments.as_u64())
611                .map(|s| s as u32);
612            
613            if self.debug {
614                println!("✅ Scan creation response parsed:");
615                println!("   Scan ID: {scan_id}");
616                if let Some(ref uri) = upload_uri {
617                    println!("   Upload URI: {uri}");
618                }
619                if let Some(ref uri) = details_uri {
620                    println!("   Details URI: {uri}");
621                }
622                if let Some(ref uri) = start_uri {
623                    println!("   Start URI: {uri}");
624                }
625                if let Some(ref uri) = cancel_uri {
626                    println!("   Cancel URI: {uri}");
627                }
628                if let Some(segments) = expected_segments {
629                    println!("   Expected segments: {segments}");
630                }
631            }
632            
633            return Ok(ScanCreationResult {
634                scan_id,
635                upload_uri,
636                details_uri,
637                start_uri,
638                cancel_uri,
639                expected_segments,
640            });
641        }
642
643        Err(PipelineError::InvalidRequest("Failed to parse scan creation response".to_string()))
644    }
645
646    /// Upload binary data for a scan using segmented upload (matching Java implementation)
647    ///
648    /// The Veracode Pipeline Scan API requires files to be uploaded in a predetermined
649    /// number of segments. This method follows the exact Java implementation pattern:
650    /// 1. Gets segment count and upload URI from scan creation response
651    /// 2. Calculates segment size as file_size / num_segments
652    /// 3. Updates URI after each segment upload based on API response
653    ///
654    /// # Arguments
655    ///
656    /// * `initial_upload_uri` - The upload URI from scan creation response
657    /// * `expected_segments` - Number of segments expected by the API
658    /// * `binary_data` - The binary file data to upload
659    /// * `file_name` - Original file name for the binary
660    ///
661    /// # Returns
662    ///
663    /// A `Result` indicating success or failure
664    pub async fn upload_binary_segments(
665        &self,
666        initial_upload_uri: &str,
667        expected_segments: i32,
668        binary_data: &[u8],
669        file_name: &str,
670    ) -> Result<(), PipelineError> {
671        let total_size = binary_data.len();
672        let segment_size = ((total_size as f64) / (expected_segments as f64)).ceil() as usize;
673        
674        if self.debug {
675            println!("📤 Uploading binary in {expected_segments} segments ({total_size} bytes total)");
676            println!("   Segment size: {segment_size} bytes each");
677        }
678        
679        let mut current_upload_uri = initial_upload_uri.to_string();
680        
681        for segment_num in 0..expected_segments {
682            let start_idx = (segment_num as usize) * segment_size;
683            let end_idx = std::cmp::min(start_idx + segment_size, total_size);
684            let segment_data = &binary_data[start_idx..end_idx];
685            
686            if self.debug {
687                println!("   Uploading segment {}/{} ({} bytes)...", segment_num + 1, expected_segments, segment_data.len());
688            }
689            
690            match self.upload_single_segment(&current_upload_uri, segment_data, file_name).await {
691                Ok(response_text) => {
692                    if self.debug {
693                        println!("   ✅ Segment {} uploaded successfully", segment_num + 1);
694                    }
695                    
696                    // Parse response to get next upload URI (like Java implementation)
697                    if segment_num < expected_segments - 1 {
698                        match self.extract_next_upload_uri(&response_text) {
699                            Some(next_uri) => {
700                                current_upload_uri = next_uri;
701                                if self.debug {
702                                    println!("   📍 Next segment URI: {current_upload_uri}");
703                                }
704                            }
705                            None => {
706                                if self.debug {
707                                    eprintln!("   ⚠️  No next URI found in response, using current");
708                                }
709                            }
710                        }
711                    }
712                }
713                Err(e) => {
714                    eprintln!("   ❌ Failed to upload segment {}: {}", segment_num + 1, e);
715                    return Err(e);
716                }
717            }
718        }
719        
720        if self.debug {
721            println!("✅ All {expected_segments} segments uploaded successfully");
722        }
723        Ok(())
724    }
725
726    /// Simplified upload method for backwards compatibility
727    pub async fn upload_binary(&self, scan_id: &str, binary_data: &[u8]) -> Result<(), PipelineError> {
728        // For backwards compatibility, use a default approach
729        let upload_uri = format!("/pipeline_scan/scans/{scan_id}/segments/1");
730        let expected_segments = 1; // Default to single segment
731        let file_name = "binary.tar.gz";
732        
733        self.upload_binary_segments(&upload_uri, expected_segments, binary_data, file_name).await
734    }
735
736    /// Upload a single segment using the provided URI (exactly matching Java implementation)
737    async fn upload_single_segment(
738        &self,
739        upload_uri: &str,
740        segment_data: &[u8],
741        file_name: &str,
742    ) -> Result<String, PipelineError> {
743        // Get base URL and create full URL using pipeline scan v1 base URL
744        let url = if upload_uri.starts_with("http") {
745            upload_uri.to_string()
746        } else {
747            format!("{}{}", self.get_pipeline_base_url(), upload_uri)
748        };
749        
750        // Prepare additional headers for pipeline scan
751        let mut headers = std::collections::HashMap::new();
752        headers.insert("accept", "application/json");
753        headers.insert("PLUGIN-VERSION", "25.2.0-0");  // CRITICAL: Java adds this header! (from MANIFEST.MF)
754        
755        // Use the client's multipart PUT upload method
756        let response = self.client.upload_file_multipart_put(
757            &url,
758            "file",
759            file_name,
760            segment_data.to_vec(),
761            Some(headers)
762        ).await.map_err(PipelineError::ApiError)?;
763
764        if response.status().is_success() {
765            let response_text = response.text().await?;
766            Ok(response_text)
767        } else {
768            let status = response.status();
769            let error_text = response.text().await.unwrap_or_else(|_| "Unknown error".to_string());
770            Err(PipelineError::InvalidRequest(format!(
771                "Segment upload failed with status {status}: {error_text}"
772            )))
773        }
774    }
775
776    /// Extract the next upload URI from the API response (matching Java getUriSuffix)
777    fn extract_next_upload_uri(&self, response_text: &str) -> Option<String> {
778        // Parse JSON response to find the next upload URI
779        if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(response_text) {
780            // Look for _links.upload.href (HAL format)
781            if let Some(links) = json_value.get("_links") {
782                if let Some(upload) = links.get("upload") {
783                    if let Some(href) = upload.get("href") {
784                        return href.as_str().map(|s| s.to_string());
785                    }
786                }
787            }
788            
789            // Alternative: look for upload_url field
790            if let Some(upload_url) = json_value.get("upload_url") {
791                return upload_url.as_str().map(|s| s.to_string());
792            }
793        }
794        
795        None
796    }
797
798    /// Start a pipeline scan using start URI from _links
799    ///
800    /// # Arguments
801    ///
802    /// * `start_uri` - The start URI from _links.start.href
803    /// * `config` - Optional scan configuration
804    ///
805    /// # Returns
806    ///
807    /// A `Result` indicating success or failure
808    pub async fn start_scan_with_uri(&self, start_uri: &str, config: Option<ScanConfig>) -> Result<(), PipelineError> {
809        // Create payload with scan_status: STARTED
810        let mut payload = serde_json::json!({
811            "scan_status": "STARTED"
812        });
813        
814        // Add scan config fields if provided
815        if let Some(config) = config {
816            if let Some(timeout) = config.timeout {
817                payload["timeout"] = serde_json::Value::Number(timeout.into());
818            }
819            if let Some(include_low_severity) = config.include_low_severity {
820                payload["include_low_severity"] = serde_json::Value::Bool(include_low_severity);
821            }
822            if let Some(max_findings) = config.max_findings {
823                payload["max_findings"] = serde_json::Value::Number(max_findings.into());
824            }
825        }
826
827        // Construct full URL with pipeline_scan/v1 base
828        let url = if start_uri.starts_with("http") {
829            start_uri.to_string()
830        } else {
831            format!("{}{}", self.get_pipeline_base_url(), start_uri)
832        };
833
834        // Generate auth header for PUT request
835        let auth_header = self.client.generate_auth_header("PUT", &url)
836            .map_err(PipelineError::ApiError)?;
837
838        let response = self.client.client()
839            .put(&url)
840            .header("Authorization", auth_header)
841            .header("accept", "application/json")
842            .header("content-type", "application/json")
843            .json(&payload)
844            .send()
845            .await?;
846
847        if response.status().is_success() {
848            Ok(())
849        } else {
850            let error_text = response.text().await.unwrap_or_else(|_| "Unknown error".to_string());
851            Err(PipelineError::InvalidRequest(format!("Failed to start scan: {error_text}")))
852        }
853    }
854
855    /// Start a pipeline scan (fallback method using scan ID)
856    ///
857    /// # Arguments
858    ///
859    /// * `scan_id` - The scan ID
860    /// * `config` - Optional scan configuration
861    ///
862    /// # Returns
863    ///
864    /// A `Result` indicating success or failure
865    pub async fn start_scan(&self, scan_id: &str, config: Option<ScanConfig>) -> Result<(), PipelineError> {
866        let endpoint = format!("/scans/{scan_id}");
867        let url = format!("{}{}", self.get_pipeline_base_url(), endpoint);
868        
869        // Create payload with scan_status: STARTED
870        let mut payload = serde_json::json!({
871            "scan_status": "STARTED"
872        });
873        
874        // Add scan config fields if provided
875        if let Some(config) = config {
876            if let Some(timeout) = config.timeout {
877                payload["timeout"] = serde_json::Value::Number(timeout.into());
878            }
879            if let Some(include_low_severity) = config.include_low_severity {
880                payload["include_low_severity"] = serde_json::Value::Bool(include_low_severity);
881            }
882            if let Some(max_findings) = config.max_findings {
883                payload["max_findings"] = serde_json::Value::Number(max_findings.into());
884            }
885        }
886        
887        // Generate auth header for PUT request
888        let auth_header = self.client.generate_auth_header("PUT", &url)
889            .map_err(PipelineError::ApiError)?;
890
891        let response = self.client.client()
892            .put(&url)
893            .header("Authorization", auth_header)
894            .header("accept", "application/json")
895            .header("content-type", "application/json")
896            .json(&payload)
897            .send()
898            .await?;
899
900        if response.status().is_success() {
901            Ok(())
902        } else {
903            let error_text = response.text().await.unwrap_or_else(|_| "Unknown error".to_string());
904            Err(PipelineError::InvalidRequest(format!("Failed to start scan: {error_text}")))
905        }
906    }
907
908    /// Get pipeline scan details using details URI from _links
909    ///
910    /// # Arguments
911    ///
912    /// * `details_uri` - The details URI from _links.details.href
913    ///
914    /// # Returns
915    ///
916    /// A `Result` containing the scan details
917    pub async fn get_scan_with_uri(&self, details_uri: &str) -> Result<Scan, PipelineError> {
918        // Construct full URL with pipeline_scan/v1 base
919        let url = if details_uri.starts_with("http") {
920            details_uri.to_string()
921        } else {
922            format!("{}{}", self.get_pipeline_base_url(), details_uri)
923        };
924
925        // Generate auth header for GET request
926        let auth_header = self.client.generate_auth_header("GET", &url)
927            .map_err(PipelineError::ApiError)?;
928
929        let response = self.client.client()
930            .get(&url)
931            .header("Authorization", auth_header)
932            .header("accept", "application/json")
933            .send()
934            .await?;
935
936        let response_text = response.text().await?;
937        
938        serde_json::from_str::<Scan>(&response_text)
939            .map_err(|e| PipelineError::InvalidRequest(format!("Failed to parse scan details: {e}")))
940    }
941
942    /// Get pipeline scan details (fallback method using scan ID)
943    ///
944    /// # Arguments
945    ///
946    /// * `scan_id` - The scan ID
947    ///
948    /// # Returns
949    ///
950    /// A `Result` containing the scan details
951    pub async fn get_scan(&self, scan_id: &str) -> Result<Scan, PipelineError> {
952        let endpoint = format!("/scans/{scan_id}");
953        let url = format!("{}{}", self.get_pipeline_base_url(), endpoint);
954        
955        // Generate auth header for GET request
956        let auth_header = self.client.generate_auth_header("GET", &url)
957            .map_err(PipelineError::ApiError)?;
958
959        let response = self.client.client()
960            .get(&url)
961            .header("Authorization", auth_header)
962            .header("accept", "application/json")
963            .send()
964            .await?;
965
966        let response_text = response.text().await?;
967        
968        serde_json::from_str::<Scan>(&response_text)
969            .map_err(|e| PipelineError::InvalidRequest(format!("Failed to parse scan details: {e}")))
970    }
971
972    /// Get pipeline scan findings
973    ///
974    /// # Arguments
975    ///
976    /// * `scan_id` - The scan ID
977    ///
978    /// # Returns
979    ///
980    /// A `Result` containing the scan findings
981    /// 
982    /// # HTTP Status Codes
983    /// 
984    /// * `200` - Findings are ready and returned
985    /// * `202` - Scan accepted but findings not yet available (returns FindingsNotReady error)
986    pub async fn get_findings(&self, scan_id: &str) -> Result<Vec<Finding>, PipelineError> {
987        let endpoint = format!("/scans/{scan_id}/findings");
988        let url = format!("{}{}", self.get_pipeline_base_url(), endpoint);
989        
990        // println!("🔍 Debug - get_findings() calling: {}", url);
991        
992        // Generate auth header for GET request
993        let auth_header = self.client.generate_auth_header("GET", &url)
994            .map_err(PipelineError::ApiError)?;
995
996        let response = self.client.client()
997            .get(&url)
998            .header("Authorization", auth_header)
999            .header("accept", "application/json")
1000            .send()
1001            .await?;
1002
1003        let status = response.status();
1004        let response_text = response.text().await?;
1005        
1006        // Debug: Print findings response summary
1007        if self.debug {
1008            println!("🔍 Debug - Findings API Response:");
1009            println!("   Status: {status}");
1010            println!("   Response Length: {} bytes", response_text.len());
1011        }
1012        
1013        match status.as_u16() {
1014            200 => {
1015                // Findings are ready - parse the response as FindingsResponse
1016                match serde_json::from_str::<FindingsResponse>(&response_text) {
1017                    Ok(findings_response) => {
1018                        if self.debug {
1019                            println!("🔍 Debug - Successfully parsed findings response:");
1020                            println!("   Scan Status: {}", findings_response.scan_status);
1021                            println!("   Message: {}", findings_response.message);
1022                            println!("   Modules: {:?}", findings_response.modules);
1023                            println!("   Findings Count: {}", findings_response.findings.len());
1024                        }
1025                        Ok(findings_response.findings)
1026                    }
1027                    Err(e) => {
1028                        if self.debug {
1029                            println!("❌ Debug - Failed to parse FindingsResponse: {e}");
1030                        }
1031                        // Fallback: try to parse as generic JSON and extract findings array
1032                        if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(&response_text) {
1033                            if let Some(findings_array) = json_value.get("findings").and_then(|f| f.as_array()) {
1034                                if self.debug {
1035                                    println!("🔍 Debug - Trying fallback parsing of findings array...");
1036                                }
1037                                let findings: Result<Vec<Finding>, _> = findings_array
1038                                    .iter()
1039                                    .map(|f| serde_json::from_value(f.clone()))
1040                                    .collect();
1041                                return findings.map_err(|e| PipelineError::InvalidRequest(format!("Failed to parse findings array: {e}")));
1042                            }
1043                        }
1044                        Err(PipelineError::InvalidRequest(format!("Failed to parse findings response: {e}")))
1045                    }
1046                }
1047            },
1048            202 => {
1049                // Findings not ready yet
1050                Err(PipelineError::FindingsNotReady)
1051            },
1052            _ => {
1053                // Other error codes
1054                Err(PipelineError::InvalidRequest(format!(
1055                    "Failed to get findings - HTTP {status}: {response_text}"
1056                )))
1057            }
1058        }
1059    }
1060
1061    /// Get complete scan results (scan details + findings + summary)
1062    ///
1063    /// # Arguments
1064    ///
1065    /// * `scan_id` - The scan ID
1066    ///
1067    /// # Returns
1068    ///
1069    /// A `Result` containing the complete scan results
1070    /// 
1071    /// # Note
1072    /// 
1073    /// This method will return `FindingsNotReady` error if the scan findings are not yet available.
1074    /// Use `get_scan()` to check scan status before calling this method.
1075    pub async fn get_results(&self, scan_id: &str) -> Result<ScanResults, PipelineError> {
1076        if self.debug {
1077            println!("🔍 Debug - get_results() getting scan details for: {scan_id}");
1078        }
1079        let scan = self.get_scan(scan_id).await?;
1080        if self.debug {
1081            println!("🔍 Debug - get_results() scan status: {}", scan.scan_status);
1082            println!("🔍 Debug - get_results() calling get_findings() for: {scan_id}");
1083        }
1084        let findings = self.get_findings(scan_id).await?;
1085        
1086        // Calculate summary
1087        let summary = self.calculate_summary(&findings);
1088        
1089        // Generate standards compliance (placeholder - would need actual implementation)
1090        let standards = SecurityStandards {
1091            owasp: None,
1092            sans: None,
1093            pci: None,
1094            cwe: None,
1095        };
1096
1097        Ok(ScanResults {
1098            scan,
1099            findings,
1100            summary,
1101            standards,
1102        })
1103    }
1104
1105    /// Cancel a running pipeline scan
1106    ///
1107    /// # Arguments
1108    ///
1109    /// * `scan_id` - The scan ID
1110    ///
1111    /// # Returns
1112    ///
1113    /// A `Result` indicating success or failure
1114    pub async fn cancel_scan(&self, scan_id: &str) -> Result<(), PipelineError> {
1115        let endpoint = format!("/scans/{scan_id}/cancel");
1116        
1117        let response = self.client
1118            .delete_with_response(&endpoint)
1119            .await?;
1120
1121        if response.status().is_success() {
1122            Ok(())
1123        } else {
1124            let error_text = response.text().await.unwrap_or_else(|_| "Unknown error".to_string());
1125            Err(PipelineError::InvalidRequest(format!("Failed to cancel scan: {error_text}")))
1126        }
1127    }
1128
1129    /// List pipeline scans with optional filtering
1130    /// 
1131    /// **Note**: This method is not supported by the Veracode Pipeline Scan API.
1132    /// Veracode does not provide endpoints to enumerate/list all scans.
1133    /// Use `get_scan()` with a specific scan ID instead.
1134    ///
1135    /// # Arguments
1136    ///
1137    /// * `project_name` - Optional project name filter  
1138    /// * `dev_stage` - Optional development stage filter
1139    /// * `limit` - Maximum number of scans to return
1140    ///
1141    /// # Returns
1142    ///
1143    /// A `Result` containing an error indicating this operation is not supported
1144    #[deprecated(note = "Veracode Pipeline Scan API does not support listing scans. Use get_scan() with specific scan ID instead.")]
1145    pub async fn list_scans(
1146        &self,
1147        _project_name: Option<&str>,
1148        _dev_stage: Option<DevStage>,
1149        _limit: Option<u32>,
1150    ) -> Result<Vec<Scan>, PipelineError> {
1151        Err(PipelineError::InvalidRequest(
1152            "Veracode Pipeline Scan API does not support listing/enumerating scans. Use get_scan() with a specific scan ID instead.".to_string()
1153        ))
1154    }
1155
1156    /// Wait for scan to complete with polling
1157    ///
1158    /// # Arguments
1159    ///
1160    /// * `scan_id` - The scan ID
1161    /// * `timeout_minutes` - Maximum time to wait (default: 60 minutes)
1162    /// * `poll_interval_seconds` - Polling interval (default: 10 seconds)
1163    ///
1164    /// # Returns
1165    ///
1166    /// A `Result` containing the completed scan or timeout error
1167    pub async fn wait_for_completion(
1168        &self,
1169        scan_id: &str,
1170        timeout_minutes: Option<u32>,
1171        poll_interval_seconds: Option<u32>,
1172    ) -> Result<Scan, PipelineError> {
1173        let timeout = timeout_minutes.unwrap_or(60);
1174        let interval = poll_interval_seconds.unwrap_or(10);
1175        let max_polls = (timeout * 60) / interval;
1176        
1177        for _ in 0..max_polls {
1178            let scan = self.get_scan(scan_id).await?;
1179            
1180            // Check if scan is completed based on status
1181            if scan.scan_status.is_successful() || scan.scan_status.is_failed() {
1182                return Ok(scan);
1183            }
1184            
1185            // Wait before next poll
1186            tokio::time::sleep(tokio::time::Duration::from_secs(interval as u64)).await;
1187        }
1188        
1189        Err(PipelineError::ScanTimeout)
1190    }
1191
1192    /// Calculate findings summary from a list of findings
1193    fn calculate_summary(&self, findings: &[Finding]) -> FindingsSummary {
1194        let mut summary = FindingsSummary {
1195            very_high: 0,
1196            high: 0,
1197            medium: 0,
1198            low: 0,
1199            very_low: 0,
1200            informational: 0,
1201            total: findings.len() as u32,
1202        };
1203
1204        for finding in findings {
1205            match finding.severity {
1206                5 => summary.very_high += 1,
1207                4 => summary.high += 1,
1208                3 => summary.medium += 1,
1209                2 => summary.low += 1,
1210                1 => summary.very_low += 1,
1211                0 => summary.informational += 1,
1212                _ => {} // Unknown severity
1213            }
1214        }
1215
1216        summary
1217    }
1218}