|
1 | 1 | import json |
2 | | - |
3 | 2 | import yara |
| 3 | +import re |
4 | 4 |
|
5 | 5 | misperrors = {"error": "Error"} |
6 | 6 | mispattributes = {"input": ["yara"], "output": ["text"]} |
7 | 7 | moduleinfo = { |
8 | | - "version": "0.1", |
9 | | - "author": "Dennis Rand", |
| 8 | + "version": "0.2", |
| 9 | + "author": "Dennis Rand and Theo Geffe", |
10 | 10 | "description": "An expansion hover module to perform a syntax check on if yara rules are valid or not.", |
11 | 11 | "module-type": ["hover"], |
12 | 12 | "name": "YARA Syntax Validator", |
|
15 | 15 | "features": ( |
16 | 16 | "This modules simply takes a YARA rule as input, and checks its syntax. It returns then a confirmation if the" |
17 | 17 | " syntax is valid, otherwise the syntax error is displayed." |
| 18 | + " This modules can compile YARA rule even if there is externals variables or miss modules" |
18 | 19 | ), |
19 | 20 | "references": ["http://virustotal.github.io/yara/"], |
20 | 21 | "input": "YARA rule attribute.", |
21 | 22 | "output": "Text to inform users if their rule is valid.", |
22 | 23 | } |
23 | 24 | moduleconfig = [] |
24 | 25 |
|
| 26 | +# List of most uses Modules in yara |
| 27 | + |
| 28 | +YARA_MODULES = {"pe", "math", "cuckoo", "magic", "hash", "dotnet", "elf", "macho"} |
| 29 | + |
| 30 | +# YARA rules can reference internal modules such as `pe`, `math`, `cuckoo`, |
| 31 | +# `elf`, etc. Normally, a rule must explicitly import these modules using: |
| 32 | +# import "pe" |
| 33 | +# import "elf" |
| 34 | +# |
| 35 | +# Even if the YARA rule itself is perfectly valid, it will fail to compile |
| 36 | +# if the correct modules are NOT imported. For example, references such as: |
| 37 | +# pe.entry_point |
| 38 | +# or elf.sections |
| 39 | +# will raise "undefined identifier" errors unless the appropriate imports |
| 40 | +# are present. |
| 41 | + |
| 42 | + |
| 43 | +def insert_import_module(rule_text, module_name): |
| 44 | + lines = rule_text.strip().splitlines() |
| 45 | + if not any(line.strip().startswith(f'import "{module_name}"') for line in lines): |
| 46 | + return f'import "{module_name}"\n' + rule_text |
| 47 | + return rule_text |
| 48 | + |
| 49 | +# ------------------------------- |
| 50 | +# HANDLER NOW DOES VALIDATION LOGIC |
| 51 | +# ------------------------------- |
| 52 | + |
| 53 | +# ------------------------------------------------------------------------- |
| 54 | +# To make the validator more user-friendly, we automatically insert missing |
| 55 | +# imports when YARA reports an undefined identifier matching a known module. |
| 56 | +# |
| 57 | +# Additionally, YARA rules sometimes use *external variables*, for example: |
| 58 | +# rule test { condition: filename == "test.exe" } |
| 59 | +# |
| 60 | +# If these variables are not provided, YARA will also fail, even though the |
| 61 | +# rule is syntactically valid. To prevent unnecessary failures, the validator |
| 62 | +# automatically assigns dummy values to any missing external variables. |
| 63 | +# |
| 64 | +# This ensures: |
| 65 | +# - A clean, user-friendly validation process |
| 66 | +# - Correct detection of real syntax errors |
| 67 | +# - No false negatives caused by missing imports or missing externals |
| 68 | +# ------------------------------------------------------------------------- |
| 69 | + |
25 | 70 |
|
26 | 71 | def handler(q=False): |
27 | 72 | if q is False: |
28 | 73 | return False |
29 | 74 | request = json.loads(q) |
30 | | - if not request.get("yara"): |
| 75 | + rule_content = request.get("yara") |
| 76 | + if not rule_content: |
31 | 77 | misperrors["error"] = "Yara rule missing" |
32 | 78 | return misperrors |
33 | | - |
34 | | - try: |
35 | | - yara.compile(source=request.get("yara")) |
36 | | - summary = "Syntax valid" |
37 | | - except Exception as e: |
38 | | - summary = "Syntax error: " + str(e) |
39 | | - |
40 | | - r = {"results": [{"types": mispattributes["output"], "values": summary}]} |
41 | | - return r |
| 79 | + externals = {} |
| 80 | + attempts = 0 |
| 81 | + max_attempts = 10 # to prevent from infinite loop |
| 82 | + current_rule_text = rule_content |
| 83 | + while attempts < max_attempts: |
| 84 | + try: |
| 85 | + yara.compile(source=current_rule_text, externals=externals) # some times the compilator needs externals variables |
| 86 | + summary = "Syntax valid" |
| 87 | + break |
| 88 | + except yara.SyntaxError as e: |
| 89 | + error_msg = str(e) |
| 90 | + # try to catch modules or externals variables errors to auto correct it |
| 91 | + match_id = re.search(r'undefined identifier "(\w+)"', error_msg) |
| 92 | + if match_id: |
| 93 | + var_name = match_id.group(1) |
| 94 | + # Auto-import YARA modules |
| 95 | + if var_name in YARA_MODULES: |
| 96 | + current_rule_text = insert_import_module(current_rule_text, var_name) |
| 97 | + else: |
| 98 | + # Treat as external variable |
| 99 | + externals[var_name] = "example.txt" # a random value so that the compiler does not make an error (most of the time the external variable are in other configs files) |
| 100 | + attempts += 1 |
| 101 | + continue |
| 102 | + # Other syntax errors |
| 103 | + summary = "Syntax error: " + error_msg |
| 104 | + break |
| 105 | + else: |
| 106 | + # Max attempts exceeded |
| 107 | + summary = "Syntax error: Max validation attempts exceeded" |
| 108 | + return {"results": [{"types": mispattributes["output"], "values": summary}]} |
42 | 109 |
|
43 | 110 |
|
44 | 111 | def introspection(): |
|
0 commit comments