use std::collections::HashMap;
use rmcp::{
ErrorData as McpError,
model::{CallToolResult, Content},
};
use rsigma_convert::Backend;
use rsigma_eval::{MatchDetailLevel, Pipeline, parse_pipeline_file, resolve_builtin_pipeline};
use rsigma_parser::{
LintWarning, SigmaCollection, parse_sigma_directory, parse_sigma_file, parse_sigma_yaml,
};
use serde_json::{Value, json};
use crate::input::resolve_path;
use super::RsigmaMcp;
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct SourceInput {
#[serde(default)]
pub yaml: Option<String>,
#[serde(default)]
pub path: Option<String>,
}
pub(crate) fn json_result(value: &Value) -> CallToolResult {
let text = serde_json::to_string_pretty(value).unwrap_or_else(|_| value.to_string());
CallToolResult::success(vec![Content::text(text)])
}
pub(crate) fn invalid(message: impl Into<String>) -> McpError {
McpError::invalid_params(message.into(), None)
}
pub(crate) fn to_value<T: serde::Serialize>(value: &T) -> Value {
serde_json::to_value(value).unwrap_or(Value::Null)
}
pub(crate) fn warning_json(w: &LintWarning) -> Value {
json!({
"rule": w.rule.to_string(),
"severity": w.severity.to_string(),
"message": w.message,
"path": w.path,
"line": w.span.map(|s| s.start_line + 1),
"fixable": w.fix.is_some(),
"fix_title": w.fix.as_ref().map(|f| f.title.clone()),
})
}
pub(crate) fn parse_match_detail(level: Option<&str>) -> Result<MatchDetailLevel, McpError> {
match level {
None => Ok(MatchDetailLevel::Off),
Some(s) => s.parse::<MatchDetailLevel>().map_err(|_| {
invalid(format!(
"invalid match_detail '{s}'; expected off, summary, or full"
))
}),
}
}
pub(crate) fn get_backend(
target: &str,
options: &HashMap<String, String>,
) -> Result<Box<dyn Backend>, McpError> {
match target {
"postgres" | "postgresql" | "pg" => Ok(Box::new(
rsigma_convert::backends::postgres::PostgresBackend::from_options(options),
)),
"lynxdb" => Ok(Box::new(
rsigma_convert::backends::lynxdb::LynxDbBackend::new(),
)),
"fibratus" => Ok(Box::new(
rsigma_convert::backends::fibratus::FibratusBackend::from_options(options),
)),
other => Err(invalid(format!(
"unknown target '{other}'; available: postgres, lynxdb, fibratus"
))),
}
}
impl RsigmaMcp {
pub(crate) fn load_source(
&self,
yaml: Option<&str>,
path: Option<&str>,
) -> Result<(String, String), McpError> {
crate::input::load_source(yaml, path, self.root())
}
pub(crate) fn load_collection(
&self,
yaml: Option<&str>,
path: Option<&str>,
) -> Result<SigmaCollection, McpError> {
match (yaml, path) {
(Some(_), Some(_)) => Err(invalid("provide either `yaml` or `path`, not both")),
(None, None) => Err(invalid("one of `yaml` or `path` is required")),
(Some(text), None) => {
parse_sigma_yaml(text).map_err(|e| invalid(format!("parse error: {e}")))
}
(None, Some(p)) => {
let resolved = resolve_path(p, self.root());
let result = if resolved.is_dir() {
parse_sigma_directory(&resolved)
} else {
parse_sigma_file(&resolved)
};
result.map_err(|e| invalid(format!("cannot load '{}': {e}", resolved.display())))
}
}
}
pub(crate) fn load_pipelines(&self, specs: &[String]) -> Result<Vec<Pipeline>, McpError> {
let mut pipelines = Vec::with_capacity(specs.len());
for spec in specs {
pipelines.push(self.load_one_pipeline(spec)?);
}
pipelines.sort_by_key(|p| p.priority);
Ok(pipelines)
}
pub(crate) fn load_one_pipeline(&self, spec: &str) -> Result<Pipeline, McpError> {
if let Some(result) = resolve_builtin_pipeline(spec) {
return result.map_err(|e| invalid(format!("builtin pipeline '{spec}': {e}")));
}
let path = resolve_path(spec, self.root());
parse_pipeline_file(&path)
.map_err(|e| invalid(format!("pipeline '{}': {e}", path.display())))
}
pub(crate) fn load_events(
&self,
events: Option<Vec<Value>>,
events_path: Option<&str>,
) -> Result<Vec<Value>, McpError> {
match (events, events_path) {
(Some(_), Some(_)) => Err(invalid(
"provide either `events` or `events_path`, not both",
)),
(None, None) => Err(invalid("one of `events` or `events_path` is required")),
(Some(list), None) => Ok(list),
(None, Some(p)) => {
let path = resolve_path(p, self.root());
let text = std::fs::read_to_string(&path)
.map_err(|e| invalid(format!("cannot read '{}': {e}", path.display())))?;
let mut out = Vec::new();
for (i, line) in text.lines().enumerate() {
let line = line.trim();
if line.is_empty() {
continue;
}
let value: Value = serde_json::from_str(line)
.map_err(|e| invalid(format!("invalid JSON on line {}: {e}", i + 1)))?;
out.push(value);
}
Ok(out)
}
}
}
}