1use serde::{Deserialize, Serialize};
7
8use crate::{VeracodeClient, VeracodeError};
9
10#[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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
35#[serde(rename_all = "UPPERCASE")]
36pub enum DevStage {
37 Development,
38 Testing,
39 Release,
40}
41
42#[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#[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 pub fn is_successful(&self) -> bool {
71 matches!(self, ScanStatus::Success)
72 }
73
74 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 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#[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#[derive(Debug, Clone, Serialize, Deserialize)]
128pub struct SourceFile {
129 pub file: String,
131 pub function_name: Option<String>,
133 pub function_prototype: String,
135 pub line: u32,
137 pub qualified_function_name: String,
139 pub scope: String,
141}
142
143#[derive(Debug, Clone, Serialize, Deserialize)]
145pub struct FindingFiles {
146 pub source_file: SourceFile,
148}
149
150#[derive(Debug, Clone, Serialize, Deserialize)]
152pub struct StackDumps {
153 pub stack_dump: Option<Vec<serde_json::Value>>,
155}
156
157#[derive(Debug, Clone, Serialize, Deserialize)]
159pub struct Finding {
160 pub cwe_id: String,
162 pub display_text: String,
164 pub files: FindingFiles,
166 pub flaw_details_link: Option<String>,
168 pub gob: String,
170 pub issue_id: u32,
172 pub issue_type: String,
174 pub issue_type_id: String,
176 pub severity: u32,
178 pub stack_dumps: Option<StackDumps>,
180 pub title: String,
182}
183
184#[derive(Debug, Clone, Serialize, Deserialize)]
186pub struct FindingsResponse {
187 #[serde(rename = "_links")]
189 pub links: Option<serde_json::Value>,
190 pub scan_id: String,
192 pub scan_status: ScanStatus,
194 pub message: String,
196 pub modules: Vec<String>,
198 pub modules_count: u32,
200 pub findings: Vec<Finding>,
202 pub selected_modules: Vec<String>,
204 pub stack_dump: Option<serde_json::Value>,
206}
207
208#[derive(Debug, Clone, Serialize, Deserialize)]
211pub struct LegacyFinding {
212 pub file: String,
214 pub line: u32,
216 pub issue_type: String,
218 pub severity: u32,
220 pub message: String,
222 pub cwe_id: u32,
224 pub details_link: Option<String>,
226 pub issue_id: Option<String>,
228 pub owasp_category: Option<String>,
230 pub sans_category: Option<String>,
232}
233
234impl Finding {
235 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
252fn strip_html_tags(html: &str) -> String {
254 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 result.split_whitespace().collect::<Vec<&str>>().join(" ")
269}
270
271#[derive(Debug, Clone, Serialize, Deserialize)]
273pub struct CreateScanRequest {
274 #[serde(skip_serializing_if = "never_skip_string")]
276 pub binary_name: String,
277 #[serde(skip_serializing_if = "never_skip_u64")]
279 pub binary_size: u64,
280 #[serde(skip_serializing_if = "never_skip_string")]
282 pub binary_hash: String,
283 pub project_name: String,
285 #[serde(skip_serializing_if = "Option::is_none")]
287 pub project_uri: Option<String>,
288 pub dev_stage: DevStage,
290 #[serde(skip_serializing_if = "Option::is_none")]
292 pub app_id: Option<String>,
293 #[serde(skip_serializing_if = "Option::is_none")]
295 pub project_ref: Option<String>,
296 #[serde(skip_serializing_if = "Option::is_none")]
298 pub scan_timeout: Option<u32>,
299 #[serde(skip_serializing_if = "Option::is_none")]
301 pub plugin_version: Option<String>,
302 #[serde(skip_serializing_if = "Option::is_none")]
304 pub emit_stack_dump: Option<String>,
305 #[serde(skip_serializing_if = "Option::is_none")]
307 pub include_modules: Option<String>,
308}
309
310fn never_skip_string(_: &String) -> bool {
312 false
313}
314
315fn never_skip_u64(_: &u64) -> bool {
317 false
318}
319
320#[derive(Debug, Clone)]
322pub struct ScanCreationResult {
323 pub scan_id: String,
325 pub upload_uri: Option<String>,
327 pub details_uri: Option<String>,
329 pub start_uri: Option<String>,
331 pub cancel_uri: Option<String>,
333 pub expected_segments: Option<u32>,
335}
336
337#[derive(Debug, Clone, Serialize, Deserialize)]
339pub struct ScanConfig {
340 pub timeout: Option<u32>,
342 pub include_low_severity: Option<bool>,
344 pub max_findings: Option<u32>,
346}
347
348#[derive(Debug, Clone, Serialize, Deserialize)]
350pub struct Scan {
351 pub scan_id: String,
353 pub scan_status: ScanStatus,
355 pub api_version: f64,
357 pub app_id: Option<String>,
359 pub project_name: String,
361 pub project_uri: Option<String>,
363 pub project_ref: Option<String>,
365 pub commit_hash: Option<String>,
367 pub dev_stage: String,
369 pub binary_name: String,
371 pub binary_size: u64,
373 pub binary_hash: String,
375 pub binary_segments_expected: u32,
377 pub binary_segments_uploaded: u32,
379 pub scan_timeout: Option<u32>,
381 pub scan_duration: Option<f64>,
383 pub results_size: Option<f64>,
385 pub message: Option<String>,
387 pub created: String,
389 pub changed: String,
391 pub modules: Vec<serde_json::Value>,
393 pub selected_modules: Vec<serde_json::Value>,
395 pub display_modules: Vec<serde_json::Value>,
397 pub display_selected_modules: Vec<serde_json::Value>,
399 #[serde(rename = "_links")]
401 pub links: Option<serde_json::Value>,
402}
403
404#[derive(Debug, Clone, Serialize, Deserialize)]
406pub struct ScanResults {
407 pub scan: Scan,
409 pub findings: Vec<Finding>,
411 pub summary: FindingsSummary,
413 pub standards: SecurityStandards,
415}
416
417#[derive(Debug, Clone, Serialize, Deserialize)]
419pub struct FindingsSummary {
420 pub very_high: u32,
422 pub high: u32,
424 pub medium: u32,
426 pub low: u32,
428 pub very_low: u32,
430 pub informational: u32,
432 pub total: u32,
434}
435
436#[derive(Debug, Clone, Serialize, Deserialize)]
438pub struct SecurityStandards {
439 pub owasp: Option<StandardCompliance>,
441 pub sans: Option<StandardCompliance>,
443 pub pci: Option<StandardCompliance>,
445 pub cwe: Option<StandardCompliance>,
447}
448
449#[derive(Debug, Clone, Serialize, Deserialize)]
451pub struct StandardCompliance {
452 pub total_rules: u32,
454 pub violations: u32,
456 pub compliance_score: f64,
458 pub violated_rules: Vec<String>,
460}
461
462pub struct PipelineApi {
464 client: VeracodeClient,
465 debug: bool,
466}
467
468impl PipelineApi {
469 pub fn new(client: VeracodeClient) -> Self {
471 Self { client, debug: false }
472 }
473
474 pub fn new_with_debug(client: VeracodeClient, debug: bool) -> Self {
476 Self { client, debug }
477 }
478
479 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 format!("{}/pipeline_scan/v1", self.client.config().base_url.trim_end_matches('/'))
486 }
487 }
488
489 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 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 pub async fn create_scan_with_app_lookup(&self, mut request: CreateScanRequest, app_name: Option<&str>) -> Result<ScanCreationResult, PipelineError> {
531 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 pub async fn create_scan(&self, mut request: CreateScanRequest) -> Result<ScanCreationResult, PipelineError> {
553 if request.plugin_version.is_none() {
555 request.plugin_version = Some("25.2.0-0".to_string());
556 }
557
558 if request.scan_timeout.is_none() {
560 request.scan_timeout = Some(30);
561 }
562
563 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 if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(&response_text) {
575 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 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 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 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(¤t_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 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 pub async fn upload_binary(&self, scan_id: &str, binary_data: &[u8]) -> Result<(), PipelineError> {
728 let upload_uri = format!("/pipeline_scan/scans/{scan_id}/segments/1");
730 let expected_segments = 1; let file_name = "binary.tar.gz";
732
733 self.upload_binary_segments(&upload_uri, expected_segments, binary_data, file_name).await
734 }
735
736 async fn upload_single_segment(
738 &self,
739 upload_uri: &str,
740 segment_data: &[u8],
741 file_name: &str,
742 ) -> Result<String, PipelineError> {
743 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 let mut headers = std::collections::HashMap::new();
752 headers.insert("accept", "application/json");
753 headers.insert("PLUGIN-VERSION", "25.2.0-0"); 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 fn extract_next_upload_uri(&self, response_text: &str) -> Option<String> {
778 if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(response_text) {
780 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 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 pub async fn start_scan_with_uri(&self, start_uri: &str, config: Option<ScanConfig>) -> Result<(), PipelineError> {
809 let mut payload = serde_json::json!({
811 "scan_status": "STARTED"
812 });
813
814 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 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 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 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 let mut payload = serde_json::json!({
871 "scan_status": "STARTED"
872 });
873
874 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 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 pub async fn get_scan_with_uri(&self, details_uri: &str) -> Result<Scan, PipelineError> {
918 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 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 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 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 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 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 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 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 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 Err(PipelineError::FindingsNotReady)
1051 },
1052 _ => {
1053 Err(PipelineError::InvalidRequest(format!(
1055 "Failed to get findings - HTTP {status}: {response_text}"
1056 )))
1057 }
1058 }
1059 }
1060
1061 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 let summary = self.calculate_summary(&findings);
1088
1089 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 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 #[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 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 if scan.scan_status.is_successful() || scan.scan_status.is_failed() {
1182 return Ok(scan);
1183 }
1184
1185 tokio::time::sleep(tokio::time::Duration::from_secs(interval as u64)).await;
1187 }
1188
1189 Err(PipelineError::ScanTimeout)
1190 }
1191
1192 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 _ => {} }
1214 }
1215
1216 summary
1217 }
1218}