veracode_platform/
reporting.rs

1//! Veracode Reporting API
2//!
3//! This module provides access to the Veracode Reporting REST API for retrieving
4//! audit logs and generating compliance reports.
5use crate::json_validator::{MAX_JSON_DEPTH, validate_json_depth};
6use crate::{VeracodeClient, VeracodeError, VeracodeRegion};
7use chrono::{DateTime, NaiveDateTime, TimeZone, Utc};
8use chrono_tz::America::New_York;
9use chrono_tz::Europe::Berlin;
10use serde::{Deserialize, Serialize};
11use urlencoding;
12
13/// Request payload for generating an audit report
14#[derive(Debug, Clone, Serialize)]
15pub struct AuditReportRequest {
16    /// The type of report to generate (always "AUDIT" for audit logs)
17    pub report_type: String,
18    /// Start date in YYYY-MM-DD format
19    pub start_date: String,
20    /// Optional end date in YYYY-MM-DD format
21    #[serde(skip_serializing_if = "Option::is_none")]
22    pub end_date: Option<String>,
23    /// Optional list of audit actions to filter (e.g., "Delete", "Create", "Update")
24    #[serde(skip_serializing_if = "Option::is_none")]
25    pub audit_action: Option<Vec<String>>,
26    /// Optional list of action types to filter (e.g., "Login", "Admin")
27    #[serde(skip_serializing_if = "Option::is_none")]
28    pub action_type: Option<Vec<String>>,
29    /// Optional list of target user IDs to filter
30    #[serde(skip_serializing_if = "Option::is_none")]
31    pub target_user_id: Option<Vec<String>>,
32    /// Optional list of modifier user IDs to filter
33    #[serde(skip_serializing_if = "Option::is_none")]
34    pub modifier_user_id: Option<Vec<String>>,
35}
36
37impl AuditReportRequest {
38    /// Create a new audit report request with just start and end dates
39    #[must_use]
40    pub fn new(start_date: impl Into<String>, end_date: Option<String>) -> Self {
41        Self {
42            report_type: "AUDIT".to_string(),
43            start_date: start_date.into(),
44            end_date,
45            audit_action: None,
46            action_type: None,
47            target_user_id: None,
48            modifier_user_id: None,
49        }
50    }
51
52    /// Add audit action filters
53    #[must_use]
54    pub fn with_audit_actions(mut self, actions: Vec<String>) -> Self {
55        self.audit_action = Some(actions);
56        self
57    }
58
59    /// Add action type filters
60    #[must_use]
61    pub fn with_action_types(mut self, types: Vec<String>) -> Self {
62        self.action_type = Some(types);
63        self
64    }
65
66    /// Add target user ID filters
67    #[must_use]
68    pub fn with_target_users(mut self, user_ids: Vec<String>) -> Self {
69        self.target_user_id = Some(user_ids);
70        self
71    }
72
73    /// Add modifier user ID filters
74    #[must_use]
75    pub fn with_modifier_users(mut self, user_ids: Vec<String>) -> Self {
76        self.modifier_user_id = Some(user_ids);
77        self
78    }
79}
80
81/// Embedded data in generate report response
82#[derive(Debug, Clone, Deserialize)]
83pub struct GenerateReportData {
84    /// The report ID used to retrieve the generated report
85    pub id: String,
86}
87
88/// Response when generating a report
89#[derive(Debug, Clone, Deserialize)]
90pub struct GenerateReportResponse {
91    /// Embedded report data
92    #[serde(rename = "_embedded")]
93    pub embedded: GenerateReportData,
94}
95
96/// Report status values
97#[derive(Debug, Clone, PartialEq, Deserialize)]
98#[serde(rename_all = "UPPERCASE")]
99pub enum ReportStatus {
100    /// Report request has been queued
101    Queued,
102    /// Report request has been submitted
103    Submitted,
104    /// Report is being processed
105    Processing,
106    /// Report has been completed and is ready
107    Completed,
108    /// Report generation failed
109    Failed,
110}
111
112impl std::fmt::Display for ReportStatus {
113    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
114        match self {
115            ReportStatus::Queued => write!(f, "Queued"),
116            ReportStatus::Submitted => write!(f, "Submitted"),
117            ReportStatus::Processing => write!(f, "Processing"),
118            ReportStatus::Completed => write!(f, "Completed"),
119            ReportStatus::Failed => write!(f, "Failed"),
120        }
121    }
122}
123
124/// Pagination links for navigating report pages
125#[derive(Debug, Clone, Deserialize)]
126pub struct ReportLinks {
127    /// Link to first page
128    pub first: Option<LinkHref>,
129    /// Link to previous page
130    pub prev: Option<LinkHref>,
131    /// Link to current page (self)
132    #[serde(rename = "self")]
133    pub self_link: Option<LinkHref>,
134    /// Link to next page
135    pub next: Option<LinkHref>,
136    /// Link to last page
137    pub last: Option<LinkHref>,
138}
139
140/// A link with href field
141#[derive(Debug, Clone, Deserialize)]
142pub struct LinkHref {
143    /// The URL path for the link
144    pub href: String,
145}
146
147/// Page metadata for pagination
148#[derive(Debug, Clone, Deserialize)]
149pub struct PageMetadata {
150    /// Current page number (0-indexed)
151    pub number: u32,
152    /// Number of items per page
153    pub size: u32,
154    /// Total number of audit log entries across all pages
155    pub total_elements: u32,
156    /// Total number of pages
157    pub total_pages: u32,
158}
159
160/// A single audit log entry (optimized for minimal deserialization)
161///
162/// This struct only deserializes the timestamp field and keeps the rest as raw JSON
163/// to minimize parsing overhead and memory allocations. The hash is computed using
164/// xxHash (much faster than SHA256 for duplicate detection).
165#[derive(Debug, Clone, Serialize)]
166pub struct AuditLogEntry {
167    /// Raw JSON string of the log entry (as received from API)
168    pub raw_log: String,
169    ///
170    /// # Errors
171    ///
172    /// Returns an error if the API request fails, the resource is not found,
173    /// or authentication/authorization fails.
174    /// Timestamp converted to UTC (computed from the timestamp field in `raw_log`)
175    #[serde(skip_serializing_if = "Option::is_none")]
176    pub timestamp_utc: Option<String>,
177    /// xxHash (128-bit) of the raw log entry for fast duplicate detection
178    pub log_hash: String,
179}
180
181/// Helper struct to extract only the timestamp during deserialization
182#[derive(Debug, Deserialize)]
183struct TimestampExtractor {
184    timestamp: Option<String>,
185}
186
187/// Report data embedded in the response
188#[derive(Debug, Clone, Deserialize)]
189pub struct ReportData {
190    /// Report ID
191    pub id: String,
192    /// Report type (always "AUDIT" for audit reports)
193    pub report_type: String,
194    /// Current status of the report
195    pub status: ReportStatus,
196    /// User who requested the report
197    pub requested_by_user: String,
198    /// Account ID that requested the report
199    pub requested_by_account: u64,
200    /// Date when report was requested
201    pub date_report_requested: String,
202    /// Date when report was completed (null if not completed)
203    pub date_report_completed: Option<String>,
204    /// Date when report expires (null if not completed)
205    pub report_expiration_date: Option<String>,
206    /// Array of audit log entries (raw JSON, processed later for efficiency)
207    pub audit_logs: serde_json::Value,
208    /// Links for pagination (null if not completed)
209    #[serde(rename = "_links")]
210    pub links: Option<ReportLinks>,
211    /// Page metadata (null if not completed)
212    pub page_metadata: Option<PageMetadata>,
213}
214
215/// Full report response with embedded data
216#[derive(Debug, Clone, Deserialize)]
217pub struct ReportResponse {
218    /// Embedded report data
219    #[serde(rename = "_embedded")]
220    pub embedded: ReportData,
221}
222
223/// Convert a timestamp from region-specific timezone to UTC
224///
225/// Each Veracode API region returns timestamps in its corresponding timezone:
226///
227/// # Errors
228///
229/// Returns an error if the API request fails, the resource is not found,
230/// or authentication/authorization fails.
231/// - **Commercial** (api.veracode.com): `America/New_York` (US-East-1)
232///   - EST (Eastern Standard Time): UTC-5 (winter)
233///   - EDT (Eastern Daylight Time): UTC-4 (summer)
234/// - **European** (api.veracode.eu): Europe/Berlin (eu-central-1)
235///   - CET (Central European Time): UTC+1 (winter)
236///   - CEST (Central European Summer Time): UTC+2 (summer)
237///
238/// # Errors
239///
240/// Returns an error if the API request fails, the resource is not found,
241/// or authentication/authorization fails.
242/// - **Federal** (api.veracode.us): `America/New_York` (US-East-1)
243///   - EST/EDT same as Commercial
244///
245/// This function automatically handles Daylight Saving Time (DST) transitions
246/// for each region using the IANA timezone database.
247///
248/// # Arguments
249///
250/// * `timestamp_str` - Timestamp string in format "YYYY-MM-DD HH:MM:SS.sss"
251/// * `region` - The Veracode region determining source timezone
252///
253/// # Returns
254///
255/// UTC timestamp string in same format, or None if parsing fails
256///
257/// # Examples
258///
259/// ```ignore
260/// use veracode_platform::VeracodeRegion;
261///
262/// // European region: Summer timestamp (CEST, UTC+2)
263/// let utc = convert_regional_timestamp_to_utc("2025-06-15 14:30:00.000", &VeracodeRegion::European);
264/// assert_eq!(utc, Some("2025-06-15 12:30:00".to_string())); // 14:30 CEST = 12:30 UTC
265///
266/// // Commercial region: Winter timestamp (EST, UTC-5)
267/// let utc = convert_regional_timestamp_to_utc("2025-12-15 14:30:00.000", &VeracodeRegion::Commercial);
268/// assert_eq!(utc, Some("2025-12-15 19:30:00".to_string())); // 14:30 EST = 19:30 UTC
269/// ```
270fn convert_regional_timestamp_to_utc(
271    timestamp_str: &str,
272    region: &VeracodeRegion,
273) -> Option<String> {
274    // Parse timestamp string - handle variable-length milliseconds
275    let has_millis = timestamp_str.contains('.');
276
277    // Parse the base datetime without milliseconds
278    let naive_dt = if has_millis {
279        // Try to parse with variable-length fractional seconds
280        // The %.f format is flexible and handles 1-9 digits
281        NaiveDateTime::parse_from_str(timestamp_str, "%Y-%m-%d %H:%M:%S%.f").ok()?
282    } else {
283        NaiveDateTime::parse_from_str(timestamp_str, "%Y-%m-%d %H:%M:%S").ok()?
284    };
285
286    // Convert from region-specific timezone to UTC
287    let utc_time = match region {
288        VeracodeRegion::European => {
289            // European region uses Europe/Berlin timezone (CET/CEST)
290            // Use earliest() to handle ambiguous times during DST fall-back
291            let regional_time: DateTime<_> = Berlin.from_local_datetime(&naive_dt).earliest()?;
292            regional_time.with_timezone(&Utc)
293        }
294        VeracodeRegion::Commercial | VeracodeRegion::Federal => {
295            // Commercial and Federal regions use America/New_York timezone (EST/EDT)
296            // Use earliest() to handle ambiguous times during DST fall-back
297            let regional_time: DateTime<_> = New_York.from_local_datetime(&naive_dt).earliest()?;
298            regional_time.with_timezone(&Utc)
299        }
300    };
301
302    // Format back to string (preserve original millisecond precision)
303    if has_millis {
304        // Format with the same number of decimal places as input
305        let formatted = utc_time.format("%Y-%m-%d %H:%M:%S%.f").to_string();
306        // Ensure we preserve the original precision
307        Some(formatted)
308    } else {
309        Some(utc_time.format("%Y-%m-%d %H:%M:%S").to_string())
310    }
311}
312
313/// Generate a fast hash of a raw log entry JSON string for duplicate detection
314///
315///
316/// # Errors
317///
318/// Returns an error if the API request fails, the resource is not found,
319/// or authentication/authorization fails.
320/// Uses xxHash (`xxh3_128`) which is significantly faster than SHA256 while still
321/// providing excellent collision resistance for deduplication purposes. This is
322/// NOT a cryptographic hash - use only for duplicate detection, not security.
323///
324/// Performance comparison vs SHA256:
325/// - xxHash: ~10-50x faster than SHA256
326/// - Still has excellent collision resistance for duplicate detection
327/// - Returns 32 hex characters (128-bit hash)
328///
329/// # Arguments
330///
331/// * `raw_json` - The raw JSON string of the log entry
332///
333/// # Returns
334///
335/// Hex-encoded xxHash3 (128-bit) hash string (32 characters)
336///
337/// # Examples
338///
339/// ```ignore
340/// let hash = generate_log_hash(r#"{"timestamp":"2025-01-01 12:00:00.000"}"#);
341/// assert_eq!(hash.len(), 32); // xxh3_128 produces 32 hex characters
342/// ```
343fn generate_log_hash(raw_json: &str) -> String {
344    use xxhash_rust::xxh3::xxh3_128;
345
346    // Hash the raw JSON bytes (extremely fast!)
347    let hash = xxh3_128(raw_json.as_bytes());
348
349    // Convert to hex string (128-bit = 32 hex chars)
350    format!("{:032x}", hash)
351}
352
353/// The Reporting API interface
354#[derive(Clone)]
355pub struct ReportingApi {
356    client: VeracodeClient,
357    region: VeracodeRegion,
358}
359
360impl ReportingApi {
361    /// Create a new Reporting API instance
362    #[must_use]
363    pub fn new(client: VeracodeClient) -> Self {
364        let region = client.config().region;
365        Self { client, region }
366    }
367
368    /// Generate an audit report (step 1 of the process)
369    ///
370    ///
371    /// # Errors
372    ///
373    /// Returns an error if the API request fails, the resource is not found,
374    /// or authentication/authorization fails.
375    /// This sends a request to generate the report. The API returns a `report_id`
376    /// which can be used to retrieve the report after it's generated.
377    ///
378    /// # Arguments
379    ///
380    /// * `request` - The audit report request parameters
381    ///
382    /// # Returns
383    ///
384    /// The report ID that can be used to retrieve the generated report
385    ///
386    /// # Errors
387    ///
388    /// Returns `VeracodeError` if the request fails
389    pub async fn generate_audit_report(
390        &self,
391        request: &AuditReportRequest,
392    ) -> Result<String, VeracodeError> {
393        let response = self
394            .client
395            .post("/appsec/v1/analytics/report", Some(request))
396            .await?;
397
398        let response_text = response.text().await?;
399        log::debug!("Generate report API response: {}", response_text);
400
401        // Validate JSON depth before parsing to prevent DoS attacks
402        validate_json_depth(&response_text, MAX_JSON_DEPTH).map_err(|e| {
403            VeracodeError::InvalidResponse(format!("JSON validation failed: {}", e))
404        })?;
405
406        let generate_response: GenerateReportResponse = serde_json::from_str(&response_text)?;
407        Ok(generate_response.embedded.id)
408    }
409
410    /// Retrieve a generated audit report (step 2 of the process)
411    ///
412    /// This retrieves the report content. The report may still be processing,
413    /// so check the status field in the response.
414    ///
415    /// # Arguments
416    ///
417    /// * `report_id` - The report ID returned from `generate_audit_report`
418    /// * `page` - Optional page number (0-indexed) for pagination
419    ///
420    /// # Returns
421    ///
422    /// The report response with status and audit log data
423    ///
424    /// # Errors
425    ///
426    /// Returns `VeracodeError` if the request fails
427    pub async fn get_audit_report(
428        &self,
429        report_id: &str,
430        page: Option<u32>,
431    ) -> Result<ReportResponse, VeracodeError> {
432        // URL-encode the report_id to prevent injection attacks
433        let encoded_report_id = urlencoding::encode(report_id);
434
435        let endpoint = if let Some(page_num) = page {
436            format!("/appsec/v1/analytics/report/{encoded_report_id}?page={page_num}")
437        } else {
438            format!("/appsec/v1/analytics/report/{encoded_report_id}")
439        };
440
441        let response = self.client.get(&endpoint, None).await?;
442        let response_text = response.text().await?;
443        log::debug!("Get audit report API response: {}", response_text);
444
445        // Validate JSON depth before parsing to prevent DoS attacks
446        validate_json_depth(&response_text, MAX_JSON_DEPTH).map_err(|e| {
447            VeracodeError::InvalidResponse(format!("JSON validation failed: {}", e))
448        })?;
449
450        let report_response: ReportResponse = serde_json::from_str(&response_text)?;
451        Ok(report_response)
452    }
453
454    /// Poll for report status until it's completed or failed
455    ///
456    /// This method polls the report status with exponential backoff until
457    /// the report is either completed or failed.
458    ///
459    /// # Arguments
460    ///
461    /// * `report_id` - The report ID to poll
462    /// * `max_attempts` - Maximum number of polling attempts (default: 30)
463    /// * `initial_delay_secs` - Initial delay between polls in seconds (default: 2)
464    ///
465    /// # Returns
466    ///
467    /// The completed report response
468    ///
469    /// # Errors
470    ///
471    /// Returns `VeracodeError` if polling fails or report generation fails
472    pub async fn poll_report_status(
473        &self,
474        report_id: &str,
475        max_attempts: Option<u32>,
476        initial_delay_secs: Option<u64>,
477    ) -> Result<ReportResponse, VeracodeError> {
478        let max_attempts = max_attempts.unwrap_or(30);
479        let initial_delay = initial_delay_secs.unwrap_or(2);
480
481        let mut attempts: u32 = 0;
482        let mut delay_secs = initial_delay;
483
484        loop {
485            attempts = attempts.saturating_add(1);
486
487            // Get current report status
488            let report = self.get_audit_report(report_id, None).await?;
489            let status = &report.embedded.status;
490
491            log::debug!(
492                "Report {} status: {} (attempt {}/{})",
493                report_id,
494                status,
495                attempts,
496                max_attempts
497            );
498
499            match status {
500                ReportStatus::Completed => {
501                    log::info!("Report {} completed successfully", report_id);
502                    return Ok(report);
503                }
504                ReportStatus::Failed => {
505                    return Err(VeracodeError::InvalidResponse(format!(
506                        "Report generation failed for report ID: {}",
507                        report_id
508                    )));
509                }
510                ReportStatus::Queued | ReportStatus::Submitted | ReportStatus::Processing => {
511                    if attempts >= max_attempts {
512                        return Err(VeracodeError::InvalidResponse(format!(
513                            "Report polling timeout after {} attempts. Status: {}",
514                            attempts, status
515                        )));
516                    }
517
518                    log::debug!("Report still processing, waiting {} seconds...", delay_secs);
519                    tokio::time::sleep(tokio::time::Duration::from_secs(delay_secs)).await;
520
521                    // Exponential backoff with max delay of 30 seconds
522                    delay_secs = std::cmp::min(delay_secs.saturating_mul(2), 30);
523                }
524            }
525        }
526    }
527
528    /// Retrieve all audit logs across all pages (OPTIMIZED)
529    ///
530    /// This method handles pagination automatically and collects all audit log
531    /// entries from all pages into a single vector. It uses optimized processing
532    /// that only deserializes the timestamp field and keeps raw JSON for efficiency.
533    ///
534    /// Performance optimizations:
535    /// - Minimal deserialization: Only extracts timestamp field
536    /// - Zero cloning: Keeps raw JSON strings instead of parsing all fields
537    /// - Fast hashing: Uses xxHash (10-50x faster than SHA256) for duplicate detection
538    ///
539    /// # Arguments
540    ///
541    /// * `report_id` - The report ID (must be in COMPLETED status)
542    ///
543    /// # Returns
544    ///
545    /// A vector containing all audit log entries from all pages
546    ///
547    /// # Errors
548    ///
549    /// Returns `VeracodeError` if any page retrieval fails
550    pub async fn get_all_audit_log_pages(
551        &self,
552        report_id: &str,
553    ) -> Result<Vec<AuditLogEntry>, VeracodeError> {
554        let mut all_logs = Vec::new();
555
556        // Get report status without page parameter first
557        let initial_report = self.get_audit_report(report_id, None).await?;
558
559        // Check if report is completed
560        if initial_report.embedded.status != ReportStatus::Completed {
561            return Err(VeracodeError::InvalidResponse(format!(
562                "Report is not completed. Status: {}",
563                initial_report.embedded.status
564            )));
565        }
566
567        // Check if there are any results
568        let page_metadata = match initial_report.embedded.page_metadata {
569            Some(metadata) if metadata.total_elements > 0 => metadata,
570            Some(metadata) => {
571                // Report has metadata but no elements (total_elements = 0)
572                log::info!(
573                    "Report completed but contains no audit log entries (0 total elements, {} total pages)",
574                    metadata.total_pages
575                );
576                return Ok(all_logs); // Return empty vector
577            }
578            None => {
579                // No metadata at all
580                log::info!("Report completed but contains no audit log entries (no page metadata)");
581                return Ok(all_logs); // Return empty vector
582            }
583        };
584
585        // Collect all pages of raw JSON
586        let mut all_pages_raw = Vec::new();
587
588        // Get first page
589        let first_page = self.get_audit_report(report_id, Some(0)).await?;
590        all_pages_raw.push(first_page.embedded.audit_logs.clone());
591
592        log::info!(
593            "Retrieved page 1/{} ({} total)",
594            page_metadata.total_pages,
595            page_metadata.total_elements
596        );
597
598        // If there are more pages, retrieve them
599        if page_metadata.total_pages > 1 {
600            for page_num in 1..page_metadata.total_pages {
601                log::debug!(
602                    "Retrieving page {}/{}",
603                    page_num.saturating_add(1),
604                    page_metadata.total_pages
605                );
606
607                let page_response = self.get_audit_report(report_id, Some(page_num)).await?;
608                all_pages_raw.push(page_response.embedded.audit_logs.clone());
609
610                log::info!(
611                    "Retrieved page {}/{}",
612                    page_num.saturating_add(1),
613                    page_metadata.total_pages
614                );
615            }
616        }
617
618        // Process all raw log entries efficiently
619        let mut conversion_stats: (u32, u32) = (0, 0); // (successes, failures)
620        let mut serialization_stats: (u32, u32) = (0, 0); // (successes, failures)
621        let mut total_entries: u32 = 0;
622
623        for page_value in all_pages_raw {
624            if let Some(logs_array) = page_value.as_array() {
625                for log_value in logs_array {
626                    total_entries = total_entries.saturating_add(1);
627
628                    // Get raw JSON string (canonical form for hashing)
629                    let raw_log = match serde_json::to_string(log_value) {
630                        Ok(json_str) => {
631                            serialization_stats.0 = serialization_stats.0.saturating_add(1);
632                            json_str
633                        }
634                        Err(e) => {
635                            log::error!(
636                                "Failed to serialize audit log entry {}: {}. Entry will be replaced with empty object.",
637                                total_entries,
638                                e
639                            );
640                            serialization_stats.1 = serialization_stats.1.saturating_add(1);
641                            "{}".to_string()
642                        }
643                    };
644
645                    // Generate hash from raw JSON (extremely fast with xxHash!)
646                    let log_hash = generate_log_hash(&raw_log);
647
648                    // Extract only the timestamp field for UTC conversion
649                    let timestamp_utc = if let Ok(extractor) =
650                        serde_json::from_value::<TimestampExtractor>(log_value.clone())
651                    {
652                        if let Some(timestamp) = extractor.timestamp {
653                            match convert_regional_timestamp_to_utc(&timestamp, &self.region) {
654                                Some(utc) => {
655                                    conversion_stats.0 = conversion_stats.0.saturating_add(1);
656                                    Some(utc)
657                                }
658                                None => {
659                                    log::warn!("Failed to convert timestamp to UTC: {}", timestamp);
660                                    conversion_stats.1 = conversion_stats.1.saturating_add(1);
661                                    None
662                                }
663                            }
664                        } else {
665                            None
666                        }
667                    } else {
668                        None
669                    };
670
671                    // Create optimized log entry with minimal allocations
672                    all_logs.push(AuditLogEntry {
673                        raw_log,
674                        timestamp_utc,
675                        log_hash,
676                    });
677                }
678            }
679        }
680
681        log::info!(
682            "Successfully processed {} audit log entries across {} pages",
683            total_entries,
684            page_metadata.total_pages
685        );
686
687        let (region_name, source_timezone) = match self.region {
688            VeracodeRegion::Commercial => (
689                "Commercial (api.veracode.com)",
690                "America/New_York (EST/EDT, UTC-5/-4)",
691            ),
692            VeracodeRegion::European => (
693                "European (api.veracode.eu)",
694                "Europe/Berlin (CET/CEST, UTC+1/+2)",
695            ),
696            VeracodeRegion::Federal => (
697                "Federal (api.veracode.us)",
698                "America/New_York (EST/EDT, UTC-5/-4)",
699            ),
700        };
701
702        log::info!(
703            "Converted {} timestamps from {} to UTC - Region: {} ({} failures)",
704            conversion_stats.0,
705            source_timezone,
706            region_name,
707            conversion_stats.1
708        );
709
710        log::info!(
711            "Generated xxHash hashes for {} log entries (optimized: 10-50x faster than SHA256, zero cloning)",
712            total_entries
713        );
714
715        if serialization_stats.1 > 0 {
716            log::warn!(
717                "Serialization statistics: {} successful, {} failed (replaced with empty objects)",
718                serialization_stats.0,
719                serialization_stats.1
720            );
721        } else {
722            log::info!(
723                "Serialization statistics: {} successful, 0 failed",
724                serialization_stats.0
725            );
726        }
727
728        Ok(all_logs)
729    }
730
731    /// Convenience method to generate and retrieve audit logs in one call
732    ///
733    /// This method combines report generation, status polling, and pagination
734    /// to retrieve all audit logs. It's the recommended way to retrieve audit logs.
735    ///
736    /// # Arguments
737    ///
738    /// * `request` - The audit report request parameters
739    ///
740    /// # Returns
741    ///
742    /// The audit log data as a JSON value containing all entries from all pages
743    ///
744    /// # Errors
745    ///
746    /// Returns `VeracodeError` if the request fails
747    pub async fn get_audit_logs(
748        &self,
749        request: &AuditReportRequest,
750    ) -> Result<serde_json::Value, VeracodeError> {
751        // Step 1: Generate the report
752        log::info!(
753            "Generating audit report for date range: {} to {}",
754            request.start_date,
755            request.end_date.as_deref().unwrap_or("now")
756        );
757        let report_id = self.generate_audit_report(request).await?;
758        log::info!("Report generated with ID: {}", report_id);
759
760        // Step 2: Poll for report completion
761        log::info!("Polling for report completion...");
762        let completed_report = self.poll_report_status(&report_id, None, None).await?;
763        log::info!(
764            "Report completed at: {}",
765            completed_report
766                .embedded
767                .date_report_completed
768                .as_deref()
769                .unwrap_or("unknown")
770        );
771
772        // Step 3: Retrieve all pages
773        log::info!("Retrieving all audit log pages...");
774        let mut all_logs = self.get_all_audit_log_pages(&report_id).await?;
775
776        // Step 4: Sort logs by timestamp_utc (oldest first, newest last)
777        log::info!(
778            "Sorting {} audit logs by timestamp (oldest to newest)...",
779            all_logs.len()
780        );
781        all_logs.sort_by(|a, b| {
782            match (&a.timestamp_utc, &b.timestamp_utc) {
783                // Both have timestamps - parse and compare them
784                (Some(ts_a), Some(ts_b)) => {
785                    // Parse timestamps for comparison
786                    // Format is "YYYY-MM-DD HH:MM:SS" (possibly with milliseconds)
787                    let parsed_a = NaiveDateTime::parse_from_str(ts_a, "%Y-%m-%d %H:%M:%S%.f")
788                        .or_else(|_| NaiveDateTime::parse_from_str(ts_a, "%Y-%m-%d %H:%M:%S"));
789                    let parsed_b = NaiveDateTime::parse_from_str(ts_b, "%Y-%m-%d %H:%M:%S%.f")
790                        .or_else(|_| NaiveDateTime::parse_from_str(ts_b, "%Y-%m-%d %H:%M:%S"));
791
792                    match (parsed_a, parsed_b) {
793                        (Ok(dt_a), Ok(dt_b)) => dt_a.cmp(&dt_b), // Both parsed successfully
794                        (Ok(_), Err(_)) => std::cmp::Ordering::Less, // a is valid, b is not - a comes first
795                        (Err(_), Ok(_)) => std::cmp::Ordering::Greater, // b is valid, a is not - b comes first
796                        (Err(_), Err(_)) => std::cmp::Ordering::Equal, // Neither parsed - keep original order
797                    }
798                }
799                // Only a has timestamp - a comes first
800                (Some(_), None) => std::cmp::Ordering::Less,
801                // Only b has timestamp - b comes first
802                (None, Some(_)) => std::cmp::Ordering::Greater,
803                // Neither has timestamp - keep original order
804                (None, None) => std::cmp::Ordering::Equal,
805            }
806        });
807        log::info!("Logs sorted successfully (oldest to newest)");
808
809        // Convert to JSON for backward compatibility with veraaudit
810        let json_logs = serde_json::to_value(&all_logs)?;
811        log::info!(
812            "Successfully retrieved {} total audit log entries",
813            all_logs.len()
814        );
815
816        Ok(json_logs)
817    }
818}
819
820/// Error type for reporting operations
821#[derive(Debug, thiserror::Error)]
822#[must_use = "Need to handle all error enum types."]
823pub enum ReportingError {
824    /// Wraps a Veracode API error
825    #[error("Veracode API error: {0}")]
826    VeracodeApi(#[from] VeracodeError),
827
828    /// Invalid date format
829    #[error("Invalid date format: {0}")]
830    InvalidDate(String),
831
832    /// Date range exceeds maximum allowed (6 months)
833    #[error("Date range exceeds maximum allowed: {0}")]
834    DateRangeExceeded(String),
835}
836
837#[cfg(test)]
838#[allow(clippy::expect_used)]
839mod tests {
840    use super::*;
841
842    #[test]
843    fn test_audit_report_request_new() {
844        let request = AuditReportRequest::new("2025-01-01", Some("2025-01-31".to_string()));
845
846        assert_eq!(request.report_type, "AUDIT");
847        assert_eq!(request.start_date, "2025-01-01");
848        assert_eq!(request.end_date, Some("2025-01-31".to_string()));
849        assert!(request.audit_action.is_none());
850        assert!(request.action_type.is_none());
851    }
852
853    #[test]
854    fn test_audit_report_request_with_filters() {
855        let request = AuditReportRequest::new("2025-01-01", Some("2025-01-31".to_string()))
856            .with_audit_actions(vec!["Delete".to_string(), "Create".to_string()])
857            .with_action_types(vec!["Admin".to_string()]);
858
859        assert_eq!(
860            request.audit_action,
861            Some(vec!["Delete".to_string(), "Create".to_string()])
862        );
863        assert_eq!(request.action_type, Some(vec!["Admin".to_string()]));
864    }
865
866    #[test]
867    fn test_audit_report_request_serialization() {
868        let request = AuditReportRequest::new("2025-01-01", Some("2025-01-31".to_string()));
869        let json = serde_json::to_string(&request).expect("should serialize to json");
870
871        assert!(json.contains("\"report_type\":\"AUDIT\""));
872        assert!(json.contains("\"start_date\":\"2025-01-01\""));
873        assert!(json.contains("\"end_date\":\"2025-01-31\""));
874    }
875
876    #[test]
877    fn test_audit_report_request_serialization_without_optional_fields() {
878        let request = AuditReportRequest::new("2025-01-01", None);
879        let json = serde_json::to_string(&request).expect("should serialize to json");
880
881        // Optional fields should not be present when None
882        assert!(!json.contains("end_date"));
883        assert!(!json.contains("audit_action"));
884        assert!(!json.contains("action_type"));
885    }
886
887    #[test]
888    fn test_convert_european_timezone_winter() {
889        // Winter timestamp: CET is UTC+1
890        let result =
891            convert_regional_timestamp_to_utc("2025-01-15 10:00:00.000", &VeracodeRegion::European);
892        assert!(result.is_some());
893        // 10:00 CET = 09:00 UTC
894        // Note: %.f format drops trailing zeros
895        assert_eq!(
896            result.expect("should convert timestamp"),
897            "2025-01-15 09:00:00"
898        );
899    }
900
901    #[test]
902    fn test_convert_european_timezone_summer() {
903        // Summer timestamp: CEST is UTC+2
904        let result =
905            convert_regional_timestamp_to_utc("2025-06-15 10:00:00.000", &VeracodeRegion::European);
906        assert!(result.is_some());
907        // 10:00 CEST = 08:00 UTC
908        assert_eq!(
909            result.expect("should convert timestamp"),
910            "2025-06-15 08:00:00"
911        );
912    }
913
914    #[test]
915    fn test_convert_commercial_timezone_winter() {
916        // Winter timestamp: EST is UTC-5
917        let result = convert_regional_timestamp_to_utc(
918            "2025-01-15 14:30:00.000",
919            &VeracodeRegion::Commercial,
920        );
921        assert!(result.is_some());
922        // 14:30 EST = 19:30 UTC
923        assert_eq!(
924            result.expect("should convert timestamp"),
925            "2025-01-15 19:30:00"
926        );
927    }
928
929    #[test]
930    fn test_convert_commercial_timezone_summer() {
931        // Summer timestamp: EDT is UTC-4
932        let result = convert_regional_timestamp_to_utc(
933            "2025-06-15 14:30:00.000",
934            &VeracodeRegion::Commercial,
935        );
936        assert!(result.is_some());
937        // 14:30 EDT = 18:30 UTC
938        assert_eq!(
939            result.expect("should convert timestamp"),
940            "2025-06-15 18:30:00"
941        );
942    }
943
944    #[test]
945    fn test_convert_federal_timezone_winter() {
946        // Federal uses same timezone as Commercial (America/New_York)
947        let result =
948            convert_regional_timestamp_to_utc("2025-12-15 14:30:00.000", &VeracodeRegion::Federal);
949        assert!(result.is_some());
950        // 14:30 EST = 19:30 UTC
951        assert_eq!(
952            result.expect("should convert timestamp"),
953            "2025-12-15 19:30:00"
954        );
955    }
956
957    #[test]
958    fn test_convert_timezone_without_milliseconds() {
959        // Test without milliseconds
960        let result =
961            convert_regional_timestamp_to_utc("2025-01-15 10:00:00", &VeracodeRegion::European);
962        assert!(result.is_some());
963        // Should not have milliseconds in output
964        assert_eq!(
965            result.expect("should convert timestamp"),
966            "2025-01-15 09:00:00"
967        );
968    }
969
970    #[test]
971    fn test_convert_timezone_variable_milliseconds() {
972        // Test with different millisecond precisions
973        let result =
974            convert_regional_timestamp_to_utc("2025-01-15 10:00:00.1", &VeracodeRegion::European);
975        assert!(result.is_some());
976
977        let result =
978            convert_regional_timestamp_to_utc("2025-01-15 10:00:00.12", &VeracodeRegion::European);
979        assert!(result.is_some());
980
981        let result = convert_regional_timestamp_to_utc(
982            "2025-01-15 10:00:00.123456",
983            &VeracodeRegion::European,
984        );
985        assert!(result.is_some());
986    }
987
988    #[test]
989    fn test_convert_timezone_invalid_format() {
990        // Invalid format should return None
991        let result = convert_regional_timestamp_to_utc("invalid", &VeracodeRegion::European);
992        assert!(result.is_none());
993
994        let result =
995            convert_regional_timestamp_to_utc("2025-13-45 25:99:99", &VeracodeRegion::Commercial);
996        assert!(result.is_none());
997    }
998
999    #[test]
1000    fn test_convert_timezone_dst_fallback_ambiguous() {
1001        // Regression test for DST fall-back ambiguous time
1002        // November 5, 2028 at 1:00 AM is ambiguous in America/New_York
1003        // (occurs twice when clocks fall back from 2:00 AM to 1:00 AM)
1004        // The function should use earliest() to resolve ambiguity
1005        let result =
1006            convert_regional_timestamp_to_utc("2028-11-05 01:00:00", &VeracodeRegion::Commercial);
1007        assert!(
1008            result.is_some(),
1009            "Should handle DST fall-back ambiguous time"
1010        );
1011
1012        // Verify conversion produces valid UTC timestamp
1013        let utc = result.expect("result should be Some as asserted above");
1014        assert!(utc.len() >= 19, "UTC timestamp should be well-formed");
1015        assert!(utc.starts_with("2028-11-05"), "Date should be preserved");
1016    }
1017
1018    // ============================================================================
1019    // SECURITY TESTS: Property-Based Testing with Proptest
1020    // ============================================================================
1021
1022    mod security_tests {
1023        use super::*;
1024        use proptest::prelude::*;
1025
1026        // ========================================================================
1027        // SECURITY TEST 1: Timestamp Parsing Edge Cases
1028        // ========================================================================
1029        // Tests: convert_regional_timestamp_to_utc
1030        // Goals: Ensure robust handling of malformed timestamps, extreme dates,
1031        //        leap years, DST transitions, and edge cases
1032
1033        proptest! {
1034            #![proptest_config(ProptestConfig {
1035                cases: if cfg!(miri) { 5 } else { 1000 },
1036                failure_persistence: None,
1037                .. ProptestConfig::default()
1038            })]
1039
1040            /// Property: Valid timestamps should always convert successfully
1041            /// Tests legitimate date ranges that should work
1042            #[test]
1043            fn proptest_valid_timestamp_conversion(
1044                year in 2000u32..2100u32,
1045                month in 1u32..=12u32,
1046                day in 1u32..=28u32, // Safe range that works for all months
1047                hour in prop::sample::select(vec![0u32, 1, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23]), // Avoid hour 2 (DST transition)
1048                minute in 0u32..=59u32,
1049                second in 0u32..=59u32,
1050                region in prop_oneof![
1051                    Just(VeracodeRegion::Commercial),
1052                    Just(VeracodeRegion::European),
1053                    Just(VeracodeRegion::Federal),
1054                ]
1055            ) {
1056                let timestamp = format!(
1057                    "{:04}-{:02}-{:02} {:02}:{:02}:{:02}",
1058                    year, month, day, hour, minute, second
1059                );
1060
1061                let result = convert_regional_timestamp_to_utc(&timestamp, &region);
1062
1063                // Valid timestamps should always convert (DST transition hour 2 is avoided)
1064                prop_assert!(result.is_some(), "Failed to convert valid timestamp: {}", timestamp);
1065
1066                // Result should be well-formed
1067                if let Some(utc) = result {
1068                    prop_assert!(utc.len() >= 19, "UTC timestamp too short: {}", utc);
1069                    prop_assert!(utc.contains('-'), "UTC timestamp missing date separator");
1070                    prop_assert!(utc.contains(':'), "UTC timestamp missing time separator");
1071                }
1072            }
1073
1074            /// Property: Malformed timestamps should fail gracefully
1075            /// Tests that invalid input doesn't panic or cause undefined behavior
1076            #[test]
1077            fn proptest_malformed_timestamp_handling(
1078                input in "\\PC{0,256}", // Any Unicode string up to 256 chars
1079            ) {
1080                // Should never panic, even on malformed input
1081                let _ = convert_regional_timestamp_to_utc(&input, &VeracodeRegion::Commercial);
1082                let _ = convert_regional_timestamp_to_utc(&input, &VeracodeRegion::European);
1083                let _ = convert_regional_timestamp_to_utc(&input, &VeracodeRegion::Federal);
1084            }
1085
1086            /// Property: Timestamps with variable millisecond precision should work
1087            /// Tests fractional seconds handling (0-9 digits)
1088            #[test]
1089            fn proptest_variable_millisecond_precision(
1090                milliseconds in "[0-9]{1,9}",
1091            ) {
1092                let timestamp = format!("2025-06-15 10:30:45.{}", milliseconds);
1093                let result = convert_regional_timestamp_to_utc(&timestamp, &VeracodeRegion::Commercial);
1094
1095                // Should handle variable precision gracefully
1096                // Either succeeds or fails safely
1097                if let Some(utc) = result {
1098                    prop_assert!(utc.len() >= 19, "UTC timestamp too short");
1099                }
1100            }
1101
1102            /// Property: Extreme dates should be handled safely
1103            /// Tests boundary conditions for year, month, day
1104            #[test]
1105            fn proptest_extreme_dates(
1106                year in 1900u32..2200u32,
1107                month in 0u32..=13u32, // Include invalid months
1108                day in 0u32..=32u32,   // Include invalid days
1109            ) {
1110                let timestamp = format!(
1111                    "{:04}-{:02}-{:02} 12:00:00",
1112                    year, month, day
1113                );
1114
1115                // Should never panic on extreme dates
1116                let _ = convert_regional_timestamp_to_utc(&timestamp, &VeracodeRegion::Commercial);
1117            }
1118        }
1119
1120        // ========================================================================
1121        // SECURITY TEST 2: Hash Function Collision Resistance
1122        // ========================================================================
1123        // Tests: generate_log_hash
1124        // Goals: Verify hash properties, collision resistance, determinism
1125
1126        proptest! {
1127            #![proptest_config(ProptestConfig {
1128                cases: if cfg!(miri) { 5 } else { 1000 },
1129                failure_persistence: None,
1130                .. ProptestConfig::default()
1131            })]
1132
1133            /// Property: Hash output should always be 32 hex characters (128-bit)
1134            /// Tests hash format consistency
1135            #[test]
1136            fn proptest_hash_format_consistency(
1137                input in "\\PC{0,1024}", // Any Unicode string up to 1KB
1138            ) {
1139                let hash = generate_log_hash(&input);
1140
1141                // xxh3_128 should always produce 32 hex characters
1142                prop_assert_eq!(hash.len(), 32, "Hash length should be 32 chars");
1143
1144                // Should only contain hex characters
1145                prop_assert!(
1146                    hash.chars().all(|c| c.is_ascii_hexdigit()),
1147                    "Hash should only contain hex chars: {}",
1148                    hash
1149                );
1150            }
1151
1152            /// Property: Same input should always produce same hash (determinism)
1153            /// Tests hash function determinism
1154            #[test]
1155            fn proptest_hash_determinism(
1156                input in "\\PC{0,2048}",
1157            ) {
1158                let hash1 = generate_log_hash(&input);
1159                let hash2 = generate_log_hash(&input);
1160
1161                prop_assert_eq!(
1162                    hash1, hash2,
1163                    "Hash function should be deterministic"
1164                );
1165            }
1166
1167            /// Property: Different inputs should produce different hashes (collision resistance)
1168            /// Tests basic collision resistance
1169            #[test]
1170            fn proptest_hash_collision_resistance(
1171                input1 in "\\PC{1,256}",
1172                input2 in "\\PC{1,256}",
1173            ) {
1174                // Only test when inputs are actually different
1175                if input1 != input2 {
1176                    let hash1 = generate_log_hash(&input1);
1177                    let hash2 = generate_log_hash(&input2);
1178
1179                    // Different inputs should produce different hashes
1180                    // (collisions are possible but extremely rare)
1181                    prop_assert_ne!(
1182                        hash1, hash2,
1183                        "Collision detected for different inputs"
1184                    );
1185                }
1186            }
1187
1188            /// Property: Small changes should produce completely different hashes (avalanche effect)
1189            /// Tests that single-bit changes cascade through the hash
1190            #[test]
1191            fn proptest_hash_avalanche_effect(
1192                base in "[a-zA-Z0-9]{10,100}",
1193                suffix in "[a-z]",
1194            ) {
1195                let input1 = base.clone();
1196                let input2 = format!("{}{}", base, suffix);
1197
1198                let hash1 = generate_log_hash(&input1);
1199                let hash2 = generate_log_hash(&input2);
1200
1201                // Adding one character should completely change the hash
1202                prop_assert_ne!(
1203                    &hash1, &hash2,
1204                    "Avalanche effect failed: similar inputs produced similar hashes"
1205                );
1206
1207                // Count differing characters (should be significant)
1208                let diff_count = hash1.chars()
1209                    .zip(hash2.chars())
1210                    .filter(|(a, b)| a != b)
1211                    .count();
1212
1213                // At least 40% of hash should change (good avalanche)
1214                prop_assert!(
1215                    diff_count >= 12,
1216                    "Poor avalanche effect: only {} of 32 chars changed",
1217                    diff_count
1218                );
1219            }
1220        }
1221
1222        // ========================================================================
1223        // SECURITY TEST 3: URL Encoding Injection Prevention
1224        // ========================================================================
1225        // Tests: URL encoding in get_audit_report
1226        // Goals: Prevent path traversal, command injection, XSS via report_id
1227
1228        proptest! {
1229            #![proptest_config(ProptestConfig {
1230                cases: if cfg!(miri) { 5 } else { 1000 },
1231                failure_persistence: None,
1232                .. ProptestConfig::default()
1233            })]
1234
1235            /// Property: URL encoding should escape all special characters
1236            /// Tests that dangerous characters are properly encoded
1237            #[test]
1238            fn proptest_url_encoding_escapes_special_chars(
1239                special_chars in prop::sample::select(vec![
1240                    "/", "\\", "?", "&", "=", "#", " ", "<", ">",
1241                    "\"", "'", "|", ";", "\n", "\r", "\0", "$"
1242                ]),
1243                base in "[a-zA-Z0-9]{5,20}",
1244            ) {
1245                let malicious_id = format!("{}{}{}", base, special_chars, base);
1246                let encoded = urlencoding::encode(&malicious_id);
1247
1248                // Encoded output should not contain the original special char
1249                // (it should be percent-encoded)
1250                prop_assert!(
1251                    !encoded.contains(special_chars),
1252                    "Special character '{}' not encoded properly",
1253                    special_chars
1254                );
1255
1256                // Should contain % for percent-encoding (or + for space)
1257                if !special_chars.chars().all(|c| c.is_alphanumeric()) {
1258                    prop_assert!(
1259                        encoded.contains('%') || (special_chars == " " && encoded.contains('+')),
1260                        "Expected encoding for '{}'",
1261                        special_chars
1262                    );
1263                }
1264            }
1265
1266            /// Property: Path traversal sequences should be encoded
1267            /// Tests protection against directory traversal attacks
1268            #[test]
1269            fn proptest_url_encoding_prevents_path_traversal(
1270                traversal in prop_oneof![
1271                    Just("../"),
1272                    Just("..\\"),
1273                    Just("../../"),
1274                    Just("..%2f"),
1275                    Just("..%5c"),
1276                    Just("%2e%2e%2f"),
1277                ],
1278                prefix in "[a-z]{1,10}",
1279                suffix in "[a-z]{1,10}",
1280            ) {
1281                let malicious_id = format!("{}{}{}", prefix, traversal, suffix);
1282                let encoded = urlencoding::encode(&malicious_id);
1283
1284                // Encoded string should not contain literal path traversal
1285                prop_assert!(
1286                    !encoded.contains("../") && !encoded.contains("..\\"),
1287                    "Path traversal not properly encoded: {}",
1288                    encoded
1289                );
1290            }
1291
1292            /// Property: Command injection characters should be encoded
1293            /// Tests protection against shell command injection
1294            #[test]
1295            fn proptest_url_encoding_prevents_command_injection(
1296                injection_char in prop::sample::select(vec![
1297                    ";", "|", "&", "$", "`", "$(", ")", "{", "}", "\n", "\r"
1298                ]),
1299                base in "[a-zA-Z0-9]{5,15}",
1300            ) {
1301                let malicious_id = format!("{}{}rm -rf /", base, injection_char);
1302                let encoded = urlencoding::encode(&malicious_id);
1303
1304                // Encoded output should not contain injection characters
1305                prop_assert!(
1306                    !encoded.contains(injection_char),
1307                    "Injection character '{}' not encoded",
1308                    injection_char
1309                );
1310            }
1311        }
1312
1313        // ========================================================================
1314        // SECURITY TEST 4: Integer Overflow Protection (Saturating Arithmetic)
1315        // ========================================================================
1316        // Tests: saturating_add, saturating_mul operations
1317        // Goals: Verify overflow protection doesn't wrap around
1318
1319        proptest! {
1320            #![proptest_config(ProptestConfig {
1321                cases: if cfg!(miri) { 5 } else { 1000 },
1322                failure_persistence: None,
1323                .. ProptestConfig::default()
1324            })]
1325
1326            /// Property: Saturating add should never overflow
1327            /// Tests u32 saturating addition behavior
1328            #[test]
1329            fn proptest_saturating_add_never_overflows(
1330                a in 0u32..=u32::MAX,
1331                b in 0u32..=1000u32,
1332            ) {
1333                let result = a.saturating_add(b);
1334
1335                // Result should be >= both operands
1336                prop_assert!(result >= a, "Saturating add decreased value");
1337
1338                // If overflow would occur, result should be MAX
1339                #[allow(clippy::arithmetic_side_effects)]
1340                {
1341                    if a as u64 + b as u64 > u32::MAX as u64 {
1342                        prop_assert_eq!(
1343                            result,
1344                            u32::MAX,
1345                            "Expected saturation at MAX for {} + {}",
1346                            a, b
1347                        );
1348                    } else {
1349                        prop_assert_eq!(
1350                            result,
1351                            a + b,
1352                            "Expected normal addition for {} + {}",
1353                            a, b
1354                        );
1355                    }
1356                }
1357            }
1358
1359            /// Property: Saturating multiply should never overflow
1360            /// Tests u64 saturating multiplication behavior (for delay_secs)
1361            #[test]
1362            fn proptest_saturating_mul_never_overflows(
1363                a in 0u64..=u64::MAX / 2,
1364                b in 0u64..=100u64,
1365            ) {
1366                let result = a.saturating_mul(b);
1367
1368                // If overflow would occur, result should be MAX
1369                if let Some(expected) = a.checked_mul(b) {
1370                    prop_assert_eq!(result, expected, "Multiplication mismatch");
1371                } else {
1372                    prop_assert_eq!(
1373                        result,
1374                        u64::MAX,
1375                        "Expected saturation at MAX for {} * {}",
1376                        a, b
1377                    );
1378                }
1379            }
1380
1381            /// Property: Counter increments should never overflow
1382            /// Tests the specific pattern used in poll_report_status and get_all_audit_log_pages
1383            #[test]
1384            fn proptest_counter_increment_safety(
1385                start in 0u32..=u32::MAX - 1000,
1386                increments in 1usize..=100,
1387            ) {
1388                let mut counter = start;
1389
1390                for _ in 0..increments {
1391                    let old_value = counter;
1392                    counter = counter.saturating_add(1);
1393
1394                    // Counter should never decrease
1395                    prop_assert!(
1396                        counter >= old_value,
1397                        "Counter decreased from {} to {}",
1398                        old_value, counter
1399                    );
1400
1401                    // If we hit MAX, it should stay at MAX
1402                    if old_value == u32::MAX {
1403                        prop_assert_eq!(counter, u32::MAX, "Counter should saturate at MAX");
1404                    }
1405                }
1406            }
1407
1408            /// Property: Page iteration should handle near-MAX values safely
1409            /// Tests the pagination loop in get_all_audit_log_pages
1410            #[test]
1411            fn proptest_page_iteration_overflow_safety(
1412                total_pages in 1u32..=1000u32,
1413            ) {
1414                // Simulate the pagination loop from get_all_audit_log_pages
1415                let mut processed = 0u32;
1416
1417                for page_num in 1..total_pages {
1418                    // This is the pattern used in line 598-612
1419                    let page_display = page_num.saturating_add(1);
1420
1421                    prop_assert!(
1422                        page_display >= page_num,
1423                        "Page display calculation overflow"
1424                    );
1425
1426                    processed = processed.saturating_add(1);
1427                }
1428
1429                // Should process total_pages - 1 pages (page 0 handled separately)
1430                prop_assert_eq!(
1431                    processed,
1432                    total_pages.saturating_sub(1),
1433                    "Page count mismatch"
1434                );
1435            }
1436        }
1437
1438        // ========================================================================
1439        // SECURITY TEST 5: Input Validation for AuditReportRequest
1440        // ========================================================================
1441        // Tests: AuditReportRequest builder methods
1442        // Goals: Ensure safe handling of user-controlled inputs
1443
1444        proptest! {
1445            #![proptest_config(ProptestConfig {
1446                cases: if cfg!(miri) { 5 } else { 1000 },
1447                failure_persistence: None,
1448                .. ProptestConfig::default()
1449            })]
1450
1451            /// Property: Request builder should handle arbitrary strings safely
1452            /// Tests that builder methods don't panic on unusual input
1453            #[test]
1454            fn proptest_request_builder_handles_arbitrary_input(
1455                start_date in "\\PC{0,256}",
1456                end_date in "\\PC{0,256}",
1457                action in "\\PC{0,100}",
1458            ) {
1459                // Should never panic, even with unusual input
1460                let request = AuditReportRequest::new(
1461                    start_date.clone(),
1462                    if end_date.is_empty() { None } else { Some(end_date.clone()) }
1463                );
1464
1465                // Verify fields are set correctly
1466                prop_assert_eq!(&request.start_date, &start_date);
1467
1468                if !end_date.is_empty() {
1469                    prop_assert_eq!(&request.end_date, &Some(end_date.clone()));
1470                }
1471
1472                // Test with_audit_actions
1473                let request = request.with_audit_actions(vec![action.clone()]);
1474                prop_assert!(request.audit_action.is_some());
1475            }
1476
1477            /// Property: Builder methods should preserve data integrity
1478            /// Tests that chained builder calls work correctly
1479            #[test]
1480            fn proptest_request_builder_data_integrity(
1481                start in "[0-9]{4}-[0-9]{2}-[0-9]{2}",
1482                actions in prop::collection::vec("[A-Za-z]{5,15}", 0..10),
1483                types in prop::collection::vec("[A-Za-z]{5,15}", 0..10),
1484                user_ids in prop::collection::vec("[0-9]{1,10}", 0..10),
1485            ) {
1486                let request = AuditReportRequest::new(start.clone(), None)
1487                    .with_audit_actions(actions.clone())
1488                    .with_action_types(types.clone())
1489                    .with_target_users(user_ids.clone())
1490                    .with_modifier_users(user_ids.clone());
1491
1492                // Verify all fields are preserved correctly
1493                prop_assert_eq!(request.start_date, start);
1494                prop_assert_eq!(request.audit_action, Some(actions));
1495                prop_assert_eq!(request.action_type, Some(types));
1496                prop_assert_eq!(request.target_user_id, Some(user_ids.clone()));
1497                prop_assert_eq!(request.modifier_user_id, Some(user_ids));
1498            }
1499
1500            /// Property: Empty collections should be handled correctly
1501            /// Tests edge case of empty filter arrays
1502            #[test]
1503            fn proptest_request_builder_empty_collections(
1504                start_date in "[0-9]{4}-[0-9]{2}-[0-9]{2}",
1505            ) {
1506                let request = AuditReportRequest::new(start_date.clone(), None)
1507                    .with_audit_actions(vec![])
1508                    .with_action_types(vec![])
1509                    .with_target_users(vec![])
1510                    .with_modifier_users(vec![]);
1511
1512                // Empty vecs should still be Some (not None)
1513                prop_assert!(request.audit_action.is_some());
1514                prop_assert!(request.action_type.is_some());
1515                prop_assert!(request.target_user_id.is_some());
1516                prop_assert!(request.modifier_user_id.is_some());
1517
1518                // But should be empty
1519                if let Some(ref actions) = request.audit_action {
1520                    prop_assert_eq!(actions.len(), 0);
1521                }
1522            }
1523
1524            /// Property: Large collections should be handled safely
1525            /// Tests that builders can handle many filter values
1526            #[test]
1527            fn proptest_request_builder_large_collections(
1528                start_date in "[0-9]{4}-[0-9]{2}-[0-9]{2}",
1529                collection_size in 1usize..=100,
1530            ) {
1531                let large_vec: Vec<String> = (0..collection_size)
1532                    .map(|i| format!("item_{}", i))
1533                    .collect();
1534
1535                let request = AuditReportRequest::new(start_date, None)
1536                    .with_audit_actions(large_vec.clone());
1537
1538                if let Some(ref actions) = request.audit_action {
1539                    prop_assert_eq!(
1540                        actions.len(),
1541                        collection_size,
1542                        "Collection size mismatch"
1543                    );
1544                }
1545            }
1546        }
1547
1548        // ========================================================================
1549        // SECURITY TEST 6: JSON Serialization Safety
1550        // ========================================================================
1551        // Tests: AuditReportRequest serialization, AuditLogEntry serialization
1552        // Goals: Ensure no injection via JSON serialization
1553
1554        proptest! {
1555            #![proptest_config(ProptestConfig {
1556                cases: if cfg!(miri) { 5 } else { 1000 },
1557                failure_persistence: None,
1558                .. ProptestConfig::default()
1559            })]
1560
1561            /// Property: Request serialization should never panic
1562            /// Tests that arbitrary input can be safely serialized
1563            #[test]
1564            fn proptest_request_serialization_safety(
1565                start_date in "\\PC{0,100}",
1566                actions in prop::collection::vec("\\PC{0,50}", 0..10),
1567            ) {
1568                let request = AuditReportRequest::new(start_date, None)
1569                    .with_audit_actions(actions);
1570
1571                // Serialization should never panic
1572                let result = serde_json::to_string(&request);
1573                prop_assert!(result.is_ok(), "Serialization failed");
1574
1575                // Serialized JSON should be valid
1576                if let Ok(json) = result {
1577                    prop_assert!(json.contains("\"report_type\""), "Missing report_type");
1578                    prop_assert!(json.contains("\"AUDIT\""), "Wrong report_type value");
1579                }
1580            }
1581
1582            /// Property: Special characters in dates should be escaped
1583            /// Tests JSON injection prevention
1584            #[test]
1585            fn proptest_json_injection_prevention(
1586                injection in prop::sample::select(vec![
1587                    r#"","malicious":"value"#,
1588                    "\n\r\t",
1589                    "\\",
1590                    "\"",
1591                    "</script>",
1592                ]),
1593                base_date in "[0-9]{4}-[0-9]{2}-[0-9]{2}",
1594            ) {
1595                let malicious_date = format!("{}{}", base_date, injection);
1596                let request = AuditReportRequest::new(malicious_date, None);
1597
1598                let json = serde_json::to_string(&request)
1599                    .expect("Should serialize even with special chars");
1600
1601                // Verify JSON is still valid after serialization
1602                let parsed: serde_json::Value = serde_json::from_str(&json)
1603                    .expect("Serialized JSON should be parseable");
1604
1605                prop_assert!(parsed.is_object(), "Should be valid JSON object");
1606            }
1607        }
1608
1609        // ========================================================================
1610        // SECURITY TEST 7: Error Handling Paths
1611        // ========================================================================
1612        // Tests: Timestamp parsing errors, hash function edge cases
1613        // Goals: Ensure errors are handled gracefully without panics
1614
1615        proptest! {
1616            #![proptest_config(ProptestConfig {
1617                cases: if cfg!(miri) { 5 } else { 500 }, // Fewer cases for error paths
1618                failure_persistence: None,
1619                .. ProptestConfig::default()
1620            })]
1621
1622            /// Property: All timestamp error paths should return None, never panic
1623            /// Tests comprehensive error handling
1624            #[test]
1625            fn proptest_timestamp_error_handling_never_panics(
1626                malformed in prop_oneof![
1627                    // Empty and whitespace
1628                    Just(""),
1629                    Just(" "),
1630                    Just("\n\t\r"),
1631
1632                    // Wrong formats
1633                    Just("2025/01/01 12:00:00"),
1634                    Just("01-01-2025 12:00:00"),
1635                    Just("2025-01-01T12:00:00Z"),
1636
1637                    // Invalid values
1638                    Just("2025-13-01 12:00:00"), // Invalid month
1639                    Just("2025-01-32 12:00:00"), // Invalid day
1640                    Just("2025-01-01 25:00:00"), // Invalid hour
1641                    Just("2025-01-01 12:60:00"), // Invalid minute
1642                    Just("2025-01-01 12:00:60"), // Invalid second
1643
1644                    // Truncated
1645                    Just("2025-01-01"),
1646                    Just("2025-01-01 12"),
1647                    Just("2025-01-01 12:00"),
1648
1649                    // Special characters
1650                    Just("2025-01-01; DROP TABLE;"),
1651                    Just("../../etc/passwd"),
1652                    Just("<script>alert('xss')</script>"),
1653
1654                    // Extreme values
1655                    Just("9999-99-99 99:99:99"),
1656                    Just("0000-00-00 00:00:00"),
1657                ],
1658            ) {
1659                // Should never panic, regardless of input
1660                let result_commercial = convert_regional_timestamp_to_utc(malformed, &VeracodeRegion::Commercial);
1661                let result_european = convert_regional_timestamp_to_utc(malformed, &VeracodeRegion::European);
1662                let result_federal = convert_regional_timestamp_to_utc(malformed, &VeracodeRegion::Federal);
1663
1664                // All should return None (error), not panic
1665                prop_assert!(result_commercial.is_none() || result_commercial.is_some());
1666                prop_assert!(result_european.is_none() || result_european.is_some());
1667                prop_assert!(result_federal.is_none() || result_federal.is_some());
1668            }
1669
1670            /// Property: Hash function should handle all input lengths
1671            /// Tests from empty to very large inputs
1672            #[test]
1673            fn proptest_hash_handles_all_input_sizes(
1674                size in 0usize..=10_000,
1675            ) {
1676                let input = "x".repeat(size);
1677
1678                // Should never panic, even with large inputs
1679                let hash = generate_log_hash(&input);
1680
1681                // Hash should always be valid format
1682                prop_assert_eq!(hash.len(), 32);
1683                prop_assert!(hash.chars().all(|c| c.is_ascii_hexdigit()));
1684            }
1685
1686            /// Property: Hash function should handle binary data safely
1687            /// Tests non-UTF8 byte sequences (via valid UTF8 with null bytes)
1688            #[test]
1689            fn proptest_hash_handles_binary_data(
1690                null_count in 0usize..=100,
1691            ) {
1692                // Create string with embedded nulls
1693                let input = format!("data{}\0{}\0end", "x".repeat(null_count), "y".repeat(null_count));
1694
1695                // Should handle null bytes gracefully
1696                let hash = generate_log_hash(&input);
1697
1698                prop_assert_eq!(hash.len(), 32);
1699                prop_assert!(hash.chars().all(|c| c.is_ascii_hexdigit()));
1700            }
1701        }
1702
1703        // ========================================================================
1704        // UNIT TESTS: Specific Security Scenarios
1705        // ========================================================================
1706
1707        #[test]
1708        fn test_url_encoding_sql_injection_attempt() {
1709            let sql_injection = "1' OR '1'='1";
1710            let encoded = urlencoding::encode(sql_injection);
1711
1712            // Should not contain unescaped quotes or spaces
1713            assert!(!encoded.contains('\''));
1714            assert!(!encoded.contains(' ') || encoded.contains('+') || encoded.contains("%20"));
1715        }
1716
1717        #[test]
1718        fn test_url_encoding_path_traversal_variants() {
1719            let variants = vec![
1720                "../../../etc/passwd",
1721                "..%2f..%2f..%2fetc%2fpasswd",
1722                "..\\..\\..\\windows\\system32",
1723            ];
1724
1725            for variant in variants {
1726                let encoded = urlencoding::encode(variant);
1727
1728                // Should not contain literal path traversal sequences
1729                assert!(!encoded.contains("../"));
1730                assert!(!encoded.contains("..\\"));
1731            }
1732        }
1733
1734        #[test]
1735        fn test_hash_known_collision_resistance() {
1736            // Test known collision-prone patterns
1737            let similar_inputs = [
1738                r#"{"timestamp":"2025-01-01 12:00:00.000"}"#,
1739                r#"{"timestamp":"2025-01-01 12:00:00.001"}"#,
1740                r#"{"timestamp":"2025-01-01 12:00:01.000"}"#,
1741            ];
1742
1743            let hashes: Vec<String> = similar_inputs
1744                .iter()
1745                .map(|input| generate_log_hash(input))
1746                .collect();
1747
1748            // All hashes should be unique
1749            for i in 0..hashes.len() {
1750                for j in i + 1..hashes.len() {
1751                    if let (Some(hash_i), Some(hash_j)) = (hashes.get(i), hashes.get(j)) {
1752                        assert_ne!(
1753                            hash_i, hash_j,
1754                            "Collision between similar inputs {} and {}",
1755                            i, j
1756                        );
1757                    }
1758                }
1759            }
1760        }
1761
1762        #[test]
1763        fn test_saturating_arithmetic_at_boundaries() {
1764            // Test u32::MAX boundary
1765            assert_eq!(u32::MAX.saturating_add(1), u32::MAX);
1766            assert_eq!((u32::MAX - 1).saturating_add(2), u32::MAX);
1767
1768            // Test u64::MAX boundary (for delay_secs)
1769            assert_eq!(u64::MAX.saturating_mul(2), u64::MAX);
1770            assert_eq!((u64::MAX / 2).saturating_mul(3), u64::MAX);
1771        }
1772
1773        #[test]
1774        fn test_timestamp_dst_transitions() {
1775            // Test DST spring forward (2025-03-09 02:00 -> 03:00 EST -> EDT)
1776            // 2:30 AM doesn't exist on this date in New_York
1777            let result = convert_regional_timestamp_to_utc(
1778                "2025-03-09 02:30:00",
1779                &VeracodeRegion::Commercial,
1780            );
1781            // Should handle gracefully (either succeed or return None)
1782            assert!(result.is_some() || result.is_none());
1783
1784            // Test DST fall back (2025-11-02 02:00 happens twice)
1785            let result = convert_regional_timestamp_to_utc(
1786                "2025-11-02 01:30:00",
1787                &VeracodeRegion::Commercial,
1788            );
1789            // Should succeed with single() if unambiguous, or fail gracefully
1790            assert!(result.is_some() || result.is_none());
1791        }
1792
1793        #[test]
1794        fn test_leap_year_handling() {
1795            // 2024 is a leap year - Feb 29 should work
1796            let result =
1797                convert_regional_timestamp_to_utc("2024-02-29 12:00:00", &VeracodeRegion::European);
1798            assert!(result.is_some(), "Leap year Feb 29 should be valid");
1799
1800            // 2025 is not a leap year - Feb 29 should fail
1801            let result =
1802                convert_regional_timestamp_to_utc("2025-02-29 12:00:00", &VeracodeRegion::European);
1803            assert!(result.is_none(), "Non-leap year Feb 29 should be invalid");
1804        }
1805
1806        #[test]
1807        fn test_empty_request_serialization() {
1808            let request = AuditReportRequest::new("2025-01-01", None);
1809            let json = serde_json::to_string(&request).expect("Should serialize");
1810
1811            // Should not include optional fields when None
1812            assert!(!json.contains("audit_action"));
1813            assert!(!json.contains("action_type"));
1814            assert!(!json.contains("target_user_id"));
1815            assert!(!json.contains("modifier_user_id"));
1816
1817            // Should include required fields
1818            assert!(json.contains("report_type"));
1819            assert!(json.contains("start_date"));
1820        }
1821    }
1822}