-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathsimple_roundtrip_support.py
More file actions
159 lines (137 loc) · 6.19 KB
/
Copy pathsimple_roundtrip_support.py
File metadata and controls
159 lines (137 loc) · 6.19 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
"""Test-only support for simple roundtrip serialization."""
from __future__ import annotations
from collections.abc import Sequence
import json
from os import PathLike, fspath
from pathlib import Path
from typing import Any
from yaraast.ast.base import ASTNode
from yaraast.errors import YaraASTError
from yaraast.parser.source import parse_yara_source
from yaraast.serialization.simple_roundtrip_helpers import deserialize_node, serialize_node
from yaraast.shared.file_patterns import iter_matching_files
from yaraast.yarax.generator import YaraXGenerator
def _read_yara_text_file(path: Path) -> str:
try:
return path.read_text(encoding="utf-8")
except UnicodeDecodeError as exc:
msg = "YARA file must contain valid UTF-8 text"
raise ValueError(msg) from exc
class SimpleRoundtripSerializer:
def serialize(self, node: ASTNode) -> dict[str, Any]:
return serialize_node(node)
def deserialize(self, data: dict[str, Any]) -> ASTNode:
return deserialize_node(data)
def serialize_to_file(self, node: ASTNode, file_path: str | Path) -> None:
Path(file_path).write_text(json.dumps(serialize_node(node), indent=2), encoding="utf-8")
def deserialize_from_file(self, file_path: str | Path) -> ASTNode:
data = json.loads(Path(file_path).read_text(encoding="utf-8"))
return deserialize_node(data)
def validate_roundtrip(self, node: ASTNode) -> tuple[bool, dict[str, Any]]:
generator = YaraXGenerator()
original_code = generator.generate(node)
regenerated_ast = parse_yara_source(original_code)
regenerated_code = generator.generate(regenerated_ast)
success = original_code == regenerated_code
return success, {
"original_code": original_code,
"regenerated_code": regenerated_code,
"round_trip_successful": success,
}
class SimpleRoundTrip:
def __init__(self) -> None:
self.generator = YaraXGenerator()
self.test_count = 0
self.success_count = 0
def test(self, yara_code: str) -> tuple[bool, Any, Any]:
self.test_count += 1
if not isinstance(yara_code, str):
return False, None, None
try:
original_ast = parse_yara_source(yara_code)
regenerated = self.generator.generate(original_ast)
regenerated_ast = parse_yara_source(regenerated)
success = original_ast is not None and regenerated_ast is not None
if success:
self.success_count += 1
return success, original_ast, regenerated_ast
except (ValueError, YaraASTError):
return False, None, None
def test_batch(self, yara_codes: list[str]) -> list[tuple[bool, Any, Any]]:
if isinstance(yara_codes, str) or not isinstance(yara_codes, Sequence):
msg = "yara_codes must be a sequence of strings"
raise TypeError(msg)
if not all(isinstance(code, str) for code in yara_codes):
msg = "yara_codes must contain only strings"
raise TypeError(msg)
return [self.test(code) for code in yara_codes]
def test_file(self, file_path: str | PathLike[str]) -> tuple[bool, Any, Any]:
if isinstance(file_path, bytes) or not isinstance(file_path, str | PathLike):
msg = "file_path must be a string or path-like object"
raise TypeError(msg)
raw_path = fspath(file_path)
if not isinstance(raw_path, str):
msg = "file_path must be a string or path-like object"
raise TypeError(msg)
if not raw_path.strip():
msg = "file_path must not be empty"
raise ValueError(msg)
path = Path(raw_path)
yara_code = _read_yara_text_file(path)
return self.test(yara_code)
def test_directory(self, dir_path: str | PathLike[str]) -> list[tuple[Path, bool, Any, Any]]:
if isinstance(dir_path, bytes) or not isinstance(dir_path, str | PathLike):
msg = "dir_path must be a string or path-like object"
raise TypeError(msg)
raw_path = fspath(dir_path)
if not isinstance(raw_path, str):
msg = "dir_path must be a string or path-like object"
raise TypeError(msg)
if not raw_path.strip():
msg = "dir_path must not be empty"
raise ValueError(msg)
dir_path = Path(raw_path)
results = []
for yar_file in iter_matching_files(dir_path):
success, orig, regen = self.test_file(yar_file)
results.append((yar_file, success, orig, regen))
return results
def get_statistics(self) -> dict[str, Any]:
return {
"total_tests": self.test_count,
"successful_tests": self.success_count,
"failed_tests": self.test_count - self.success_count,
"success_rate": self.success_count / max(1, self.test_count) * 100,
}
def simple_roundtrip_test(yara_source: str) -> dict[str, Any]:
try:
original_ast = parse_yara_source(yara_source)
generator = YaraXGenerator()
reconstructed_source = generator.generate(original_ast)
reconstructed_ast = parse_yara_source(reconstructed_source)
success = original_ast is not None and reconstructed_ast is not None
return {
"original_source": yara_source,
"reconstructed_source": reconstructed_source,
"serialized_data": reconstructed_source,
"format": "simple",
"round_trip_successful": success,
"differences": [] if success else ["Error during roundtrip"],
"metadata": {
"original_rule_count": len(original_ast.rules),
"reconstructed_rule_count": len(reconstructed_ast.rules),
},
}
except (ValueError, YaraASTError) as exc:
return {
"original_source": yara_source,
"reconstructed_source": "",
"serialized_data": "",
"format": "simple",
"round_trip_successful": False,
"differences": [f"Error during roundtrip: {exc}"],
"metadata": {
"original_rule_count": 0,
"reconstructed_rule_count": 0,
},
}