pipa_core/engine/contracts/
runner.rs

1//! Note: Before moving data, we validate profile connectivity.
2//! - If source profile is invalid → abort validation.
3//! - If destination/quarantine profile is invalid → skip movement and log error.
4//! This prevents wasted work and clearer operator feedback.
5use crate::connectors::fetch::fetch_data_from_source; // fetch raw bytes from source connector
6use crate::contracts::load_contract_for_file; // load TOML contract into SchemaContracts
7use crate::engine::log_action; // audit logging
8use crate::engine::validation::execute_validation; // run validators against data
9use crate::logging::error::{ValidationError, ValidationResult};
10use crate::logging::schema::{Executor, RuleResult};
11use crate::logging::{AuditLogEntry, AuditLogger};
12use crate::movement::FileMovement; // handles writing success/quarantine data
13use crate::profiles::{Profiles, load_profiles}; // profile management
14use chrono::Utc;
15use std::path::Path as StdPath;
16
17/// Outcome of running a contract validation
18pub struct ValidationOutcome {
19    pub passed: bool,             // true if no rules failed
20    pub pass_count: usize,        // number of passing rules
21    pub fail_count: usize,        // number of failing rules
22    pub results: Vec<RuleResult>, // detailed results per rule
23}
24
25/// Run a contract validation end-to-end:
26/// - Load contract + profiles
27/// - Fetch data from source
28/// - Execute validations
29/// - Move data to destination or quarantine (only if profiles are valid)
30/// - Log all actions
31///
32/// # Arguments
33/// * `logger` - The audit logger implementation to use
34/// * `contract_name` - Name of the contract to validate
35/// * `executor` - Executor context (user/host info)
36/// * `log_to_console` - Whether to print messages to console
37pub async fn run_contract_validation<L: AuditLogger>(
38    logger: &L,
39    contract_name: &str,
40    executor: &Executor,
41    log_to_console: bool,
42) -> ValidationResult<(ValidationOutcome, String)> {
43    // --- Contract existence check ---
44    let contract_path = format!("contracts/{}.toml", contract_name);
45    if !StdPath::new(&contract_path).exists() {
46        return Err(ValidationError::Other(format!(
47            "Contract '{}' not found",
48            contract_name
49        )));
50    }
51
52    // --- Load contract + profiles ---
53    let contracts = load_contract_for_file(StdPath::new(&contract_path));
54    let profiles: Profiles = load_profiles()?;
55
56    // --- Validate source config ---
57    let source = contracts
58        .source
59        .as_ref()
60        .ok_or_else(|| ValidationError::Other("Contract missing source".to_string()))?;
61    let location = source
62        .location
63        .as_ref()
64        .ok_or_else(|| ValidationError::Other("Source missing location".to_string()))?;
65
66    // --- Start log ---
67    let start_message = log_action(
68        logger,
69        "contract_validation_started",
70        None,
71        Some(contract_name),
72        None,
73        None,
74    );
75    if log_to_console {
76        println!("{}", start_message);
77    }
78
79    // --- Fetch data ---
80    let data = fetch_data_from_source(source, &profiles).await?;
81    let _ = log_action(
82        logger,
83        "file_read",
84        Some(&format!("bytes={}", data.len())),
85        None,
86        None,
87        Some(location),
88    );
89
90    // --- Determine file extension ---
91    let extension = source
92        .location
93        .as_ref()
94        .and_then(|loc| StdPath::new(loc).extension().and_then(|s| s.to_str()))
95        .unwrap_or("csv");
96
97    // --- Execute validations ---
98    let results = execute_validation(&data, extension, &contracts, executor).await?;
99    let pass_count = results.iter().filter(|r| r.result == "pass").count();
100    let fail_count = results.iter().filter(|r| r.result == "fail").count();
101    let validation_passed = fail_count == 0;
102
103    // --- Load DataFrame for movement ---
104    let original_location = source.location.as_deref().unwrap_or("unknown");
105    let driver = crate::drivers::get_driver(extension)?;
106    let df = driver.load(&data)?;
107
108    // --- Validate profile connectivity before movement ---
109    let (source_valid, dest_valid, quarantine_valid) = FileMovement::validate_profiles(
110        contracts.source.as_ref(),
111        contracts.destination.as_ref(),
112        contracts.quarantine.as_ref(),
113        &profiles,
114    )
115    .await;
116
117    if !source_valid {
118        return Err(ValidationError::Other(
119            "Source profile connectivity failed".to_string(),
120        ));
121    }
122
123    // --- Movement logic ---
124    if validation_passed {
125        if let Some(dest) = &contracts.destination {
126            if dest.r#type != "not_moved" {
127                if !dest_valid {
128                    logger.log_and_print(
129                        &AuditLogEntry {
130                            timestamp: Utc::now().to_rfc3339(),
131                            level: "AUDIT",
132                            event: "movement_skipped",
133                            contract: Some(crate::logging::schema::Contract {
134                                name: &contracts.contract.name,
135                                version: &contracts.contract.version,
136                            }),
137                            target: None,
138                            results: None,
139                            executor: executor.clone(),
140                            details: Some("Destination profile connectivity failed"),
141                            summary: None,
142                        },
143                        "⚠️ Skipped movement: destination profile invalid",
144                    );
145                } else {
146                    match FileMovement::write_success_data(&df, original_location, dest, &profiles)
147                        .await
148                    {
149                        Ok(_) => logger.log_and_print(
150                            &AuditLogEntry {
151                                timestamp: Utc::now().to_rfc3339(),
152                                level: "AUDIT",
153                                event: "movement_success",
154                                contract: Some(crate::logging::schema::Contract {
155                                    name: &contracts.contract.name,
156                                    version: &contracts.contract.version,
157                                }),
158                                target: None,
159                                results: None,
160                                executor: executor.clone(),
161                                details: Some("Data written to destination"),
162                                summary: None,
163                            },
164                            "✅ Data written to destination",
165                        ),
166                        Err(e) => logger.log_and_print(
167                            &AuditLogEntry {
168                                timestamp: Utc::now().to_rfc3339(),
169                                level: "AUDIT",
170                                event: "movement_error",
171                                contract: Some(crate::logging::schema::Contract {
172                                    name: &contracts.contract.name,
173                                    version: &contracts.contract.version,
174                                }),
175                                target: None,
176                                results: None,
177                                executor: executor.clone(),
178                                details: Some(&format!("Failed to write to destination: {}", e)),
179                                summary: None,
180                            },
181                            &format!("❌ Failed to write to destination: {}", e),
182                        ),
183                    }
184                }
185            }
186        }
187    } else {
188        if let Some(quarantine) = &contracts.quarantine {
189            if quarantine.r#type != "not_moved" {
190                if !quarantine_valid {
191                    logger.log_and_print(
192                        &AuditLogEntry {
193                            timestamp: Utc::now().to_rfc3339(),
194                            level: "AUDIT",
195                            event: "movement_skipped",
196                            contract: Some(crate::logging::schema::Contract {
197                                name: &contracts.contract.name,
198                                version: &contracts.contract.version,
199                            }),
200                            target: None,
201                            results: None,
202                            executor: executor.clone(),
203                            details: Some("Quarantine profile connectivity failed"),
204                            summary: None,
205                        },
206                        "⚠️ Skipped movement: quarantine profile invalid",
207                    );
208                } else {
209                    match FileMovement::write_quarantine_data(
210                        &df,
211                        original_location,
212                        quarantine,
213                        &profiles,
214                    )
215                    .await
216                    {
217                        Ok(_) => logger.log_and_print(
218                            &AuditLogEntry {
219                                timestamp: Utc::now().to_rfc3339(),
220                                level: "AUDIT",
221                                event: "movement_quarantine",
222                                contract: Some(crate::logging::schema::Contract {
223                                    name: &contracts.contract.name,
224                                    version: &contracts.contract.version,
225                                }),
226                                target: None,
227                                results: None,
228                                executor: executor.clone(),
229                                details: Some("Data written to quarantine"),
230                                summary: None,
231                            },
232                            "⚠️ Data quarantined",
233                        ),
234                        Err(e) => logger.log_and_print(
235                            &AuditLogEntry {
236                                timestamp: Utc::now().to_rfc3339(),
237                                level: "AUDIT",
238                                event: "movement_error",
239                                contract: Some(crate::logging::schema::Contract {
240                                    name: &contracts.contract.name,
241                                    version: &contracts.contract.version,
242                                }),
243                                target: None,
244                                results: None,
245                                executor: executor.clone(),
246                                details: Some(&format!("Failed to write to quarantine: {}", e)),
247                                summary: None,
248                            },
249                            &format!("❌ Failed to write to quarantine: {}", e),
250                        ),
251                    }
252                }
253            }
254        }
255    }
256
257    // --- Completion log ---
258    let details = format!("pass={}, fail={}", pass_count, fail_count);
259    let message = log_action(
260        logger,
261        "contract_validation_completed",
262        Some(&details),
263        Some(&contracts.contract.name),
264        Some(&contracts.contract.version),
265        None,
266    );
267    if log_to_console {
268        println!("{}", message);
269    }
270
271    Ok((
272        ValidationOutcome {
273            passed: validation_passed,
274            pass_count,
275            fail_count,
276            results,
277        },
278        message,
279    ))
280}