Skip to content

Commit 57dde57

Browse files
authored
feat: add tag linter. (#304)
Add a tag linter that can be configured to use either a list of allowed strings, or a regexp. If both are provided in the config file the allowed list is preferred by the "check" command as it is more explicit.
1 parent 519d3fe commit 57dde57

File tree

9 files changed

+475
-11
lines changed

9 files changed

+475
-11
lines changed

cli/src/commands/check.rs

+15
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,21 @@ pub fn exec_check(
182182
linters::rule_name(re)?.error(config.rule_name.error));
183183
}
184184

185+
// Prefer allowed list over the regex, as it is more explicit.
186+
if !config.tags.allowed.is_empty() {
187+
compiler.add_linter(
188+
linters::tags_allowed(config.tags.allowed.clone())
189+
.error(config.tags.error));
190+
} else if let Some(re) = config
191+
.tags
192+
.regexp
193+
.as_ref()
194+
.filter(|re| !re.is_empty()) {
195+
compiler.add_linter(
196+
linters::tag_regex(re)?.error(config.tags.error)
197+
);
198+
}
199+
185200
compiler.colorize_errors(io::stdout().is_tty());
186201

187202
match compiler.add_source(src) {

cli/src/config.rs

+22-5
Original file line numberDiff line numberDiff line change
@@ -69,30 +69,47 @@ pub enum MetaValueType {
6969
Hash,
7070
}
7171

72-
/// Format specific configuration information.
72+
/// Linter specific configuration information.
7373
#[derive(Deserialize, Serialize, Debug, Default)]
7474
#[serde(deny_unknown_fields)]
7575
pub struct CheckConfig {
76-
/// Meta specific formatting information.
76+
/// Meta specific linting information.
7777
// Note: Using a BTreeMap here because we want a consistent ordering when
7878
// we iterate over it, so that warnings always appear in the same order.
7979
pub metadata: BTreeMap<String, MetadataConfig>,
80-
/// Rule name formatting information.
80+
/// Rule name linting information.
8181
pub rule_name: RuleNameConfig,
82+
/// Tag linting information.
83+
pub tags: TagConfig,
8284
}
8385

84-
/// Format specific configuration information.
86+
/// Allowed tag names in the linter.
87+
#[derive(Deserialize, Serialize, Debug, Default)]
88+
#[serde(deny_unknown_fields)]
89+
pub struct TagConfig {
90+
/// List of allowed tags.
91+
pub allowed: Vec<String>,
92+
/// Regexp that must match all tags.
93+
pub regexp: Option<String>,
94+
/// If `true`, an incorrect tag name will raise an error instead of a
95+
/// warning.
96+
#[serde(default)]
97+
pub error: bool,
98+
}
99+
100+
/// Rule name linter specific configuration information.
85101
#[derive(Deserialize, Serialize, Debug, Default)]
86102
#[serde(deny_unknown_fields)]
87103
pub struct RuleNameConfig {
88104
/// Regexp used to validate the rule name.
89105
pub regexp: Option<String>,
90-
/// If `true`, an incorrect rule anme will raise an error instead of a
106+
/// If `true`, an incorrect rule name will raise an error instead of a
91107
/// warning.
92108
#[serde(default)]
93109
pub error: bool,
94110
}
95111

112+
/// Metadata linter specific configuration information.
96113
#[derive(Deserialize, Serialize, Debug)]
97114
#[serde(deny_unknown_fields)]
98115
pub struct MetadataConfig {

cli/src/tests/check.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ fn check_rule_name_error() {
153153
.success()
154154
.stdout(
155155
r#"[ FAIL ] src/tests/testdata/foo.yar
156-
error[E038]: rule name does not match regex `APT_.+`
156+
error[E039]: rule name does not match regex `APT_.+`
157157
--> src/tests/testdata/foo.yar:1:6
158158
|
159159
1 | rule foo : bar baz {
@@ -187,7 +187,7 @@ fn config_error() {
187187
.failure()
188188
.stderr(
189189
predicate::str::contains(
190-
r#"error: unknown field: found `foo`, expected ``metadata` or `rule_name`` for key "default.check.foo""#,
190+
r#"error: unknown field: found `foo`, expected `one of `metadata`, `rule_name`, `tags`` for key "default.check.foo""#,
191191
)
192192
);
193193
}

lib/src/compiler/errors.rs

+67-3
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ pub enum CompileError {
6868
InvalidRegexp(Box<InvalidRegexp>),
6969
InvalidRegexpModifier(Box<InvalidRegexpModifier>),
7070
InvalidRuleName(Box<InvalidRuleName>),
71+
InvalidTag(Box<InvalidTag>),
7172
InvalidUTF8(Box<InvalidUTF8>),
7273
MethodNotAllowedInWith(Box<MethodNotAllowedInWith>),
7374
MismatchingTypes(Box<MismatchingTypes>),
@@ -84,6 +85,7 @@ pub enum CompileError {
8485
UnknownIdentifier(Box<UnknownIdentifier>),
8586
UnknownModule(Box<UnknownModule>),
8687
UnknownPattern(Box<UnknownPattern>),
88+
UnknownTag(Box<UnknownTag>),
8789
UnusedPattern(Box<UnusedPattern>),
8890
WrongArguments(Box<WrongArguments>),
8991
WrongType(Box<WrongType>),
@@ -685,7 +687,7 @@ pub struct InvalidMetadata {
685687
/// ## Example
686688
///
687689
/// ```text
688-
/// warning[missing_metadata]: required metadata is missing
690+
/// error[E038]: required metadata is missing
689691
/// --> test.yar:12:6
690692
/// |
691693
/// 12 | rule pants {
@@ -716,7 +718,7 @@ pub struct MissingMetadata {
716718
/// ## Example
717719
///
718720
/// ```text
719-
/// warning[invalid_rule_name]: rule name does not match regex `APT_.*`
721+
/// error[E039]: rule name does not match regex `APT_.*`
720722
/// --> test.yar:13:6
721723
/// |
722724
/// 13 | rule pants {
@@ -726,7 +728,7 @@ pub struct MissingMetadata {
726728
#[derive(ErrorStruct, Clone, Debug, PartialEq, Eq)]
727729
#[associated_enum(CompileError)]
728730
#[error(
729-
code = "E038",
731+
code = "E039",
730732
title = "rule name does not match regex `{regex}`"
731733
)]
732734
#[label(
@@ -739,6 +741,68 @@ pub struct InvalidRuleName {
739741
regex: String,
740742
}
741743

744+
/// Unknown tag. This is only used if the compiler is configured to check
745+
/// for required tags (see: [`crate::linters::Tags`]).
746+
///
747+
/// ## Example
748+
///
749+
/// ```text
750+
/// error[E040]: tag not in allowed list
751+
/// --> rules/test.yara:1:10
752+
/// |
753+
/// 1 | rule a : foo {
754+
/// | ^^^ tag `foo` not in allowed list
755+
/// |
756+
/// = note: Allowed tags: test, bar
757+
/// ```
758+
#[derive(ErrorStruct, Clone, Debug, PartialEq, Eq)]
759+
#[associated_enum(CompileError)]
760+
#[error(
761+
code = "E040",
762+
title = "tag not in allowed list"
763+
)]
764+
#[label(
765+
"tag `{name}` not in allowed list",
766+
tag_loc
767+
)]
768+
#[footer(note)]
769+
pub struct UnknownTag {
770+
report: Report,
771+
tag_loc: CodeLoc,
772+
name: String,
773+
note: Option<String>,
774+
}
775+
776+
/// Tag does not match regex. This is only used if the compiler is configured to
777+
/// check for it (see: [`crate::linters::Tags`]).
778+
///
779+
/// ## Example
780+
///
781+
/// ```text
782+
/// error[E041]: tag does not match regex `bar`
783+
/// --> rules/test.yara:1:10
784+
/// |
785+
/// 1 | rule a : foo {
786+
/// | ^^^ tag `foo` does not match regex `bar`
787+
/// |
788+
/// ```
789+
#[derive(ErrorStruct, Clone, Debug, PartialEq, Eq)]
790+
#[associated_enum(CompileError)]
791+
#[error(
792+
code = "E041",
793+
title = "tag does not match regex `{regex}`"
794+
)]
795+
#[label(
796+
"tag `{name}` does not match regex `{regex}`",
797+
tag_loc
798+
)]
799+
pub struct InvalidTag {
800+
report: Report,
801+
tag_loc: CodeLoc,
802+
name: String,
803+
regex: String,
804+
}
805+
742806
/// A custom error has occurred.
743807
#[derive(ErrorStruct, Clone, Debug, PartialEq, Eq)]
744808
#[associated_enum(CompileError)]

lib/src/compiler/linters.rs

+151
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ pub(crate) trait LinterInternal {
3737
pub(crate) enum LinterResult {
3838
Ok,
3939
Warn(Warning),
40+
Warns(Vec<Warning>),
4041
Err(CompileError),
4142
}
4243

@@ -114,6 +115,145 @@ impl LinterInternal for RuleName {
114115

115116
type Predicate<'a> = dyn Fn(&Meta) -> bool + 'a;
116117

118+
/// A linter that ensures tags meet specified requirements in either an allowed
119+
/// list of tags or in a regex.
120+
///
121+
/// ```
122+
/// # use yara_x::Compiler;
123+
/// use yara_x::linters;
124+
/// let mut compiler = Compiler::new();
125+
/// let warnings = compiler
126+
/// .add_linter(linters::tags_allowed(vec!["foo".to_string(), "bar".to_string()]))
127+
/// // This produces a warning because the rule tags are not from the
128+
/// // allowed list
129+
/// .add_source(r#"rule foo : test { strings: $foo = "foo" condition: $foo }"#)
130+
/// .unwrap()
131+
/// .warnings();
132+
///
133+
/// assert_eq!(
134+
/// warnings[0].to_string(),
135+
/// r#"warning[unknown_tag]: tag not in allowed list
136+
/// --> line:1:12
137+
/// |
138+
/// 1 | rule foo : test { strings: $foo = "foo" condition: $foo }
139+
/// | ---- tag `test` not in allowed list
140+
/// |
141+
/// = note: allowed tags: foo, bar"#);
142+
pub struct Tags {
143+
allow_list: Vec<String>,
144+
regex: Option<String>,
145+
compiled_regex: Option<Regex>,
146+
error: bool,
147+
}
148+
149+
impl Tags {
150+
/// A list of strings that tags for each rule must match one of.
151+
pub(crate) fn from_list(list: Vec<String>) -> Self {
152+
Self {
153+
allow_list: list,
154+
regex: None,
155+
compiled_regex: None,
156+
error: false,
157+
}
158+
}
159+
160+
/// Regular expression that tags for each rule must match.
161+
pub(crate) fn from_regex<R: Into<String>>(
162+
regex: R,
163+
) -> Result<Self, regex::Error> {
164+
let regex = regex.into();
165+
let compiled_regex = Some(Regex::new(regex.as_str())?);
166+
let tags = Self {
167+
allow_list: Vec::new(),
168+
regex: Some(regex),
169+
compiled_regex,
170+
error: false,
171+
};
172+
Ok(tags)
173+
}
174+
175+
/// Specifies whether the linter should produce an error instead of a
176+
/// warning.
177+
///
178+
/// By default, the linter raises warnings about tags that don't match the
179+
/// regular expression. This setting allows turning such warnings into
180+
/// errors.
181+
pub fn error(mut self, yes: bool) -> Self {
182+
self.error = yes;
183+
self
184+
}
185+
}
186+
187+
impl LinterInternal for Tags {
188+
fn check(
189+
&self,
190+
report_builder: &ReportBuilder,
191+
rule: &ast::Rule,
192+
) -> LinterResult {
193+
if rule.tags.is_none() {
194+
return LinterResult::Ok;
195+
}
196+
197+
let mut results: Vec<Warning> = Vec::new();
198+
let tags = rule.tags.as_ref().unwrap();
199+
if !self.allow_list.is_empty() {
200+
for tag in tags.iter() {
201+
if !self.allow_list.contains(&tag.name.to_string()) {
202+
if self.error {
203+
return LinterResult::Err(errors::UnknownTag::build(
204+
report_builder,
205+
tag.span().into(),
206+
tag.name.to_string(),
207+
Some(format!(
208+
"allowed tags: {}",
209+
self.allow_list.join(", ")
210+
)),
211+
));
212+
} else {
213+
results.push(warnings::UnknownTag::build(
214+
report_builder,
215+
tag.span().into(),
216+
tag.name.to_string(),
217+
Some(format!(
218+
"allowed tags: {}",
219+
self.allow_list.join(", ")
220+
)),
221+
));
222+
}
223+
}
224+
}
225+
} else {
226+
let compiled_regex = self.compiled_regex.as_ref().unwrap();
227+
228+
for tag in tags.iter() {
229+
if !compiled_regex.is_match(tag.name) {
230+
if self.error {
231+
return LinterResult::Err(errors::InvalidTag::build(
232+
report_builder,
233+
tag.span().into(),
234+
tag.name.to_string(),
235+
self.regex.as_ref().unwrap().clone(),
236+
));
237+
} else {
238+
results.push(warnings::InvalidTag::build(
239+
report_builder,
240+
tag.span().into(),
241+
tag.name.to_string(),
242+
self.regex.as_ref().unwrap().clone(),
243+
));
244+
}
245+
}
246+
}
247+
}
248+
249+
if results.is_empty() {
250+
LinterResult::Ok
251+
} else {
252+
LinterResult::Warns(results)
253+
}
254+
}
255+
}
256+
117257
/// A linter that validates metadata entries.
118258
///
119259
/// ```
@@ -286,6 +426,17 @@ impl LinterInternal for Metadata<'_> {
286426
}
287427
}
288428

429+
/// Creates a tag linter from a list of allowed tags.
430+
pub fn tags_allowed(list: Vec<String>) -> Tags {
431+
Tags::from_list(list)
432+
}
433+
434+
/// Creates a tag linter that makes sure that each tag matches the given regular
435+
/// expression.
436+
pub fn tag_regex<R: Into<String>>(regex: R) -> Result<Tags, Error> {
437+
Tags::from_regex(regex)
438+
}
439+
289440
/// Creates a linter that validates metadata entries.
290441
///
291442
/// See [`Metadata`] for details.

lib/src/compiler/mod.rs

+5
Original file line numberDiff line numberDiff line change
@@ -1189,6 +1189,11 @@ impl Compiler<'_> {
11891189
LinterResult::Warn(warning) => {
11901190
self.warnings.add(|| warning);
11911191
}
1192+
LinterResult::Warns(warnings) => {
1193+
for warning in warnings {
1194+
self.warnings.add(|| warning);
1195+
}
1196+
}
11921197
LinterResult::Err(err) => return Err(err),
11931198
}
11941199
}

0 commit comments

Comments
 (0)