diff --git a/ls/editors/code/package.json b/ls/editors/code/package.json index 54ae88d4b..b683b270b 100644 --- a/ls/editors/code/package.json +++ b/ls/editors/code/package.json @@ -69,6 +69,36 @@ "description": "Puts the opening curly brace on a new line." } } + }, + "YARA.metadataValidation": { + "type": "array", + "default": [ + { + "identifier": "author", + "required": false, + "type": "string" + } + ], + "description": "Rules for validating metadata in YARA rules.", + "items": { + "type": "object", + "properties": { + "identifier": { + "type": "string", + "description": "The metadata identifier (e.g., `author`, `version`)." + }, + "required": { + "type": "boolean", + "default": false, + "description": "Specifies if the metadata is required." + }, + "type": { + "type": "string", + "enum": ["string", "integer", "float", "bool"], + "description": "The expected type of the metadata value." + } + } + } } } }, diff --git a/ls/src/features/diagnostics.rs b/ls/src/features/diagnostics.rs index 67e1fc7cd..e25955368 100644 --- a/ls/src/features/diagnostics.rs +++ b/ls/src/features/diagnostics.rs @@ -3,13 +3,16 @@ use std::sync::Arc; use async_lsp::lsp_types::{ Diagnostic, DiagnosticRelatedInformation, Location, Range, Url, }; +#[cfg(feature = "full-compiler")] +use async_lsp::lsp_types::{DiagnosticSeverity, NumberOrString}; use dashmap::mapref::one::Ref; use serde::{Deserialize, Serialize}; use crate::documents::{document::Document, storage::DocumentStorage}; +use crate::server::MetadataValidationRule; #[cfg(feature = "full-compiler")] -use async_lsp::lsp_types::{DiagnosticSeverity, NumberOrString}; +use yara_x::linters; #[cfg(feature = "full-compiler")] use yara_x::{Compiler, SourceCode}; @@ -29,6 +32,7 @@ pub struct Patch { pub fn diagnostics( documents: Arc, uri: Url, + meta_validation_rules: Vec, ) -> Vec { #[allow(unused_mut)] let mut diagnostics: Vec = Vec::new(); @@ -37,7 +41,7 @@ pub fn diagnostics( if let Some(doc) = doc { #[cfg(feature = "full-compiler")] - diagnostics.extend(compiler_diagnostics(doc)); + diagnostics.extend(compiler_diagnostics(doc, meta_validation_rules)); } diagnostics @@ -52,11 +56,54 @@ pub fn diagnostics( #[cfg(feature = "full-compiler")] pub fn compiler_diagnostics( document: Ref<'_, Url, Document>, + metadata_validation_rules: Vec, ) -> Vec { let source_code = SourceCode::from(document.text.as_str()) .with_origin(document.uri.clone()); let mut compiler = Compiler::new(); + + for validation_rule in metadata_validation_rules { + let mut linter = linters::metadata(&validation_rule.identifier) + .required(validation_rule.required); + + if let Some(ty) = validation_rule.ty { + let predicate = match ty.as_str() { + "string" => |meta: &yara_x_parser::ast::Meta| { + matches!( + meta.value, + yara_x_parser::ast::MetaValue::String(_) + ) + }, + "integer" => |meta: &yara_x_parser::ast::Meta| { + matches!( + meta.value, + yara_x_parser::ast::MetaValue::Integer(_) + ) + }, + "float" => |meta: &yara_x_parser::ast::Meta| { + matches!( + meta.value, + yara_x_parser::ast::MetaValue::Float(_) + ) + }, + "bool" => |meta: &yara_x_parser::ast::Meta| { + matches!( + meta.value, + yara_x_parser::ast::MetaValue::Bool(_) + ) + }, + _ => continue, + }; + linter = linter.validator( + predicate, + format!("`{}` must be a `{}`", validation_rule.identifier, ty), + ); + } + + compiler.add_linter(linter); + } + // VSCode don't handle well error messages with too many columns. compiler.errors_max_width(110); // Attempt to compile the source. We don't care about the result diff --git a/ls/src/server.rs b/ls/src/server.rs index 9c9160348..dd66fc4b4 100644 --- a/ls/src/server.rs +++ b/ls/src/server.rs @@ -36,6 +36,7 @@ use async_lsp::lsp_types::{ use async_lsp::router::Router; use async_lsp::{ClientSocket, LanguageClient, LanguageServer, ResponseError}; use futures::future::BoxFuture; +use serde::Deserialize; use crate::documents::storage::DocumentStorage; use crate::features::code_action::code_actions; @@ -54,6 +55,20 @@ use crate::features::semantic_tokens::{ semantic_tokens, SEMANTIC_TOKEN_MODIFIERS, SEMANTIC_TOKEN_TYPES, }; +/// Rule that describes a how to validate a metadata entry in a rule. +#[derive(Deserialize, Debug, Clone, Default)] +#[serde(rename_all = "camelCase")] +pub struct MetadataValidationRule { + /// Metadata identifier + pub identifier: String, + /// Whether the metadata entry is required or not. + #[serde(default)] + pub required: bool, + /// Type of the metadata entry. + #[serde(rename = "type")] + pub ty: Option, +} + /// Represents a YARA language server. pub struct YARALanguageServer { /// Client socket for communication with the Development Tool. @@ -408,15 +423,20 @@ impl LanguageServer for YARALanguageServer { { let uri = params.text_document.uri; let documents = Arc::clone(&self.documents); + let client = self.client.clone(); Box::pin(async move { + let meta_specs = + Self::get_meta_validation_rules(client.clone(), uri.clone()) + .await; + Ok(DocumentDiagnosticReportResult::Report( async_lsp::lsp_types::DocumentDiagnosticReport::Full( RelatedFullDocumentDiagnosticReport { full_document_diagnostic_report: FullDocumentDiagnosticReport { result_id: None, - items: diagnostics(documents, uri), + items: diagnostics(documents, uri, meta_specs), }, related_documents: None, }, @@ -543,18 +563,51 @@ impl YARALanguageServer { }) } + async fn get_meta_validation_rules( + mut client: ClientSocket, + scope_uri: Url, + ) -> Vec { + client + .configuration(ConfigurationParams { + items: vec![ConfigurationItem { + scope_uri: Some(scope_uri), + section: Some("YARA.metadataValidation".to_string()), + }], + }) + .await + .ok() + .and_then(|mut v| v.pop()) + .and_then(|value| { + serde_json::from_value::>(value) + .ok() + }) + .unwrap_or_default() + } + /// Sends diagnostics for specific document if publish model is used. fn publish_diagnostics(&mut self, uri: &Url) { if self.should_send_diagnostics { - let _ = - self.client.publish_diagnostics(PublishDiagnosticsParams { + let documents = Arc::clone(&self.documents); + let mut client = self.client.clone(); + let uri = uri.clone(); + + tokio::spawn(async move { + let meta_validation_rules = Self::get_meta_validation_rules( + client.clone(), + uri.clone(), + ) + .await; + + client.publish_diagnostics(PublishDiagnosticsParams { uri: uri.clone(), diagnostics: diagnostics( - Arc::clone(&self.documents), - uri.clone(), + documents, + uri, + meta_validation_rules, ), version: None, - }); + }) + }); } } }