use regex::{Error, Regex};
use yara_x_parser::ast::{self, Meta, WithSpan};
use crate::compiler::Warning;
use crate::compiler::report::ReportBuilder;
use crate::compiler::{errors, warnings};
use crate::errors::CompileError;
#[allow(private_bounds)]
pub trait Linter: LinterInternal {}
impl<T: LinterInternal> Linter for T {}
pub(crate) trait LinterInternal {
fn check(
&self,
report_builder: &ReportBuilder,
rule: &ast::Rule,
) -> LinterResult;
}
pub(crate) enum LinterResult {
Ok,
Warn(Warning),
Warns(Vec<Warning>),
Err(CompileError),
}
pub struct RuleName {
regex: String,
error: bool,
compiled_regex: Regex,
}
impl RuleName {
fn new<R: Into<String>>(regex: R) -> Result<Self, regex::Error> {
let regex = regex.into();
let compiled_regex = Regex::new(regex.as_str())?;
Ok(Self { regex, compiled_regex, error: false })
}
pub fn error(mut self, yes: bool) -> Self {
self.error = yes;
self
}
}
impl LinterInternal for RuleName {
fn check(
&self,
report_builder: &ReportBuilder,
rule: &ast::Rule,
) -> LinterResult {
if !self.compiled_regex.is_match(rule.identifier.name) {
if self.error {
LinterResult::Err(errors::InvalidRuleName::build(
report_builder,
report_builder.span_to_code_loc(rule.identifier.span()),
self.regex.clone(),
))
} else {
LinterResult::Warn(warnings::InvalidRuleName::build(
report_builder,
report_builder.span_to_code_loc(rule.identifier.span()),
self.regex.clone(),
))
}
} else {
LinterResult::Ok
}
}
}
type Predicate<'a> = dyn Fn(&Meta) -> bool + 'a;
pub struct Tags {
allowed: Vec<String>,
regex: Option<String>,
compiled_regex: Option<Regex>,
error: bool,
}
impl Tags {
pub(crate) fn from_list(list: Vec<String>) -> Self {
Self { allowed: list, regex: None, compiled_regex: None, error: false }
}
pub(crate) fn from_regex<R: Into<String>>(
regex: R,
) -> Result<Self, regex::Error> {
let regex = regex.into();
let compiled_regex = Some(Regex::new(regex.as_str())?);
let tags = Self {
allowed: Vec::new(),
regex: Some(regex),
compiled_regex,
error: false,
};
Ok(tags)
}
pub fn error(mut self, yes: bool) -> Self {
self.error = yes;
self
}
}
impl LinterInternal for Tags {
fn check(
&self,
report_builder: &ReportBuilder,
rule: &ast::Rule,
) -> LinterResult {
if rule.tags.is_none() {
return LinterResult::Ok;
}
let mut results: Vec<Warning> = Vec::new();
let tags = rule.tags.as_ref().unwrap();
if !self.allowed.is_empty() {
for tag in tags.iter() {
if !self.allowed.contains(&tag.name.to_string()) {
if self.error {
return LinterResult::Err(errors::UnknownTag::build(
report_builder,
report_builder.span_to_code_loc(tag.span()),
tag.name.to_string(),
Some(format!(
"allowed tags: {}",
self.allowed.join(", ")
)),
));
} else {
results.push(warnings::UnknownTag::build(
report_builder,
report_builder.span_to_code_loc(tag.span()),
tag.name.to_string(),
Some(format!(
"allowed tags: {}",
self.allowed.join(", ")
)),
));
}
}
}
} else {
let compiled_regex = self.compiled_regex.as_ref().unwrap();
for tag in tags.iter() {
if !compiled_regex.is_match(tag.name) {
if self.error {
return LinterResult::Err(errors::InvalidTag::build(
report_builder,
report_builder.span_to_code_loc(tag.span()),
tag.name.to_string(),
self.regex.as_ref().unwrap().clone(),
));
} else {
results.push(warnings::InvalidTag::build(
report_builder,
report_builder.span_to_code_loc(tag.span()),
tag.name.to_string(),
self.regex.as_ref().unwrap().clone(),
));
}
}
}
}
if results.is_empty() {
LinterResult::Ok
} else {
LinterResult::Warns(results)
}
}
}
pub struct Metadata<'a> {
identifier: String,
predicate: Option<Box<Predicate<'a>>>,
required: bool,
error: bool,
message: Option<String>,
note: Option<String>,
}
impl<'a> Metadata<'a> {
fn new<I: Into<String>>(identifier: I) -> Self {
Self {
identifier: identifier.into(),
predicate: None,
required: false,
error: false,
message: None,
note: None,
}
}
pub fn required(mut self, yes: bool) -> Self {
self.required = yes;
self
}
pub fn error(mut self, yes: bool) -> Self {
self.error = yes;
self
}
pub fn validator<P, M>(mut self, predicate: P, message: M) -> Self
where
P: Fn(&Meta) -> bool + 'a,
M: Into<String>,
{
self.predicate = Some(Box::new(predicate));
self.message = Some(message.into());
self
}
}
impl LinterInternal for Metadata<'_> {
fn check(
&self,
report_builder: &ReportBuilder,
rule: &ast::Rule,
) -> LinterResult {
let mut found = false;
for meta in rule.meta.iter().flatten() {
if meta.identifier.name == self.identifier.as_str() {
if let Some(predicate) = &self.predicate
&& !predicate(meta)
{
return if self.error {
LinterResult::Err(errors::InvalidMetadata::build(
report_builder,
meta.identifier.name.to_string(),
report_builder.span_to_code_loc(meta.value.span()),
self.message
.clone()
.unwrap_or("invalid metadata".to_string()),
))
} else {
LinterResult::Warn(warnings::InvalidMetadata::build(
report_builder,
meta.identifier.name.to_string(),
report_builder.span_to_code_loc(meta.value.span()),
self.message
.clone()
.unwrap_or("invalid metadata".to_string()),
))
};
}
found = true;
}
}
if self.required && !found {
return if self.error {
LinterResult::Err(errors::MissingMetadata::build(
report_builder,
report_builder.span_to_code_loc(rule.identifier.span()),
self.identifier.clone(),
self.note.clone(),
))
} else {
LinterResult::Warn(warnings::MissingMetadata::build(
report_builder,
report_builder.span_to_code_loc(rule.identifier.span()),
self.identifier.clone(),
self.note.clone(),
))
};
}
LinterResult::Ok
}
}
pub fn tags_allowed(list: Vec<String>) -> Tags {
Tags::from_list(list)
}
pub fn tag_regex<R: Into<String>>(regex: R) -> Result<Tags, Error> {
Tags::from_regex(regex)
}
pub fn metadata<'a, I: Into<String>>(identifier: I) -> Metadata<'a> {
Metadata::new(identifier)
}
pub fn rule_name<R: Into<String>>(regex: R) -> Result<RuleName, Error> {
RuleName::new(regex)
}