//! Configuration: security thresholds and workspace paths. //! //! All tunable parameters live here so that operators can override them //! at runtime via [`Config::override_from_env`] or by constructing a //! custom [`Config`] and passing it to [`crate::core::pipeline::Pipeline`]. use std::path::{Path, PathBuf}; use serde::{Deserialize, Serialize}; /// Default maximum size for a payload preview string in the threat report. const DEFAULT_MAX_PREVIEW_LEN: usize = 512; /// Default maximum number of bytes of a ZIP-container entry (EPUB or /// DOCX) to load into memory. Enforced via [`crate::util::read_with_cap`] /// which counts **actual decompressed bytes** — not the size declared /// in the ZIP central directory — so it defends against lying-size /// zip bombs as well as honest-but-oversized entries. const DEFAULT_EPUB_ENTRY_SCAN_CAP: usize = 8 * 1024 * 1024; // 8 MiB /// Default total memory budget for all ZIP-container entries combined. /// Prevents the "many small entries" attack where an archive with /// thousands of entries each under the per-entry cap still exhausts memory. const DEFAULT_TOTAL_ARCHIVE_SCAN_CAP: usize = 256 * 1024 * 1024; // 256 MiB /// Default quarantine directory, relative to the current working dir. const DEFAULT_QUARANTINE_DIR: &str = "corbel_quarantine"; /// Default cleansed-output directory, relative to the current working dir. const DEFAULT_CLEANSE_DIR: &str = "corbel_clean"; /// How the cleansed document should be produced. /// /// The default (`Markdown`) is the safest option — a Markdown file /// cannot carry executable content by definition. `PreserveFormat` /// repackages the document in its original format (PDF/EPUB/DOCX) /// with the malicious entries stripped, which is more useful when /// the document itself is the artifact of interest (e.g. a book /// the researcher wants to actually read after cleaning). #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] pub enum CleanseMode { /// Produce a sanitized Markdown derivative (default, always safe). #[default] Markdown, /// Repackage the document in its original format with malicious /// entries stripped. The output is structurally similar to the /// input but with all executable vectors removed. PreserveFormat, } /// Top-level configuration for the CorbelPurge pipeline. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Config { /// Where to write quarantine tarballs. pub quarantine_dir: PathBuf, /// Where to write cleansed document derivatives. pub cleanse_dir: PathBuf, /// Maximum length (in chars) of any payload preview in the report. pub max_payload_preview_len: usize, /// Maximum size (in bytes) of a single ZIP-container entry (EPUB /// or DOCX) that will be loaded into memory. Entries that exceed /// this cap are truncated mid-stream by [`crate::util::read_with_cap`] /// and emitted as `UnknownPayload` vectors — the partial bytes are /// still inspected for file signatures (PE, ELF, OLE2, etc.). /// /// **This cap counts actual decompressed bytes, not the size /// declared in the ZIP header.** A malicious archive that /// declares `size = 100` but actually decompresses to gigabytes /// is detected and truncated at this cap. pub epub_entry_scan_cap: usize, /// Total memory budget (in bytes) for all ZIP-container entries combined. /// The parser tracks cumulative decompressed bytes across all entries /// and stops processing entries once this budget is exhausted. /// This prevents the "many small entries" attack where thousands of /// entries each under `epub_entry_scan_cap` still exhaust memory. pub total_archive_scan_cap: usize, /// If `true`, the pipeline aborts with [`crate::CorbelError::ThreatDetected`] /// the moment a malicious finding is produced, instead of proceeding /// to quarantine + cleanse. Useful for automated CI gates. pub abort_on_threat: bool, /// If `true`, the scanner will emit `Suspicious` findings for any /// executable vector it cannot confidently classify. If `false`, /// only confidently-malicious vectors are reported. pub emit_suspicious: bool, /// List of URI schemes that are considered safe for hyperlink /// navigation (e.g. `https`, `mailto`). Anything else is flagged. pub allowed_uri_schemes: Vec, /// If `true`, generate a Markdown report alongside the JSON report. pub emit_markdown_report: bool, /// Path to an external YARA rules file. If set, the scanner loads /// additional signature rules from this file at startup. pub external_rules_path: Option, /// Path to an external CVE signature database file (JSON array). pub external_cve_db_path: Option, /// How to produce the cleansed document derivative. pub cleanse_mode: CleanseMode, } impl Default for Config { fn default() -> Self { Self { quarantine_dir: PathBuf::from(DEFAULT_QUARANTINE_DIR), cleanse_dir: PathBuf::from(DEFAULT_CLEANSE_DIR), max_payload_preview_len: DEFAULT_MAX_PREVIEW_LEN, epub_entry_scan_cap: DEFAULT_EPUB_ENTRY_SCAN_CAP, total_archive_scan_cap: DEFAULT_TOTAL_ARCHIVE_SCAN_CAP, abort_on_threat: false, emit_suspicious: true, allowed_uri_schemes: vec![ "https".to_string(), "mailto".to_string(), "ftp".to_string(), ], emit_markdown_report: true, external_rules_path: None, external_cve_db_path: None, cleanse_mode: CleanseMode::Markdown, } } } impl Config { /// Construct a new config with default values, but with the /// quarantine and cleanse directories rooted at `base`. #[must_use] pub fn with_workspace(base: impl AsRef) -> Self { let base = base.as_ref(); Self { quarantine_dir: base.join(DEFAULT_QUARANTINE_DIR), cleanse_dir: base.join(DEFAULT_CLEANSE_DIR), ..Self::default() } } /// Override select fields from environment variables. /// /// Recognized variables: /// - `CORBEL_QUARANTINE_DIR` /// - `CORBEL_CLEANSE_DIR` /// - `CORBEL_ABORT_ON_THREAT` (`1`/`true`/`yes` → true) /// - `CORBEL_EMIT_SUSPICIOUS` /// - `CORBEL_EMIT_MARKDOWN_REPORT` /// - `CORBEL_EXTERNAL_RULES` (path to external YARA rules JSON) /// - `CORBEL_EXTERNAL_CVE_DB` (path to external CVE DB JSON) pub fn override_from_env(mut self) -> Self { if let Ok(v) = std::env::var("CORBEL_QUARANTINE_DIR") { self.quarantine_dir = PathBuf::from(v); } if let Ok(v) = std::env::var("CORBEL_CLEANSE_DIR") { self.cleanse_dir = PathBuf::from(v); } if let Ok(v) = std::env::var("CORBEL_ABORT_ON_THREAT") { self.abort_on_threat = truthy(&v); } if let Ok(v) = std::env::var("CORBEL_EMIT_SUSPICIOUS") { self.emit_suspicious = truthy(&v); } if let Ok(v) = std::env::var("CORBEL_EMIT_MARKDOWN_REPORT") { self.emit_markdown_report = truthy(&v); } if let Ok(v) = std::env::var("CORBEL_TOTAL_ARCHIVE_SCAN_CAP") { if let Ok(cap) = v.parse::() { self.total_archive_scan_cap = cap; } } if let Ok(v) = std::env::var("CORBEL_EXTERNAL_RULES") { if !v.is_empty() { self.external_rules_path = Some(PathBuf::from(v)); } } if let Ok(v) = std::env::var("CORBEL_EXTERNAL_CVE_DB") { if !v.is_empty() { self.external_cve_db_path = Some(PathBuf::from(v)); } } self } /// Ensure the workspace directories exist. /// /// Called automatically by [`crate::core::pipeline::Pipeline::run`], /// but exposed publicly for callers that want to pre-create them. pub fn ensure_workspace(&self) -> std::io::Result<()> { std::fs::create_dir_all(&self.quarantine_dir)?; std::fs::create_dir_all(&self.cleanse_dir)?; Ok(()) } } fn truthy(v: &str) -> bool { matches!(v.to_ascii_lowercase().as_str(), "1" | "true" | "yes" | "on") } #[cfg(test)] mod tests { use super::*; #[test] fn defaults_are_sane() { let c = Config::default(); assert!(c.quarantine_dir.ends_with(DEFAULT_QUARANTINE_DIR)); assert!(c.cleanse_dir.ends_with(DEFAULT_CLEANSE_DIR)); assert!(!c.abort_on_threat); assert!(c.emit_suspicious); assert!(c.allowed_uri_schemes.contains(&"https".to_string())); } #[test] fn env_override_works() { // Temporarily set env vars, build config, restore. let old_q = std::env::var("CORBEL_QUARANTINE_DIR").ok(); let old_a = std::env::var("CORBEL_ABORT_ON_THREAT").ok(); std::env::set_var("CORBEL_QUARANTINE_DIR", "/tmp/corbel_q_test"); std::env::set_var("CORBEL_ABORT_ON_THREAT", "yes"); let c = Config::default().override_from_env(); assert_eq!(c.quarantine_dir, PathBuf::from("/tmp/corbel_q_test")); assert!(c.abort_on_threat); match old_q { Some(v) => std::env::set_var("CORBEL_QUARANTINE_DIR", v), None => std::env::remove_var("CORBEL_QUARANTINE_DIR"), } match old_a { Some(v) => std::env::set_var("CORBEL_ABORT_ON_THREAT", v), None => std::env::remove_var("CORBEL_ABORT_ON_THREAT"), } } #[test] fn workspace_with_base() { let c = Config::with_workspace("/tmp/corbel_ws_test"); assert_eq!(c.quarantine_dir, PathBuf::from("/tmp/corbel_ws_test/corbel_quarantine")); assert_eq!(c.cleanse_dir, PathBuf::from("/tmp/corbel_ws_test/corbel_clean")); } }