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