boreal_parser/
file.rs

1//! Types related to YARA files.
2use std::ops::Range;
3
4use nom::branch::alt;
5use nom::bytes::complete::take_till1;
6use nom::character::complete::char;
7use nom::combinator::map;
8use nom::sequence::delimited;
9use nom::Parser;
10use nom::{combinator::cut, sequence::preceded};
11
12use crate::rule::Rule;
13
14use super::rule::rule;
15use super::{
16    nom_recipes::{ltrim, rtrim, textual_tag as ttag},
17    types::{Input, ParseResult},
18};
19
20/// A parsed Yara file.
21#[derive(Clone, Debug, PartialEq)]
22pub struct YaraFile {
23    /// List of components contained in the file.
24    ///
25    /// This enum form is required to keep the order in which rules and imports
26    /// appear in the file. This is needed to properly resolve symbols to a rule
27    /// or a module, or to properly use rules included by wildcard use of rule
28    /// names in conditions.
29    pub components: Vec<YaraFileComponent>,
30}
31
32/// A top-level component of a Yara file.
33#[derive(Clone, Debug, PartialEq)]
34pub enum YaraFileComponent {
35    /// A Yara rule
36    Rule(Box<Rule>),
37    /// A module import
38    Import(Import),
39    /// An include of another file
40    Include(Include),
41}
42
43/// An import inside a Yara file.
44#[derive(Clone, Debug, PartialEq, Eq)]
45pub struct Import {
46    /// The name being imported
47    pub name: String,
48    /// The span covering the whole import, ie `import "foo"`
49    pub span: Range<usize>,
50}
51
52/// An import inside a Yara file.
53#[derive(Clone, Debug, PartialEq, Eq)]
54pub struct Include {
55    /// Path to the file being included
56    pub path: String,
57    /// The span covering the whole include, ie `include "./bar.yar"`
58    pub span: Range<usize>,
59}
60
61/// Parse a full YARA file.
62///
63/// # Errors
64///
65/// If the input cannot be parsed properly and entirely as a list
66/// of yara rules, an error is returned.
67pub(crate) fn parse_yara_file(input: Input) -> ParseResult<YaraFile> {
68    let (mut input, ()) = ltrim(input)?;
69
70    let mut file = YaraFile {
71        components: Vec::new(),
72    };
73    while !input.is_empty() {
74        let (i, component) = alt((
75            map(include_file, YaraFileComponent::Include),
76            map(import, YaraFileComponent::Import),
77            map(rule, |r| YaraFileComponent::Rule(Box::new(r))),
78        ))
79        .parse(input)?;
80        file.components.push(component);
81        input = i;
82    }
83
84    Ok((input, file))
85}
86
87/// Parse an include declaration
88fn include_file(input: Input) -> ParseResult<Include> {
89    let start = input.pos();
90
91    let (input, path) = rtrim(preceded(
92        rtrim(ttag("include")),
93        cut(delimited(
94            char('"'),
95            map(take_till1(|c| c == '"'), |v: Input| v.to_string()),
96            char('"'),
97        )),
98    ))(input)?;
99
100    Ok((
101        input,
102        Include {
103            path,
104            span: input.get_span_from(start),
105        },
106    ))
107}
108
109/// Parse an import declaration
110fn import(input: Input) -> ParseResult<Import> {
111    let start = input.pos();
112
113    let (input, name) = rtrim(preceded(
114        rtrim(ttag("import")),
115        cut(delimited(
116            char('"'),
117            map(take_till1(|c| c == '"'), |v: Input| v.to_string()),
118            char('"'),
119        )),
120    ))(input)?;
121
122    Ok((
123        input,
124        Import {
125            name,
126            span: input.get_span_from(start),
127        },
128    ))
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134    use crate::expression::{Expression, ExpressionKind};
135    use crate::test_helpers::{parse, parse_err, test_public_type};
136
137    #[test]
138    fn test_parse_yara_file() {
139        parse(
140            parse_yara_file,
141            "  global rule c { condition: false }",
142            "",
143            YaraFile {
144                components: vec![YaraFileComponent::Rule(Box::new(Rule {
145                    name: "c".to_owned(),
146                    name_span: 14..15,
147                    condition: Expression {
148                        expr: ExpressionKind::Boolean(false),
149                        span: 29..34,
150                    },
151                    tags: Vec::new(),
152                    metadatas: Vec::new(),
153                    variables: Vec::new(),
154                    is_private: false,
155                    is_global: true,
156                }))],
157            },
158        );
159
160        parse(
161            parse_yara_file,
162            r#" import "pe"
163                global rule c { condition: false }
164                import "foo"
165                import "quux"
166                rule d { condition: true }
167                "#,
168            "",
169            YaraFile {
170                components: vec![
171                    YaraFileComponent::Import(Import {
172                        name: "pe".to_owned(),
173                        span: 1..12,
174                    }),
175                    YaraFileComponent::Rule(Box::new(Rule {
176                        name: "c".to_owned(),
177                        name_span: 41..42,
178                        condition: Expression {
179                            expr: ExpressionKind::Boolean(false),
180                            span: 56..61,
181                        },
182                        tags: Vec::new(),
183                        metadatas: Vec::new(),
184                        variables: Vec::new(),
185                        is_private: false,
186                        is_global: true,
187                    })),
188                    YaraFileComponent::Import(Import {
189                        name: "foo".to_owned(),
190                        span: 80..92,
191                    }),
192                    YaraFileComponent::Import(Import {
193                        name: "quux".to_owned(),
194                        span: 109..122,
195                    }),
196                    YaraFileComponent::Rule(Box::new(Rule {
197                        name: "d".to_owned(),
198                        name_span: 144..145,
199                        condition: Expression {
200                            expr: ExpressionKind::Boolean(true),
201                            span: 159..163,
202                        },
203                        tags: Vec::new(),
204                        metadatas: Vec::new(),
205                        variables: Vec::new(),
206                        is_private: false,
207                        is_global: false,
208                    })),
209                ],
210            },
211        );
212        parse(parse_yara_file, "", "", YaraFile { components: vec![] });
213        parse(
214            parse_yara_file,
215            " /* removed */ ",
216            "",
217            YaraFile { components: vec![] },
218        );
219        parse(
220            parse_yara_file,
221            "include \"v\"\ninclude\"i\"",
222            "",
223            YaraFile {
224                components: vec![
225                    YaraFileComponent::Include(Include {
226                        path: "v".to_owned(),
227                        span: 0..11,
228                    }),
229                    YaraFileComponent::Include(Include {
230                        path: "i".to_owned(),
231                        span: 12..22,
232                    }),
233                ],
234            },
235        );
236
237        parse_err(parse_yara_file, "rule");
238        parse_err(parse_yara_file, "rule a { condition: true } b");
239        parse_err(parse_yara_file, " /*");
240    }
241
242    #[test]
243    fn test_public_types() {
244        test_public_type(
245            parse_yara_file(Input::new(
246                r#"
247import "a"
248include "b"
249
250rule a { condition: true }
251"#,
252            ))
253            .unwrap(),
254        );
255    }
256}