ferrosift-cli 0.1.0-alpha.1

Native command-line interface for FerroSift
//! Explicit format-specific recipe loading.

use std::fmt::Write as _;

use ferrosift_compat::cyberchef;
use ferrosift_core::OperationRegistry;
use ferrosift_model::{CompatibilityProfile, Recipe, SchemaVersion};

use crate::{args::RecipeFormat, error::CliError};

pub fn load(
    bytes: &[u8],
    format: RecipeFormat,
    registry: &OperationRegistry,
) -> Result<Recipe, CliError> {
    match format {
        RecipeFormat::FerroSift => load_native(bytes),
        RecipeFormat::CyberChefV11_3 => {
            load_cyberchef(bytes, CompatibilityProfile::CyberChefV11_3, registry)
        }
        RecipeFormat::CyberChefV11_4 => {
            load_cyberchef(bytes, CompatibilityProfile::CyberChefV11_4, registry)
        }
    }
}

fn load_native(bytes: &[u8]) -> Result<Recipe, CliError> {
    let recipe: Recipe = serde_json::from_slice(bytes)
        .map_err(|error| CliError::new("cli.recipe.malformed", error.to_string()))?;
    if recipe.schema_version != SchemaVersion::CURRENT.get() {
        return Err(CliError::new(
            "cli.recipe.schema_unsupported",
            format!("schema_version={}", recipe.schema_version),
        ));
    }
    Ok(recipe)
}

fn load_cyberchef(
    bytes: &[u8],
    profile: CompatibilityProfile,
    registry: &OperationRegistry,
) -> Result<Recipe, CliError> {
    let report = cyberchef::import_recipe(bytes, profile, registry)
        .map_err(|error| CliError::new(error.code(), error.to_string()))?;
    if let Some(first) = report.findings.first() {
        let mut detail = String::new();
        for (position, finding) in report.findings.iter().enumerate() {
            if position > 0 {
                detail.push_str("; ");
            }
            let _ = write!(detail, "{} step={}", finding.code, finding.source_step);
            if let Some(operation) = &finding.original_operation {
                let _ = write!(detail, " operation={operation}");
            }
        }
        return Err(CliError::new(first.code, detail));
    }
    report.recipe.ok_or_else(|| {
        CliError::new(
            "cli.recipe.not_executable",
            "compatibility import produced no recipe",
        )
    })
}