khive-pack-code 0.5.0

Code ontology pack - typed edge rules and audit findings vocabulary
Documentation
//! Validation/defaulting for the shared finding-note create path.

use async_trait::async_trait;
use serde_json::{json, Value};

use khive_runtime::{KhiveRuntime, KindHook, RuntimeError};

use crate::vocab::{is_valid_confidence, is_valid_finding_status, is_valid_severity};

#[derive(Debug, Default)]
pub(crate) struct FindingHook;

#[async_trait]
impl KindHook for FindingHook {
    async fn prepare_create(
        &self,
        _runtime: &KhiveRuntime,
        args: &mut Value,
    ) -> Result<(), RuntimeError> {
        let title = args
            .get("title")
            .or_else(|| args.get("name"))
            .and_then(Value::as_str)
            .map(str::to_string)
            .ok_or_else(|| {
                RuntimeError::InvalidInput(
                    "kind=note + note_kind=finding requires 'title' or 'name'".into(),
                )
            })?;
        if title.trim().is_empty() {
            return Err(RuntimeError::InvalidInput("title must not be empty".into()));
        }

        let mut props = match args.get("properties") {
            Some(Value::Object(map)) => Value::Object(map.clone()),
            Some(Value::Null) | None => json!({}),
            Some(_) => {
                return Err(RuntimeError::InvalidInput(
                    "properties must be an object".into(),
                ))
            }
        };
        let obj = props
            .as_object_mut()
            .expect("props is object by construction");

        for key in [
            "severity",
            "confidence",
            "categories",
            "source_run",
            "standard",
            "evidence",
            "refs",
        ] {
            if let Some(v) = args.get(key) {
                obj.insert(key.to_string(), v.clone());
            }
        }

        if let Some(v) = args.get("kind_status") {
            obj.insert("kind_status".into(), v.clone());
        } else if !obj.contains_key("kind_status") {
            obj.insert("kind_status".into(), json!("open"));
        }

        let kind_status = obj
            .get("kind_status")
            .and_then(Value::as_str)
            .map(str::to_string)
            .ok_or_else(|| RuntimeError::InvalidInput("kind_status must be a string".into()))?;
        if !is_valid_finding_status(&kind_status) {
            return Err(RuntimeError::InvalidInput(format!(
                "invalid kind_status {kind_status:?}; valid: open, resolved, wontfix, invalid"
            )));
        }

        if let Some(v) = obj.get("severity") {
            let s = v
                .as_str()
                .ok_or_else(|| RuntimeError::InvalidInput("severity must be a string".into()))?;
            if !is_valid_severity(s) {
                return Err(RuntimeError::InvalidInput(format!(
                    "invalid severity {s:?}; valid: critical, high, medium, low, info"
                )));
            }
        }

        if let Some(v) = obj.get("confidence") {
            let s = v
                .as_str()
                .ok_or_else(|| RuntimeError::InvalidInput("confidence must be a string".into()))?;
            if !is_valid_confidence(s) {
                return Err(RuntimeError::InvalidInput(format!(
                    "invalid confidence {s:?}; valid: high, medium, low"
                )));
            }
        }

        if let Some(v) = obj.get("evidence") {
            if !v.is_array() {
                return Err(RuntimeError::InvalidInput(
                    "evidence must be an array".into(),
                ));
            }
        }

        let content = args
            .get("content")
            .and_then(Value::as_str)
            .map(str::to_string)
            .unwrap_or_else(|| title.clone());

        let root = args
            .as_object_mut()
            .ok_or_else(|| RuntimeError::Internal("create args must be a JSON object".into()))?;
        root.insert("name".into(), json!(title));
        root.insert("content".into(), json!(content));
        root.insert("properties".into(), props);

        Ok(())
    }

    async fn after_create(
        &self,
        _runtime: &KhiveRuntime,
        _id: uuid::Uuid,
        _args: &Value,
    ) -> Result<(), RuntimeError> {
        Ok(())
    }
}