lib.rs (10071B)
1 #![deny(clippy::all)] 2 3 use napi::{ 4 anyhow::Context, 5 bindgen_prelude::{Buffer, Either3, Either4, Reference, SharedReference}, 6 Env, Result, 7 }; 8 use yara::{Compiler, MetadataValue, Rule as ExtYaraRule, Rules, Scanner}; 9 10 #[macro_use] 11 extern crate napi_derive; 12 13 #[napi(object)] 14 #[derive(Debug)] 15 pub struct YaraRule { 16 pub filename: Option<String>, 17 pub string: Option<String>, 18 pub namespace: Option<String>, 19 } 20 21 #[napi(object)] 22 #[derive(Debug)] 23 pub struct YaraVariable { 24 pub id: String, 25 pub value: Either4<i64, f64, bool, String>, 26 } 27 28 /// An interface to use yara with node in a stable manner using Rust 29 /// @public 30 #[napi] 31 pub struct YaraCompiler { 32 rules: Rules, 33 } 34 35 /// An interface to use yara with node in a stable manner using Rust 36 /// @public 37 #[napi] 38 pub struct YaraScanner { 39 scanner: SharedReference<YaraCompiler, Scanner<'static>>, 40 } 41 42 #[napi(object)] 43 #[derive(Debug)] 44 pub struct YaraRuleResult { 45 /// Name of the rule. 46 pub identifier: String, 47 /// Namespace of the rule. 48 pub namespace: String, 49 /// Metadatas of the rule. 50 pub metadatas: Vec<YaraRuleMetadata>, 51 /// Tags of the rule. 52 pub tags: Vec<String>, 53 /// Matcher strings of the rule. 54 pub strings: Vec<YaraString>, 55 } 56 57 #[napi(object)] 58 #[derive(Debug)] 59 pub struct YaraRuleMetadata { 60 pub identifier: String, 61 pub value: Either3<i64, String, bool>, 62 } 63 64 #[napi(object)] 65 #[derive(Debug)] 66 pub struct YaraString { 67 /// Name of the string, with the '$'. 68 pub identifier: String, 69 /// Matches of the string for the scan. 70 pub matches: Vec<YaraMatch>, 71 } 72 73 #[napi(object)] 74 #[derive(Debug, Clone)] 75 pub struct YaraMatch { 76 // base offset of the memory block in which the match occurred. 77 pub base: i64, 78 /// Offset of the match within the scanning area. 79 pub offset: i64, 80 /// Length of the file. Can be useful if the matcher string has not a fixed length. 81 pub length: i64, 82 /// Matched data. 83 pub data: Vec<u8>, 84 /// If utf-8 then we decode it here 85 pub string_data: Option<String>, 86 } 87 88 #[napi] 89 impl YaraCompiler { 90 /// Constructs a new Yara instance and compiles the provided rules and variables. 91 /// 92 /// @param rules - The rules which shall be compiled. 93 /// @param variables - The variables you want to pass to the rules. 94 /// @throws This can throw if there is an unexpected error. 95 /// 96 /// @returns A new instance of a YaraScanner which can be used to scan data 97 #[napi(constructor)] 98 pub fn new(rules: Vec<YaraRule>, variables: Vec<YaraVariable>) -> napi::Result<Self> { 99 let mut compiler = Compiler::new().context("Failed to create yara compiler")?; 100 101 // Load variables 102 for variable in variables { 103 match variable.value { 104 Either4::A(integer_value) => { 105 compiler 106 .define_variable(&variable.id, integer_value) 107 .context(format!("Failed to set variable with id: {}", variable.id))?; 108 } 109 Either4::B(float_value) => { 110 compiler 111 .define_variable(&variable.id, float_value) 112 .context(format!("Failed to set variable with id: {}", variable.id))?; 113 } 114 Either4::C(bool_value) => { 115 compiler 116 .define_variable(&variable.id, bool_value) 117 .context(format!("Failed to set variable with id: {}", variable.id))?; 118 } 119 Either4::D(string_value) => { 120 compiler 121 .define_variable(&variable.id, string_value.as_str()) 122 .context(format!("Failed to set variable with id: {}", variable.id))?; 123 } 124 } 125 } 126 127 // Load rules 128 for rule in rules { 129 if let Some(namespace) = rule.namespace { 130 if let Some(filepath) = rule.filename { 131 compiler = compiler 132 .add_rules_file_with_namespace(filepath.clone(), &namespace) 133 .context(format!("Failed to load rule at {}", filepath))?; 134 } else if let Some(string) = rule.string { 135 compiler = compiler 136 .add_rules_str_with_namespace(&string, &namespace) 137 .context("Failed to load string rule")?; 138 } 139 } else if let Some(filepath) = rule.filename { 140 compiler = compiler 141 .add_rules_file(filepath.clone()) 142 .context(format!("Failed to load rule at {}", filepath))?; 143 } else if let Some(string) = rule.string { 144 compiler = compiler 145 .add_rules_str(&string) 146 .context("Failed to load string rule")?; 147 } 148 } 149 150 let rules = compiler 151 .compile_rules() 152 .context("Failed to compile rules")?; 153 154 Ok(YaraCompiler { rules }) 155 } 156 157 /// Creates a new yara scanner for the rules defined earlier. 158 /// This can be called multiple times 159 /// 160 /// @returns A {@link YaraScanner} instance 161 #[napi] 162 pub fn new_scanner(&self, reference: Reference<YaraCompiler>, env: Env) -> Result<YaraScanner> { 163 YaraScanner::new(reference, env) 164 } 165 } 166 167 #[napi] 168 impl YaraScanner { 169 pub fn new(reference: Reference<YaraCompiler>, env: Env) -> Result<Self> { 170 let scanner = reference.share_with(env, |compiler| { 171 Ok(compiler.rules.scanner().context("Failed to get scanner")?) 172 })?; 173 Ok(YaraScanner { scanner }) 174 } 175 176 /// Converts the yara-rs types to types which we can return to napi-rs. 177 /// Ideally we dont need this but sadly for now this is required. 178 fn convert_yara_results(&self, rules: Vec<ExtYaraRule>) -> Vec<YaraRuleResult> { 179 let results = rules 180 .iter() 181 .map(|rule| YaraRuleResult { 182 identifier: rule.identifier.to_string(), 183 namespace: rule.namespace.to_string(), 184 metadatas: rule 185 .metadatas 186 .iter() 187 .map(|metadata| match metadata.value { 188 MetadataValue::Integer(int) => YaraRuleMetadata { 189 identifier: metadata.identifier.to_string(), 190 value: Either3::A(int), 191 }, 192 MetadataValue::String(string) => YaraRuleMetadata { 193 identifier: metadata.identifier.to_string(), 194 value: Either3::B(string.to_string()), 195 }, 196 MetadataValue::Boolean(boolean) => YaraRuleMetadata { 197 identifier: metadata.identifier.to_string(), 198 value: Either3::C(boolean), 199 }, 200 }) 201 .collect(), 202 tags: rule.tags.iter().map(ToString::to_string).collect(), 203 strings: rule 204 .strings 205 .iter() 206 .map(|string| YaraString { 207 identifier: string.identifier.to_string(), 208 matches: string 209 .matches 210 .iter() 211 .map(|matches| { 212 let string = String::from_utf8(matches.data.clone()); 213 let string_data = if let Ok(string_data) = string { 214 Some(string_data) 215 } else { 216 None 217 }; 218 YaraMatch { 219 base: matches.base as i64, 220 offset: matches.offset as i64, 221 length: matches.length as i64, 222 data: matches.data.clone(), 223 string_data, 224 } 225 }) 226 .collect(), 227 }) 228 .collect(), 229 }) 230 .collect(); 231 results 232 } 233 234 /// Scan a buffer of data with yara 235 /// 236 /// @param buffer - The data which shall be scanned by yara. 237 /// @throws This can throw if there is an unexpected error. 238 /// 239 /// @returns The results of yara scan_mem. 240 #[napi] 241 pub fn scan_buffer(&mut self, buffer: Buffer) -> Result<Vec<YaraRuleResult>> { 242 let buf: Vec<u8> = buffer.into(); 243 let results = self 244 .scanner 245 .scan_mem(&buf) 246 .context("Failed to scan buffer")?; 247 248 Ok(self.convert_yara_results(results)) 249 } 250 251 /// Scan a string of data with yara 252 /// 253 /// @param input - The data which shall be scanned by yara. 254 /// @throws This can throw if there is an unexpected error. 255 /// 256 /// @returns The results of yara scan_mem. 257 #[napi] 258 pub fn scan_string(&mut self, input: String) -> Result<Vec<YaraRuleResult>> { 259 let buf: &[u8] = input.as_bytes(); 260 let results = self 261 .scanner 262 .scan_mem(buf) 263 .context("Failed to scan string")?; 264 265 Ok(self.convert_yara_results(results)) 266 } 267 268 /// Scan a file with yara 269 /// 270 /// @param filepath - The path to the file yara shall scan 271 /// @throws This can throw if there is an unexpected error. 272 /// 273 /// @returns The results of yara scan_mem. 274 #[napi] 275 pub fn scan_file(&mut self, filepath: String) -> Result<Vec<YaraRuleResult>> { 276 let results = self 277 .scanner 278 .scan_file(filepath) 279 .context("Failed to scan file")?; 280 281 Ok(self.convert_yara_results(results)) 282 } 283 284 /// Scan a process with yara 285 /// 286 /// @param pid - The process id of the process that shall be scanned. 287 /// @throws This can throw if there is an unexpected error. 288 /// 289 /// @returns The results of yara scan_mem. 290 #[napi] 291 pub fn scan_process(&mut self, pid: u32) -> Result<Vec<YaraRuleResult>> { 292 let results = self 293 .scanner 294 .scan_process(pid) 295 .context("Failed to scan file")?; 296 297 Ok(self.convert_yara_results(results)) 298 } 299 300 #[napi] 301 pub fn define_variable( 302 &mut self, 303 identifier: String, 304 value: Either4<String, i64, f64, bool>, 305 ) -> Result<()> { 306 match value { 307 Either4::A(string_value) => Ok( 308 self 309 .scanner 310 .define_variable(&identifier, string_value.as_str()) 311 .context(format!("Failed to define string variable: {identifier}"))?, 312 ), 313 Either4::B(bool_value) => Ok( 314 self 315 .scanner 316 .define_variable(&identifier, bool_value) 317 .context(format!("Failed to define bool variable: {identifier}"))?, 318 ), 319 Either4::C(float_value) => Ok( 320 self 321 .scanner 322 .define_variable(&identifier, float_value) 323 .context(format!("Failed to define float variable: {identifier}"))?, 324 ), 325 Either4::D(integer_value) => Ok( 326 self 327 .scanner 328 .define_variable(&identifier, integer_value) 329 .context(format!("Failed to define integer variable: {identifier}"))?, 330 ), 331 } 332 } 333 }