reg2es 2.2.1__tar.gz → 2.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {reg2es-2.2.1 → reg2es-2.3.0}/PKG-INFO +15 -35
- {reg2es-2.2.1 → reg2es-2.3.0}/README.md +14 -34
- reg2es-2.3.0/src/reg2es/__about__.py +1 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/models/Reg2es.py +28 -18
- {reg2es-2.2.1 → reg2es-2.3.0}/tests/test_model.py +91 -7
- reg2es-2.3.0/tests/test_output_contract.py +174 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/tests/test_timestamp_review.py +8 -8
- reg2es-2.2.1/src/reg2es/__about__.py +0 -1
- reg2es-2.2.1/tests/test_output_contract.py +0 -65
- {reg2es-2.2.1 → reg2es-2.3.0}/.devcontainer/Dockerfile +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/.devcontainer/devcontainer.json +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/.devcontainer/docker-compose.yaml +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/.devcontainer/postCreateCommands.fish +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/.github/collect_runtime_licenses.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/.github/workflows/publish-binary-pypi.yaml +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/.github/workflows/publish-release.yaml +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/.github/workflows/test.yaml +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/.gitignore +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/CONTRIBUTING.md +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/LICENSE +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/LICENSES/Apache-2.0.txt +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/pyproject.toml +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/__init__.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/models/ElasticsearchUtils.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/plugins/__init__.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/plugins/antivirus.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/plugins/auditpol.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/plugins/base.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/plugins/compname.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/plugins/env.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/plugins/filedialogmru.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/plugins/gpo.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/plugins/kb.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/plugins/keyboard.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/plugins/lastloggedon.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/plugins/lastshutdown.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/plugins/localgroups.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/plugins/localusers.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/plugins/mndmru.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/plugins/mounteddevices.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/plugins/mstscmru.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/plugins/office_macros.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/plugins/portproxy.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/plugins/printer_history.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/plugins/printer_ports.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/plugins/proxy.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/plugins/putty.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/plugins/rdphint.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/plugins/recentdocs.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/plugins/regtime.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/plugins/run.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/plugins/runmru.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/plugins/services.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/plugins/shimcache.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/plugins/srum.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/plugins/sysinternals.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/plugins/systeminfo.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/plugins/tasks.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/plugins/teamviewer.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/plugins/timezone.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/plugins/typedurls.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/plugins/uninstall.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/plugins/usbstor.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/plugins/userassist.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/plugins/usersids.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/plugins/version.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/plugins/wordwheelquery.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/presenters/Reg2esPresenter.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/presenters/Reg2jsonPresenter.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/views/BaseView.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/views/Reg2esView.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/views/Reg2jsonView.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/tests/__init__.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/tests/conftest.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/tests/regrippy/__init__.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/tests/regrippy/reg_mock.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/tests/regrippy/test_antivirus.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/tests/regrippy/test_compname.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/tests/regrippy/test_env.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/tests/regrippy/test_filedialogmru.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/tests/regrippy/test_keyboard.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/tests/regrippy/test_lastloggedon.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/tests/regrippy/test_lastshutdown.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/tests/regrippy/test_localgroups.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/tests/regrippy/test_localusers.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/tests/regrippy/test_mndmru.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/tests/regrippy/test_mstscmru.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/tests/regrippy/test_portproxy.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/tests/regrippy/test_printer_history.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/tests/regrippy/test_printer_ports.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/tests/regrippy/test_proxy.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/tests/regrippy/test_putty.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/tests/regrippy/test_rdphint.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/tests/regrippy/test_recentdocs.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/tests/regrippy/test_run.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/tests/regrippy/test_runmru.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/tests/regrippy/test_services.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/tests/regrippy/test_shimcache.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/tests/regrippy/test_sysinternals.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/tests/regrippy/test_systeminfo.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/tests/regrippy/test_tasks.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/tests/regrippy/test_timezone.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/tests/regrippy/test_typedurls.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/tests/regrippy/test_uninstall.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/tests/regrippy/test_userassist.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/tests/regrippy/test_usersids.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/tests/regrippy/test_version.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/tests/test_case_plugins.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/tests/test_es_contract.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/tests/test_integration.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/tests/test_plugin_timestamps.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/tests/test_plugins.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/tests/test_runtime_licenses.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/tests/test_shared_output.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/tests/test_shimcache_entries.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/tests/test_timestamp_section_c.py +0 -0
- {reg2es-2.2.1 → reg2es-2.3.0}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: reg2es
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.0
|
|
4
4
|
Summary: A fast library for parsing and importing Windows NT Registry(REGF) into Elasticsearch.
|
|
5
5
|
Author-email: sumeshi <j15322sn@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -24,13 +24,9 @@ Description-Content-Type: text/markdown
|
|
|
24
24
|
A command-line tool and Python library for extracting forensic artifacts from
|
|
25
25
|
Windows NT Registry (REGF) hives and importing them into Elasticsearch.
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
[
|
|
29
|
-
|
|
30
|
-
from [RegRipper](https://github.com/keydet89/RegRipper3.0). `reg2es` runs
|
|
31
|
-
standalone and does not require the `regrippy` package at runtime. Both
|
|
32
|
-
`reg2es` and `reg2json` use the same plugin runner and emit the same
|
|
33
|
-
ECS-oriented documents.
|
|
27
|
+
Includes Registry artifact plugins based on
|
|
28
|
+
[regrippy](https://github.com/airbus-cert/regrippy) and inspired by
|
|
29
|
+
[RegRipper](https://github.com/keydet89/RegRipper3.0).
|
|
34
30
|
|
|
35
31
|
|
|
36
32
|
## Usage
|
|
@@ -87,14 +83,14 @@ unrelated files are not treated as standalone hives.
|
|
|
87
83
|
opt-in.
|
|
88
84
|
- `--list-plugins`: print the 41 bundled plugins and exit.
|
|
89
85
|
- `--size N`: set the generation and indexing chunk size (default: 500).
|
|
90
|
-
- `--tags tag1,tag2`: add tags to every document.
|
|
86
|
+
- `--tags tag1,tag2`: add custom tags to every document.
|
|
91
87
|
- `--quiet`: suppress progress output.
|
|
92
88
|
|
|
93
89
|
`reg2es` also accepts Elasticsearch connection options including
|
|
94
90
|
`--host`, `--port`, `--index`, `--scheme`, `--pipeline`, `--login`, `--pwd`,
|
|
95
91
|
`--ca-certs`, and `--no-verify-certs`. TLS verification is enabled by default;
|
|
96
92
|
use `--ca-certs /path/to/ca.pem` for a private CA bundle. Run `reg2es --help`
|
|
97
|
-
or `reg2json --help` for
|
|
93
|
+
or `reg2json --help` for all options.
|
|
98
94
|
|
|
99
95
|
|
|
100
96
|
### Examples
|
|
@@ -134,9 +130,8 @@ reg2es SYSTEM --login elastic --pwd '******'
|
|
|
134
130
|
reg2json NTUSER.DAT --plugin userassist -o artifacts.json
|
|
135
131
|
```
|
|
136
132
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
no output path is specified, the default extension is `.jsonl`:
|
|
133
|
+
Use `--format jsonl` (or `ndjson`) to write one record per line.
|
|
134
|
+
The default output extension is `.jsonl`:
|
|
140
135
|
|
|
141
136
|
```bash
|
|
142
137
|
reg2json NTUSER.DAT --plugin userassist --format jsonl -o artifacts.jsonl
|
|
@@ -172,28 +167,13 @@ result: list[dict] = reg2json(
|
|
|
172
167
|
|
|
173
168
|
## Output Format Example
|
|
174
169
|
|
|
175
|
-
Each plugin result becomes one document with `ecs.version` set to `8.17.0`.
|
|
176
|
-
Standard `event`,
|
|
177
|
-
`registry`, `log.file`, `tags`, and `@timestamp` fields describe the artifact.
|
|
178
|
-
Lossless plugin-specific data and the original offline-hive location are kept
|
|
179
|
-
under `reg2es`.
|
|
180
|
-
|
|
181
|
-
When a plugin has a reliable artifact time, it uses that time for `@timestamp`.
|
|
182
|
-
Otherwise it uses the registry key's LastWrite; if no valid time exists,
|
|
183
|
-
`@timestamp` is omitted. Check `reg2es.timestamp.source` and
|
|
184
|
-
`reg2es.timestamp.meaning` before treating a result as an activity time.
|
|
185
|
-
Original values and fallback reasons remain in the document.
|
|
186
|
-
|
|
187
|
-
The current ECS output changes `registry.data.bytes` from a byte count to
|
|
188
|
-
Base64, uses `REG_*` names in `registry.data.type`, and includes the value
|
|
189
|
-
name in `registry.path`. Existing Elasticsearch mappings may conflict with
|
|
190
|
-
these fields; use a new index for the updated output.
|
|
191
|
-
|
|
192
170
|
```json
|
|
193
171
|
{
|
|
194
|
-
"
|
|
195
|
-
"@timestamp": "2015-10-30T07:24:57.814133+00:00",
|
|
172
|
+
"@timestamp": "2015-10-30T07:24:57.814133Z",
|
|
196
173
|
"event": {
|
|
174
|
+
"provider": "registry",
|
|
175
|
+
"module": "windows",
|
|
176
|
+
"dataset": "windows.registry",
|
|
197
177
|
"kind": "event",
|
|
198
178
|
"category": ["registry"],
|
|
199
179
|
"type": ["info"],
|
|
@@ -203,8 +183,8 @@ these fields; use a new index for the updated output.
|
|
|
203
183
|
"hive": "HKLM",
|
|
204
184
|
"key": "SYSTEM\\ControlSet001\\Control\\ComputerName\\ComputerName",
|
|
205
185
|
"path": "HKLM\\SYSTEM\\ControlSet001\\Control\\ComputerName\\ComputerName\\ComputerName",
|
|
206
|
-
|
|
207
|
-
|
|
186
|
+
"value": "ComputerName",
|
|
187
|
+
"data": {
|
|
208
188
|
"type": "REG_SZ",
|
|
209
189
|
"strings": ["DESKTOP-EXAMPLE"]
|
|
210
190
|
}
|
|
@@ -242,7 +222,7 @@ $ uv add reg2es
|
|
|
242
222
|
|
|
243
223
|
### From GitHub Releases
|
|
244
224
|
|
|
245
|
-
Standalone binaries
|
|
225
|
+
Standalone binaries are available from GitHub Releases for systems without a Python environment.
|
|
246
226
|
|
|
247
227
|
```bash
|
|
248
228
|
$ chmod +x ./reg2es
|
|
@@ -8,13 +8,9 @@
|
|
|
8
8
|
A command-line tool and Python library for extracting forensic artifacts from
|
|
9
9
|
Windows NT Registry (REGF) hives and importing them into Elasticsearch.
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
[
|
|
13
|
-
|
|
14
|
-
from [RegRipper](https://github.com/keydet89/RegRipper3.0). `reg2es` runs
|
|
15
|
-
standalone and does not require the `regrippy` package at runtime. Both
|
|
16
|
-
`reg2es` and `reg2json` use the same plugin runner and emit the same
|
|
17
|
-
ECS-oriented documents.
|
|
11
|
+
Includes Registry artifact plugins based on
|
|
12
|
+
[regrippy](https://github.com/airbus-cert/regrippy) and inspired by
|
|
13
|
+
[RegRipper](https://github.com/keydet89/RegRipper3.0).
|
|
18
14
|
|
|
19
15
|
|
|
20
16
|
## Usage
|
|
@@ -71,14 +67,14 @@ unrelated files are not treated as standalone hives.
|
|
|
71
67
|
opt-in.
|
|
72
68
|
- `--list-plugins`: print the 41 bundled plugins and exit.
|
|
73
69
|
- `--size N`: set the generation and indexing chunk size (default: 500).
|
|
74
|
-
- `--tags tag1,tag2`: add tags to every document.
|
|
70
|
+
- `--tags tag1,tag2`: add custom tags to every document.
|
|
75
71
|
- `--quiet`: suppress progress output.
|
|
76
72
|
|
|
77
73
|
`reg2es` also accepts Elasticsearch connection options including
|
|
78
74
|
`--host`, `--port`, `--index`, `--scheme`, `--pipeline`, `--login`, `--pwd`,
|
|
79
75
|
`--ca-certs`, and `--no-verify-certs`. TLS verification is enabled by default;
|
|
80
76
|
use `--ca-certs /path/to/ca.pem` for a private CA bundle. Run `reg2es --help`
|
|
81
|
-
or `reg2json --help` for
|
|
77
|
+
or `reg2json --help` for all options.
|
|
82
78
|
|
|
83
79
|
|
|
84
80
|
### Examples
|
|
@@ -118,9 +114,8 @@ reg2es SYSTEM --login elastic --pwd '******'
|
|
|
118
114
|
reg2json NTUSER.DAT --plugin userassist -o artifacts.json
|
|
119
115
|
```
|
|
120
116
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
no output path is specified, the default extension is `.jsonl`:
|
|
117
|
+
Use `--format jsonl` (or `ndjson`) to write one record per line.
|
|
118
|
+
The default output extension is `.jsonl`:
|
|
124
119
|
|
|
125
120
|
```bash
|
|
126
121
|
reg2json NTUSER.DAT --plugin userassist --format jsonl -o artifacts.jsonl
|
|
@@ -156,28 +151,13 @@ result: list[dict] = reg2json(
|
|
|
156
151
|
|
|
157
152
|
## Output Format Example
|
|
158
153
|
|
|
159
|
-
Each plugin result becomes one document with `ecs.version` set to `8.17.0`.
|
|
160
|
-
Standard `event`,
|
|
161
|
-
`registry`, `log.file`, `tags`, and `@timestamp` fields describe the artifact.
|
|
162
|
-
Lossless plugin-specific data and the original offline-hive location are kept
|
|
163
|
-
under `reg2es`.
|
|
164
|
-
|
|
165
|
-
When a plugin has a reliable artifact time, it uses that time for `@timestamp`.
|
|
166
|
-
Otherwise it uses the registry key's LastWrite; if no valid time exists,
|
|
167
|
-
`@timestamp` is omitted. Check `reg2es.timestamp.source` and
|
|
168
|
-
`reg2es.timestamp.meaning` before treating a result as an activity time.
|
|
169
|
-
Original values and fallback reasons remain in the document.
|
|
170
|
-
|
|
171
|
-
The current ECS output changes `registry.data.bytes` from a byte count to
|
|
172
|
-
Base64, uses `REG_*` names in `registry.data.type`, and includes the value
|
|
173
|
-
name in `registry.path`. Existing Elasticsearch mappings may conflict with
|
|
174
|
-
these fields; use a new index for the updated output.
|
|
175
|
-
|
|
176
154
|
```json
|
|
177
155
|
{
|
|
178
|
-
"
|
|
179
|
-
"@timestamp": "2015-10-30T07:24:57.814133+00:00",
|
|
156
|
+
"@timestamp": "2015-10-30T07:24:57.814133Z",
|
|
180
157
|
"event": {
|
|
158
|
+
"provider": "registry",
|
|
159
|
+
"module": "windows",
|
|
160
|
+
"dataset": "windows.registry",
|
|
181
161
|
"kind": "event",
|
|
182
162
|
"category": ["registry"],
|
|
183
163
|
"type": ["info"],
|
|
@@ -187,8 +167,8 @@ these fields; use a new index for the updated output.
|
|
|
187
167
|
"hive": "HKLM",
|
|
188
168
|
"key": "SYSTEM\\ControlSet001\\Control\\ComputerName\\ComputerName",
|
|
189
169
|
"path": "HKLM\\SYSTEM\\ControlSet001\\Control\\ComputerName\\ComputerName\\ComputerName",
|
|
190
|
-
|
|
191
|
-
|
|
170
|
+
"value": "ComputerName",
|
|
171
|
+
"data": {
|
|
192
172
|
"type": "REG_SZ",
|
|
193
173
|
"strings": ["DESKTOP-EXAMPLE"]
|
|
194
174
|
}
|
|
@@ -226,7 +206,7 @@ $ uv add reg2es
|
|
|
226
206
|
|
|
227
207
|
### From GitHub Releases
|
|
228
208
|
|
|
229
|
-
Standalone binaries
|
|
209
|
+
Standalone binaries are available from GitHub Releases for systems without a Python environment.
|
|
230
210
|
|
|
231
211
|
```bash
|
|
232
212
|
$ chmod +x ./reg2es
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "2.3.0"
|
|
@@ -373,7 +373,7 @@ def _normalize_value(value: Any) -> Any:
|
|
|
373
373
|
if isinstance(value, (bytes, bytearray, memoryview)):
|
|
374
374
|
return bytes(value).hex()
|
|
375
375
|
if isinstance(value, datetime):
|
|
376
|
-
return value
|
|
376
|
+
return _datetime_to_iso(value)
|
|
377
377
|
if isinstance(value, Path):
|
|
378
378
|
return str(value)
|
|
379
379
|
if isinstance(value, Enum):
|
|
@@ -409,8 +409,25 @@ def _normalize_value(value: Any) -> Any:
|
|
|
409
409
|
)
|
|
410
410
|
|
|
411
411
|
|
|
412
|
+
def _datetime_to_iso(value: datetime) -> str:
|
|
413
|
+
"""Format a datetime consistently, using ``Z`` for UTC values."""
|
|
414
|
+
serialized = value.isoformat()
|
|
415
|
+
if value.tzinfo is not None and value.utcoffset() == timezone.utc.utcoffset(value):
|
|
416
|
+
if serialized.endswith("+00:00"):
|
|
417
|
+
return serialized[:-6] + "Z"
|
|
418
|
+
return serialized
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def _normalize_tags(tags: Optional[Union[str, Sequence[str]]]) -> List[str]:
|
|
422
|
+
"""Normalize comma-delimited strings or tag sequences to clean strings."""
|
|
423
|
+
if tags is None:
|
|
424
|
+
return []
|
|
425
|
+
values = tags.split(",") if isinstance(tags, str) else tags
|
|
426
|
+
return [tag.strip() for tag in values if isinstance(tag, str) and tag.strip()]
|
|
427
|
+
|
|
428
|
+
|
|
412
429
|
def _timestamp_to_iso(value: Any) -> Optional[str]:
|
|
413
|
-
"""Convert
|
|
430
|
+
"""Convert a valid timestamp to UTC ISO 8601 without changing its raw value."""
|
|
414
431
|
if isinstance(value, datetime):
|
|
415
432
|
if value.tzinfo is None or value.utcoffset() is None:
|
|
416
433
|
return None
|
|
@@ -418,7 +435,7 @@ def _timestamp_to_iso(value: Any) -> Optional[str]:
|
|
|
418
435
|
normalized = value.astimezone(timezone.utc)
|
|
419
436
|
if normalized.timestamp() <= 0:
|
|
420
437
|
return None
|
|
421
|
-
return normalized
|
|
438
|
+
return _datetime_to_iso(normalized)
|
|
422
439
|
except (OSError, OverflowError, ValueError):
|
|
423
440
|
return None
|
|
424
441
|
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
|
@@ -426,7 +443,7 @@ def _timestamp_to_iso(value: Any) -> Optional[str]:
|
|
|
426
443
|
try:
|
|
427
444
|
if not math.isfinite(value) or value <= 0:
|
|
428
445
|
return None
|
|
429
|
-
return datetime.fromtimestamp(value, tz=timezone.utc)
|
|
446
|
+
return _datetime_to_iso(datetime.fromtimestamp(value, tz=timezone.utc))
|
|
430
447
|
except (OSError, OverflowError, ValueError):
|
|
431
448
|
return None
|
|
432
449
|
|
|
@@ -470,9 +487,6 @@ _ECS_REGISTRY_TYPES = {
|
|
|
470
487
|
"RegQWord": "REG_QWORD",
|
|
471
488
|
}
|
|
472
489
|
|
|
473
|
-
_ECS_VERSION = "8.17.0"
|
|
474
|
-
|
|
475
|
-
|
|
476
490
|
def _join_registry_path(*parts: Optional[str]) -> str:
|
|
477
491
|
return "\\".join(part.strip("\\") for part in parts if part)
|
|
478
492
|
|
|
@@ -512,7 +526,7 @@ def plugin_result_to_document(
|
|
|
512
526
|
plugin_name: str,
|
|
513
527
|
hive_name: str,
|
|
514
528
|
hive_path: str,
|
|
515
|
-
additional_tags: Optional[Sequence[str]] = None,
|
|
529
|
+
additional_tags: Optional[Union[str, Sequence[str]]] = None,
|
|
516
530
|
recovery: Optional[dict] = None,
|
|
517
531
|
) -> dict:
|
|
518
532
|
"""Convert a single PluginResult to an ECS-compliant document.
|
|
@@ -572,8 +586,10 @@ def plugin_result_to_document(
|
|
|
572
586
|
if result.value_name is not None:
|
|
573
587
|
registry_path = _join_registry_path(registry_path, result.value_name)
|
|
574
588
|
doc: dict = {
|
|
575
|
-
"ecs": {"version": _ECS_VERSION},
|
|
576
589
|
"event": {
|
|
590
|
+
"provider": "registry",
|
|
591
|
+
"module": "windows",
|
|
592
|
+
"dataset": "windows.registry",
|
|
577
593
|
"kind": "event",
|
|
578
594
|
"category": ["registry"],
|
|
579
595
|
"type": ["info"],
|
|
@@ -589,11 +605,7 @@ def plugin_result_to_document(
|
|
|
589
605
|
"path": (str(Path(hive_path).resolve()) if hive_path != "-" else "-"),
|
|
590
606
|
},
|
|
591
607
|
},
|
|
592
|
-
"tags": list(
|
|
593
|
-
dict.fromkeys(
|
|
594
|
-
["registry", *(additional_tags or [])],
|
|
595
|
-
)
|
|
596
|
-
),
|
|
608
|
+
"tags": list(dict.fromkeys(["registry", *_normalize_tags(additional_tags)])),
|
|
597
609
|
"reg2es": {
|
|
598
610
|
"plugin": {"name": plugin_name},
|
|
599
611
|
"source": {"hive": hive_name, "key_path": result.path},
|
|
@@ -776,7 +788,7 @@ class Reg2es:
|
|
|
776
788
|
plugin_names: Optional[Sequence[str]] = None,
|
|
777
789
|
chunk_size: int = 500,
|
|
778
790
|
error_policy: str = "continue",
|
|
779
|
-
additional_tags: Optional[Sequence[str]] = None,
|
|
791
|
+
additional_tags: Optional[Union[str, Sequence[str]]] = None,
|
|
780
792
|
) -> None:
|
|
781
793
|
if error_policy not in ("continue", "raise"):
|
|
782
794
|
raise ValueError(
|
|
@@ -790,9 +802,7 @@ class Reg2es:
|
|
|
790
802
|
self.input_paths = [Path(path) for path in input_paths]
|
|
791
803
|
self.chunk_size = chunk_size
|
|
792
804
|
self.error_policy = error_policy
|
|
793
|
-
self.additional_tags =
|
|
794
|
-
tag.strip() for tag in (additional_tags or []) if tag and tag.strip()
|
|
795
|
-
]
|
|
805
|
+
self.additional_tags = _normalize_tags(additional_tags)
|
|
796
806
|
|
|
797
807
|
# Discover and validate plugins once.
|
|
798
808
|
all_plugins = discover_plugins()
|
|
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import base64
|
|
6
6
|
import logging
|
|
7
|
-
from datetime import datetime, timezone
|
|
7
|
+
from datetime import datetime, timedelta, timezone
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
from types import SimpleNamespace
|
|
10
10
|
from unittest.mock import MagicMock, patch
|
|
@@ -20,6 +20,7 @@ from reg2es.models.Reg2es import (
|
|
|
20
20
|
_build_hive_dataset,
|
|
21
21
|
_close_parsers,
|
|
22
22
|
_normalize_value,
|
|
23
|
+
_timestamp_to_iso,
|
|
23
24
|
_prepare_registry_hive,
|
|
24
25
|
detect_hive_type,
|
|
25
26
|
discover_plugins,
|
|
@@ -352,7 +353,7 @@ def test_normalize_nested_plugin_data_without_stringifying_objects() -> None:
|
|
|
352
353
|
}
|
|
353
354
|
normalized = _normalize_value(value)
|
|
354
355
|
assert normalized["binary"] == "dead"
|
|
355
|
-
assert normalized["when"] == "2024-01-01T00:00:
|
|
356
|
+
assert normalized["when"] == "2024-01-01T00:00:00Z"
|
|
356
357
|
assert normalized["action"] == {"runas": "SYSTEM", "cmd": "cmd.exe"}
|
|
357
358
|
assert normalized["items"] == ["01", {"ok": True}]
|
|
358
359
|
|
|
@@ -377,6 +378,13 @@ def test_plugin_result_conversion_is_ecs_shaped_and_lossless() -> None:
|
|
|
377
378
|
)
|
|
378
379
|
|
|
379
380
|
assert document["event"]["action"] == "example"
|
|
381
|
+
assert document["event"]["provider"] == "registry"
|
|
382
|
+
assert document["event"]["module"] == "windows"
|
|
383
|
+
assert document["event"]["dataset"] == "windows.registry"
|
|
384
|
+
assert document["event"]["kind"] == "event"
|
|
385
|
+
assert document["event"]["category"] == ["registry"]
|
|
386
|
+
assert document["event"]["type"] == ["info"]
|
|
387
|
+
assert document["tags"] == ["registry"]
|
|
380
388
|
assert document["registry"]["hive"] == "HKLM"
|
|
381
389
|
assert document["registry"]["key"] == "SYSTEM\\Control\\Test"
|
|
382
390
|
assert document["registry"]["path"] == "HKLM\\SYSTEM\\Control\\Test\\Payload"
|
|
@@ -386,7 +394,6 @@ def test_plugin_result_conversion_is_ecs_shaped_and_lossless() -> None:
|
|
|
386
394
|
"bytes": "3q0=",
|
|
387
395
|
}
|
|
388
396
|
assert base64.b64decode(document["registry"]["data"]["bytes"]) == b"\xde\xad"
|
|
389
|
-
assert document["ecs"]["version"] == "8.17.0"
|
|
390
397
|
assert document["registry"]["path"] == "HKLM\\SYSTEM\\Control\\Test\\Payload"
|
|
391
398
|
assert document["reg2es"]["value_type"] == result.value_type
|
|
392
399
|
assert document["reg2es"]["value_size"] == 2
|
|
@@ -402,6 +409,80 @@ def test_plugin_result_conversion_is_ecs_shaped_and_lossless() -> None:
|
|
|
402
409
|
}
|
|
403
410
|
|
|
404
411
|
|
|
412
|
+
@pytest.mark.parametrize(
|
|
413
|
+
("tags", "expected"),
|
|
414
|
+
[
|
|
415
|
+
(None, ["registry"]),
|
|
416
|
+
("host-01, registry, case-42", ["registry", "host-01", "case-42"]),
|
|
417
|
+
(["host-01", "registry", "host-01"], ["registry", "host-01"]),
|
|
418
|
+
((" host-01 ", "", "case-42"), ["registry", "host-01", "case-42"]),
|
|
419
|
+
],
|
|
420
|
+
)
|
|
421
|
+
def test_registry_tags_normalize_and_deduplicate(tags, expected) -> None:
|
|
422
|
+
document = plugin_result_to_document(
|
|
423
|
+
PluginResult(), "example", "SYSTEM", "/host/SYSTEM", tags
|
|
424
|
+
)
|
|
425
|
+
assert document["tags"] == expected
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def test_datetime_normalization_uses_z_for_utc_and_preserves_other_offsets() -> None:
|
|
429
|
+
utc = datetime(2015, 10, 30, 7, 24, 57, 814133, tzinfo=timezone.utc)
|
|
430
|
+
assert _normalize_value(utc) == "2015-10-30T07:24:57.814133Z"
|
|
431
|
+
assert _timestamp_to_iso(utc) == "2015-10-30T07:24:57.814133Z"
|
|
432
|
+
assert _timestamp_to_iso(utc.timestamp()) == "2015-10-30T07:24:57.814133Z"
|
|
433
|
+
naive = datetime(2015, 10, 30, 7, 24, 57, 814133)
|
|
434
|
+
assert _normalize_value(naive) == "2015-10-30T07:24:57.814133"
|
|
435
|
+
assert _timestamp_to_iso(naive) is None
|
|
436
|
+
plus_nine = datetime(2026, 10, 2, 13, tzinfo=timezone(timedelta(hours=9)))
|
|
437
|
+
assert _normalize_value(plus_nine) == "2026-10-02T13:00:00+09:00"
|
|
438
|
+
assert _timestamp_to_iso(plus_nine) == "2026-10-02T04:00:00Z"
|
|
439
|
+
result = PluginResult()
|
|
440
|
+
result.set_event_time(plus_nine, source="test", meaning="test")
|
|
441
|
+
document = plugin_result_to_document(result, "example", "SYSTEM", "-")
|
|
442
|
+
assert document["@timestamp"] == "2026-10-02T04:00:00Z"
|
|
443
|
+
assert document["reg2es"]["timestamp"]["raw"] == "2026-10-02T13:00:00+09:00"
|
|
444
|
+
assert result.event_time == plus_nine
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
@pytest.mark.parametrize(
|
|
448
|
+
("original", "expected"),
|
|
449
|
+
[
|
|
450
|
+
(
|
|
451
|
+
datetime(2026, 10, 2, 0, 30, 0, 814133, tzinfo=timezone(timedelta(hours=9))),
|
|
452
|
+
"2026-10-01T15:30:00.814133Z",
|
|
453
|
+
),
|
|
454
|
+
(
|
|
455
|
+
datetime(2026, 10, 2, 23, 30, 0, 814133, tzinfo=timezone(timedelta(hours=-5))),
|
|
456
|
+
"2026-10-03T04:30:00.814133Z",
|
|
457
|
+
),
|
|
458
|
+
],
|
|
459
|
+
)
|
|
460
|
+
def test_event_timestamp_is_utc_while_original_offset_and_precision_are_preserved(
|
|
461
|
+
original, expected
|
|
462
|
+
) -> None:
|
|
463
|
+
result = PluginResult()
|
|
464
|
+
result.mtime = 100.25
|
|
465
|
+
result.custom = {"original_event_time": original}
|
|
466
|
+
result.set_event_time(
|
|
467
|
+
original, source="artifact", meaning="execution", precision="microseconds"
|
|
468
|
+
)
|
|
469
|
+
document = plugin_result_to_document(result, "example", "SYSTEM", "-")
|
|
470
|
+
assert document["@timestamp"] == expected
|
|
471
|
+
assert document["reg2es"]["timestamp"] == {
|
|
472
|
+
"source": "artifact",
|
|
473
|
+
"meaning": "execution",
|
|
474
|
+
"precision": "microseconds",
|
|
475
|
+
"fallback_reason": None,
|
|
476
|
+
"raw": original.isoformat(),
|
|
477
|
+
}
|
|
478
|
+
assert document["reg2es"]["custom"]["original_event_time"] == original.isoformat()
|
|
479
|
+
assert document["reg2es"]["timestamps"]["modified"] == 100.25
|
|
480
|
+
assert document["reg2es"]["timestamp_iso"]["modified"] == (
|
|
481
|
+
"1970-01-01T00:01:40.250000Z"
|
|
482
|
+
)
|
|
483
|
+
assert result.event_time == original
|
|
484
|
+
|
|
485
|
+
|
|
405
486
|
def test_recentdocs_promotes_decoded_name_to_ecs_file_field() -> None:
|
|
406
487
|
result = PluginResult()
|
|
407
488
|
result.path = "ROOT\\Software\\Microsoft\\Windows\\RecentDocs"
|
|
@@ -487,7 +568,7 @@ def test_artifact_timestamp_precedes_last_write_and_preserves_both() -> None:
|
|
|
487
568
|
|
|
488
569
|
document = plugin_result_to_document(result, "example", "SYSTEM", "/host/SYSTEM")
|
|
489
570
|
|
|
490
|
-
assert document["@timestamp"] == "2020-01-02T03:04:05.
|
|
571
|
+
assert document["@timestamp"] == "2020-01-02T03:04:05.123456Z"
|
|
491
572
|
assert document["reg2es"]["timestamp"] == {
|
|
492
573
|
"source": "ShutdownTime",
|
|
493
574
|
"meaning": "shutdown",
|
|
@@ -496,12 +577,13 @@ def test_artifact_timestamp_precedes_last_write_and_preserves_both() -> None:
|
|
|
496
577
|
"raw": "132223214451234560",
|
|
497
578
|
}
|
|
498
579
|
assert document["reg2es"]["timestamps"]["modified"] == result.mtime
|
|
499
|
-
assert document["reg2es"]["timestamp_iso"]["modified"].endswith("
|
|
580
|
+
assert document["reg2es"]["timestamp_iso"]["modified"].endswith("Z")
|
|
500
581
|
|
|
501
582
|
|
|
502
583
|
def test_invalid_artifact_time_falls_back_with_reason() -> None:
|
|
503
584
|
key = RegistryKeyMock.build("Control\\Test")
|
|
504
585
|
result = PluginResult(key=key)
|
|
586
|
+
result.mtime = 145.25
|
|
505
587
|
result.set_event_time(
|
|
506
588
|
datetime(2020, 1, 2, 3, 4, 5),
|
|
507
589
|
source="TypedURLsTime",
|
|
@@ -514,8 +596,10 @@ def test_invalid_artifact_time_falls_back_with_reason() -> None:
|
|
|
514
596
|
result, "typedurls", "NTUSER.DAT", "/host/NTUSER.DAT"
|
|
515
597
|
)
|
|
516
598
|
|
|
517
|
-
assert document["@timestamp"]
|
|
599
|
+
assert document["@timestamp"] == "1970-01-01T00:02:25.250000Z"
|
|
518
600
|
assert document["reg2es"]["timestamp"]["source"] == "key.last_write"
|
|
601
|
+
assert document["reg2es"]["timestamp"]["meaning"] == "registry_key_modified"
|
|
602
|
+
assert document["reg2es"]["timestamp"]["precision"] == "subsecond"
|
|
519
603
|
assert (
|
|
520
604
|
document["reg2es"]["timestamp"]["fallback_reason"] == "invalid_typedurls_time"
|
|
521
605
|
)
|
|
@@ -527,7 +611,7 @@ def test_invalid_artifact_time_is_rejected_without_plugin_hint(value) -> None:
|
|
|
527
611
|
result.mtime = 100
|
|
528
612
|
result.set_event_time(value, source="test", meaning="test")
|
|
529
613
|
document = plugin_result_to_document(result, "example", "SYSTEM", "-")
|
|
530
|
-
assert document["@timestamp"] == "1970-01-01T00:01:
|
|
614
|
+
assert document["@timestamp"] == "1970-01-01T00:01:40Z"
|
|
531
615
|
assert document["reg2es"]["timestamp"]["fallback_reason"] == "invalid_event_time"
|
|
532
616
|
|
|
533
617
|
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""Focused contracts for bounded exports and safe output paths."""
|
|
2
|
+
import importlib
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from reg2es.presenters.Reg2jsonPresenter import Reg2jsonPresenter
|
|
10
|
+
from reg2es.models.Reg2es import plugin_result_to_document
|
|
11
|
+
from reg2es.plugins.base import PluginResult
|
|
12
|
+
from datetime import datetime, timezone
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@pytest.mark.parametrize("output_format", ["jsonl", "ndjson"])
|
|
16
|
+
def test_cli_default_output_uses_line_format_suffix(tmp_path, monkeypatch, output_format):
|
|
17
|
+
module = importlib.import_module("reg2es.views.Reg2jsonView")
|
|
18
|
+
source = tmp_path / "SYSTEM"
|
|
19
|
+
source.write_bytes(b"regf")
|
|
20
|
+
captured = {}
|
|
21
|
+
|
|
22
|
+
class Presenter:
|
|
23
|
+
def __init__(self, **kwargs):
|
|
24
|
+
captured.update(kwargs)
|
|
25
|
+
|
|
26
|
+
def export_json(self):
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
monkeypatch.setattr(module, "Reg2jsonPresenter", Presenter)
|
|
30
|
+
monkeypatch.chdir(tmp_path)
|
|
31
|
+
monkeypatch.setattr(
|
|
32
|
+
sys,
|
|
33
|
+
"argv",
|
|
34
|
+
["reg2json", str(source), "--format", output_format],
|
|
35
|
+
)
|
|
36
|
+
module.Reg2jsonView().run()
|
|
37
|
+
assert Path(captured["output_path"]).name == "SYSTEM.jsonl"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@pytest.mark.parametrize("output_format", ["jsonl", "ndjson"])
|
|
41
|
+
@pytest.mark.parametrize("records", [[], [{"text": "端末\nnext"}, {"n": 2}]])
|
|
42
|
+
def test_jsonl_streams_chunks_without_list_api(tmp_path, monkeypatch, output_format, records):
|
|
43
|
+
module = importlib.import_module("reg2es.presenters.Reg2jsonPresenter")
|
|
44
|
+
state = []
|
|
45
|
+
|
|
46
|
+
class Runner:
|
|
47
|
+
def __init__(self, **kwargs):
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
def gen_records(self):
|
|
51
|
+
try:
|
|
52
|
+
for record in records:
|
|
53
|
+
yield [record]
|
|
54
|
+
finally:
|
|
55
|
+
state.append("generator closed")
|
|
56
|
+
|
|
57
|
+
def close(self):
|
|
58
|
+
state.append("parser closed")
|
|
59
|
+
|
|
60
|
+
monkeypatch.setattr(module, "Reg2es", Runner)
|
|
61
|
+
monkeypatch.chdir(tmp_path)
|
|
62
|
+
presenter = Reg2jsonPresenter("SYSTEM", is_quiet=True, output_format=output_format)
|
|
63
|
+
monkeypatch.setattr(presenter, "reg2json", lambda: pytest.fail("materialized records"))
|
|
64
|
+
paths = presenter.export_json()
|
|
65
|
+
assert paths == [tmp_path / "SYSTEM.jsonl"]
|
|
66
|
+
payload = paths[0].read_bytes()
|
|
67
|
+
assert payload == b"".join((json.dumps(r, ensure_ascii=False, separators=(",", ":")) + "\n").encode() for r in records)
|
|
68
|
+
assert state == ["generator closed", "parser closed"]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@pytest.mark.parametrize("offset", [0, 9, -5])
|
|
72
|
+
def test_json_and_jsonl_preserve_the_same_generated_record_schema(
|
|
73
|
+
tmp_path, monkeypatch, offset
|
|
74
|
+
):
|
|
75
|
+
from datetime import timedelta
|
|
76
|
+
|
|
77
|
+
result = PluginResult()
|
|
78
|
+
result.mtime = 100
|
|
79
|
+
original = datetime(2015, 10, 30, 7, 24, 57, 814133, tzinfo=timezone.utc).astimezone(
|
|
80
|
+
timezone(timedelta(hours=offset))
|
|
81
|
+
)
|
|
82
|
+
result.custom = {"original_event_time": original}
|
|
83
|
+
result.set_event_time(
|
|
84
|
+
original,
|
|
85
|
+
source="test.artifact_time",
|
|
86
|
+
meaning="test_event",
|
|
87
|
+
precision="microseconds",
|
|
88
|
+
raw="raw-time",
|
|
89
|
+
)
|
|
90
|
+
record = plugin_result_to_document(
|
|
91
|
+
result,
|
|
92
|
+
"example",
|
|
93
|
+
"SYSTEM",
|
|
94
|
+
"/evidence/SYSTEM",
|
|
95
|
+
["registry", "host-01"],
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
class Runner:
|
|
99
|
+
def __init__(self, **kwargs):
|
|
100
|
+
pass
|
|
101
|
+
|
|
102
|
+
def gen_records(self):
|
|
103
|
+
yield [record]
|
|
104
|
+
|
|
105
|
+
def close(self):
|
|
106
|
+
pass
|
|
107
|
+
|
|
108
|
+
module = importlib.import_module("reg2es.presenters.Reg2jsonPresenter")
|
|
109
|
+
monkeypatch.setattr(module, "Reg2es", Runner)
|
|
110
|
+
exported = {}
|
|
111
|
+
for output_format in ("json", "jsonl"):
|
|
112
|
+
output = tmp_path / f"records.{output_format}"
|
|
113
|
+
presenter = Reg2jsonPresenter(
|
|
114
|
+
"SYSTEM",
|
|
115
|
+
output_path=str(output),
|
|
116
|
+
is_quiet=True,
|
|
117
|
+
output_format=output_format,
|
|
118
|
+
additional_tags=["registry", "host-01"],
|
|
119
|
+
)
|
|
120
|
+
payload = presenter.export_json()[0].read_bytes()
|
|
121
|
+
parsed = json.loads(payload)
|
|
122
|
+
exported[output_format] = parsed[0] if output_format == "json" else parsed
|
|
123
|
+
|
|
124
|
+
assert exported["json"] == exported["jsonl"] == record
|
|
125
|
+
assert exported["json"]["event"] == {
|
|
126
|
+
"provider": "registry",
|
|
127
|
+
"module": "windows",
|
|
128
|
+
"dataset": "windows.registry",
|
|
129
|
+
"kind": "event",
|
|
130
|
+
"category": ["registry"],
|
|
131
|
+
"type": ["info"],
|
|
132
|
+
"action": "example",
|
|
133
|
+
}
|
|
134
|
+
assert exported["json"]["tags"] == ["registry", "host-01"]
|
|
135
|
+
assert exported["json"]["@timestamp"] == "2015-10-30T07:24:57.814133Z"
|
|
136
|
+
assert exported["json"]["reg2es"]["custom"]["original_event_time"] == (
|
|
137
|
+
original.isoformat().replace("+00:00", "Z")
|
|
138
|
+
)
|
|
139
|
+
assert exported["json"]["reg2es"]["timestamp"] == {
|
|
140
|
+
"source": "test.artifact_time",
|
|
141
|
+
"meaning": "test_event",
|
|
142
|
+
"fallback_reason": None,
|
|
143
|
+
"precision": "microseconds",
|
|
144
|
+
"raw": "raw-time",
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@pytest.mark.parametrize("command", ["Reg2json", "Reg2es"])
|
|
149
|
+
def test_cli_tags_are_parsed_and_forwarded(monkeypatch, tmp_path, command):
|
|
150
|
+
module = importlib.import_module(f"reg2es.views.{command}View")
|
|
151
|
+
source = tmp_path / "SYSTEM"
|
|
152
|
+
source.write_bytes(b"regf")
|
|
153
|
+
captured = {}
|
|
154
|
+
|
|
155
|
+
class Presenter:
|
|
156
|
+
def __init__(self, **kwargs):
|
|
157
|
+
captured.update(kwargs)
|
|
158
|
+
|
|
159
|
+
def export_json(self):
|
|
160
|
+
pass
|
|
161
|
+
|
|
162
|
+
def bulk_import(self):
|
|
163
|
+
pass
|
|
164
|
+
|
|
165
|
+
attribute = "Reg2jsonPresenter" if command == "Reg2json" else "Reg2esPresenter"
|
|
166
|
+
monkeypatch.setattr(module, attribute, Presenter)
|
|
167
|
+
monkeypatch.setattr(
|
|
168
|
+
sys,
|
|
169
|
+
"argv",
|
|
170
|
+
[command.lower(), str(source), "--tags", "registry, host-01,,case-42,host-01"],
|
|
171
|
+
)
|
|
172
|
+
getattr(module, command + "View")().run()
|
|
173
|
+
|
|
174
|
+
assert captured["additional_tags"] == ["registry", "host-01", "case-42", "host-01"]
|
|
@@ -43,7 +43,7 @@ def test_userassist_filetime_submicrosecond_uses_shared_integer_conversion(
|
|
|
43
43
|
UserAssistPlugin(registry, logging.getLogger(__name__), "NTUSER.DAT", "-").run()
|
|
44
44
|
)
|
|
45
45
|
doc = document(results[0], "userassist", "NTUSER.DAT")
|
|
46
|
-
assert doc["@timestamp"] == "2024-01-01T00:00:00.
|
|
46
|
+
assert doc["@timestamp"] == "2024-01-01T00:00:00.123456Z"
|
|
47
47
|
assert doc["reg2es"]["timestamp"]["raw"] == str(FILETIME + 7)
|
|
48
48
|
|
|
49
49
|
|
|
@@ -76,7 +76,7 @@ def test_typedurls_uses_sibling_key_case_insensitive_value_matching():
|
|
|
76
76
|
)
|
|
77
77
|
assert len(results) == 1
|
|
78
78
|
doc = document(results[0], "typedurls", "NTUSER.DAT")
|
|
79
|
-
assert doc["@timestamp"] == "2024-01-01T00:00:00.
|
|
79
|
+
assert doc["@timestamp"] == "2024-01-01T00:00:00.123456Z"
|
|
80
80
|
assert doc["reg2es"]["value_data"] == "https://example.org"
|
|
81
81
|
orjson.dumps(doc)
|
|
82
82
|
|
|
@@ -108,7 +108,7 @@ def test_sam_rid_is_value_type_and_expiry_sentinel_does_not_drop_user():
|
|
|
108
108
|
results = sam_results(sam_f_data())
|
|
109
109
|
assert len(results) == 1
|
|
110
110
|
doc = document(results[0], "localusers", "SAM")
|
|
111
|
-
assert doc["@timestamp"] == "2024-01-01T00:00:00.
|
|
111
|
+
assert doc["@timestamp"] == "2024-01-01T00:00:00.123456Z"
|
|
112
112
|
orjson.dumps(doc)
|
|
113
113
|
|
|
114
114
|
|
|
@@ -201,7 +201,7 @@ def test_office_uses_enabled_time_not_document_creation_time():
|
|
|
201
201
|
)
|
|
202
202
|
assert len(results) == 1
|
|
203
203
|
doc = document(results[0], "office_macros", "NTUSER.DAT")
|
|
204
|
-
assert doc["@timestamp"] == "2024-04-23T22:03:
|
|
204
|
+
assert doc["@timestamp"] == "2024-04-23T22:03:00Z"
|
|
205
205
|
assert doc["reg2es"]["value_data"] == raw.hex()
|
|
206
206
|
|
|
207
207
|
|
|
@@ -239,7 +239,7 @@ def test_tasks_selects_one_representative_time_and_keeps_others_in_custom():
|
|
|
239
239
|
)
|
|
240
240
|
assert len(results) == 1
|
|
241
241
|
doc = document(results[0], "tasks", "SOFTWARE")
|
|
242
|
-
assert doc["@timestamp"] == "2024-01-01T00:00:00.
|
|
242
|
+
assert doc["@timestamp"] == "2024-01-01T00:00:00.123456Z"
|
|
243
243
|
assert doc["reg2es"]["timestamp"]["source"] == "TaskCache.DynamicInfo.last_start"
|
|
244
244
|
# The non-representative candidate stays in custom, not in a second document.
|
|
245
245
|
assert (
|
|
@@ -281,7 +281,7 @@ def test_naive_registry_lastwrite_is_utc_regardless_of_host_timezone(monkeypatch
|
|
|
281
281
|
monkeypatch.setenv("TZ", zone)
|
|
282
282
|
time.tzset()
|
|
283
283
|
doc = document(PluginResult(key=key), "regtime", "SYSTEM")
|
|
284
|
-
assert doc["@timestamp"] == "2024-01-01T00:00:00.
|
|
284
|
+
assert doc["@timestamp"] == "2024-01-01T00:00:00.123456Z"
|
|
285
285
|
finally:
|
|
286
286
|
if original is None:
|
|
287
287
|
monkeypatch.delenv("TZ", raising=False)
|
|
@@ -377,7 +377,7 @@ def test_systeminfo_mixes_time_bearing_and_plain_rows():
|
|
|
377
377
|
assert hostname_doc["@timestamp"] != shutdown_doc["@timestamp"]
|
|
378
378
|
assert hostname_doc["reg2es"]["timestamp"]["source"] == "key.last_write"
|
|
379
379
|
assert shutdown_doc["reg2es"]["timestamp"]["source"] == "ShutdownTime"
|
|
380
|
-
assert shutdown_doc["@timestamp"] == "2024-01-01T00:00:00.
|
|
380
|
+
assert shutdown_doc["@timestamp"] == "2024-01-01T00:00:00.123456Z"
|
|
381
381
|
orjson.dumps(by_value)
|
|
382
382
|
|
|
383
383
|
|
|
@@ -401,7 +401,7 @@ def test_document_count_and_custom_fields_preserved_with_artifact_time():
|
|
|
401
401
|
)
|
|
402
402
|
assert len(results) == 3
|
|
403
403
|
docs = [document(result, "kb", "SOFTWARE") for result in results]
|
|
404
|
-
assert all(doc["@timestamp"] == "2024-01-01T00:00:00.
|
|
404
|
+
assert all(doc["@timestamp"] == "2024-01-01T00:00:00.123456Z" for doc in docs)
|
|
405
405
|
assert sorted(doc["reg2es"]["custom"]["kb"] for doc in docs) == [
|
|
406
406
|
"KB0",
|
|
407
407
|
"KB1",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "2.2.1"
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
"""Focused contracts for bounded exports and safe output paths."""
|
|
2
|
-
import importlib
|
|
3
|
-
import json
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
import sys
|
|
6
|
-
|
|
7
|
-
import pytest
|
|
8
|
-
|
|
9
|
-
from reg2es.presenters.Reg2jsonPresenter import Reg2jsonPresenter
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
@pytest.mark.parametrize("output_format", ["jsonl", "ndjson"])
|
|
13
|
-
def test_cli_default_output_uses_line_format_suffix(tmp_path, monkeypatch, output_format):
|
|
14
|
-
module = importlib.import_module("reg2es.views.Reg2jsonView")
|
|
15
|
-
source = tmp_path / "SYSTEM"
|
|
16
|
-
source.write_bytes(b"regf")
|
|
17
|
-
captured = {}
|
|
18
|
-
|
|
19
|
-
class Presenter:
|
|
20
|
-
def __init__(self, **kwargs):
|
|
21
|
-
captured.update(kwargs)
|
|
22
|
-
|
|
23
|
-
def export_json(self):
|
|
24
|
-
pass
|
|
25
|
-
|
|
26
|
-
monkeypatch.setattr(module, "Reg2jsonPresenter", Presenter)
|
|
27
|
-
monkeypatch.chdir(tmp_path)
|
|
28
|
-
monkeypatch.setattr(
|
|
29
|
-
sys,
|
|
30
|
-
"argv",
|
|
31
|
-
["reg2json", str(source), "--format", output_format],
|
|
32
|
-
)
|
|
33
|
-
module.Reg2jsonView().run()
|
|
34
|
-
assert Path(captured["output_path"]).name == "SYSTEM.jsonl"
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
@pytest.mark.parametrize("output_format", ["jsonl", "ndjson"])
|
|
38
|
-
@pytest.mark.parametrize("records", [[], [{"text": "端末\nnext"}, {"n": 2}]])
|
|
39
|
-
def test_jsonl_streams_chunks_without_list_api(tmp_path, monkeypatch, output_format, records):
|
|
40
|
-
module = importlib.import_module("reg2es.presenters.Reg2jsonPresenter")
|
|
41
|
-
state = []
|
|
42
|
-
|
|
43
|
-
class Runner:
|
|
44
|
-
def __init__(self, **kwargs):
|
|
45
|
-
pass
|
|
46
|
-
|
|
47
|
-
def gen_records(self):
|
|
48
|
-
try:
|
|
49
|
-
for record in records:
|
|
50
|
-
yield [record]
|
|
51
|
-
finally:
|
|
52
|
-
state.append("generator closed")
|
|
53
|
-
|
|
54
|
-
def close(self):
|
|
55
|
-
state.append("parser closed")
|
|
56
|
-
|
|
57
|
-
monkeypatch.setattr(module, "Reg2es", Runner)
|
|
58
|
-
monkeypatch.chdir(tmp_path)
|
|
59
|
-
presenter = Reg2jsonPresenter("SYSTEM", is_quiet=True, output_format=output_format)
|
|
60
|
-
monkeypatch.setattr(presenter, "reg2json", lambda: pytest.fail("materialized records"))
|
|
61
|
-
paths = presenter.export_json()
|
|
62
|
-
assert paths == [tmp_path / "SYSTEM.jsonl"]
|
|
63
|
-
payload = paths[0].read_bytes()
|
|
64
|
-
assert payload == b"".join((json.dumps(r, ensure_ascii=False, separators=(",", ":")) + "\n").encode() for r in records)
|
|
65
|
-
assert state == ["generator closed", "parser closed"]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|