use rsigma_parser::lint::{
LintConfig, LintWarning, Severity as LintSeverity, lint_yaml_str_with_config,
};
use tower_lsp_server::ls_types::*;
use crate::position::{LineIndex, resolve_path};
#[allow(dead_code)]
pub fn diagnose(text: &str) -> Vec<Diagnostic> {
diagnose_with_config(text, &LintConfig::default())
}
pub fn diagnose_with_config(text: &str, config: &LintConfig) -> Vec<Diagnostic> {
let index = LineIndex::new(text);
let mut diagnostics = Vec::new();
let warnings = lint_yaml_str_with_config(text, config);
for w in &warnings {
diagnostics.push(lint_warning_to_diagnostic(w, text, &index));
}
match rsigma_parser::parse_sigma_yaml(text) {
Ok(collection) => {
for rule in &collection.rules {
if let Err(e) = rsigma_eval::compile_rule(rule) {
diagnostics.push(compile_error_to_diagnostic(&e.to_string(), text, &index));
}
}
}
Err(e) => {
diagnostics.push(parse_error_to_diagnostic(&e.to_string(), text, &index));
}
}
diagnostics
}
fn lint_warning_to_diagnostic(warning: &LintWarning, text: &str, index: &LineIndex) -> Diagnostic {
let range = if let Some(span) = &warning.span {
Range::new(
Position::new(span.start_line, span.start_col),
Position::new(span.end_line, span.end_col),
)
} else {
resolve_path(text, index, &warning.path)
};
let severity = match warning.severity {
LintSeverity::Error => DiagnosticSeverity::ERROR,
LintSeverity::Warning => DiagnosticSeverity::WARNING,
LintSeverity::Info => DiagnosticSeverity::INFORMATION,
LintSeverity::Hint => DiagnosticSeverity::HINT,
};
Diagnostic {
range,
severity: Some(severity),
code: Some(NumberOrString::String(warning.rule.to_string())),
source: Some("rsigma".to_string()),
message: warning.message.clone(),
..Default::default()
}
}
fn parse_error_to_diagnostic(message: &str, text: &str, index: &LineIndex) -> Diagnostic {
let range =
extract_range_from_error(message, index).unwrap_or_else(|| resolve_path(text, index, "/"));
Diagnostic {
range,
severity: Some(DiagnosticSeverity::ERROR),
code: Some(NumberOrString::String("parse_error".to_string())),
source: Some("rsigma".to_string()),
message: message.to_string(),
..Default::default()
}
}
fn compile_error_to_diagnostic(message: &str, text: &str, index: &LineIndex) -> Diagnostic {
let range = resolve_compile_error_range(message, text, index);
Diagnostic {
range,
severity: Some(DiagnosticSeverity::ERROR),
code: Some(NumberOrString::String("compile_error".to_string())),
source: Some("rsigma".to_string()),
message: message.to_string(),
..Default::default()
}
}
fn resolve_compile_error_range(message: &str, text: &str, index: &LineIndex) -> Range {
if let Some(name) = message.strip_prefix("unknown detection identifier: ") {
let name = name.trim();
if !name.is_empty() {
for (i, line) in text.lines().enumerate() {
let trimmed = line.trim();
let indent = line.len() - trimmed.len();
if indent > 0
&& trimmed.starts_with("condition:")
&& let Some(offset) = line.find(name)
{
return Range::new(
Position::new(i as u32, offset as u32),
Position::new(i as u32, (offset + name.len()) as u32),
);
}
}
}
}
resolve_path(text, index, "/detection/condition")
}
fn extract_range_from_error(message: &str, index: &LineIndex) -> Option<Range> {
let line_idx = message.find("line ")?;
let after_line = &message[line_idx + 5..];
let line_end = after_line
.find(|c: char| !c.is_ascii_digit())
.unwrap_or(after_line.len());
let line: u32 = after_line[..line_end].parse().ok()?;
let line = line.saturating_sub(1);
let col = if let Some(col_idx) = message.find("column ") {
let after_col = &message[col_idx + 7..];
let col_end = after_col
.find(|c: char| !c.is_ascii_digit())
.unwrap_or(after_col.len());
after_col[..col_end].parse::<u32>().unwrap_or(0)
} else {
0
};
let (_, line_end_offset) = index.line_range(line as usize);
Some(Range::new(
Position::new(line, col),
index.position_of(line_end_offset),
))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn extract_range_from_yaml_error() {
let msg = "invalid type: string \"foo\", expected a mapping at line 5 column 3";
let text = "title: Test\nstatus: test\nlevel: high\nlogsource:\n foo\n";
let index = LineIndex::new(text);
let range = extract_range_from_error(msg, &index).unwrap();
assert_eq!(range.start.line, 4); assert_eq!(range.start.character, 3);
}
#[test]
fn extract_range_line_only() {
let msg = "unexpected character at line 2";
let text = "title: Test\nbad\n";
let index = LineIndex::new(text);
let range = extract_range_from_error(msg, &index).unwrap();
assert_eq!(range.start.line, 1); assert_eq!(range.start.character, 0); }
#[test]
fn extract_range_no_match() {
let msg = "something went wrong";
let text = "title: Test\n";
let index = LineIndex::new(text);
assert!(extract_range_from_error(msg, &index).is_none());
}
#[test]
fn compile_error_unknown_detection_highlights_name() {
let text = "\
title: Test
detection:
selection:
User: admin
condition: selecton
";
let index = LineIndex::new(text);
let range =
resolve_compile_error_range("unknown detection identifier: selecton", text, &index);
assert_eq!(range.start.line, 4);
let line = text.lines().nth(4).unwrap();
let offset = line.find("selecton").unwrap();
assert_eq!(range.start.character, offset as u32);
assert_eq!(range.end.character, (offset + "selecton".len()) as u32);
}
#[test]
fn compile_error_fallback_to_condition_line() {
let text = "\
title: Test
detection:
selection:
User: admin
condition: selection
";
let index = LineIndex::new(text);
let range =
resolve_compile_error_range("invalid modifier combination: something", text, &index);
assert_eq!(range.start.line, 4);
}
}