reg2es 2.2.2__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.2 → reg2es-2.3.0}/PKG-INFO +15 -33
- {reg2es-2.2.2 → reg2es-2.3.0}/README.md +14 -32
- reg2es-2.3.0/src/reg2es/__about__.py +1 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/models/Reg2es.py +28 -14
- {reg2es-2.2.2 → reg2es-2.3.0}/tests/test_model.py +91 -6
- reg2es-2.3.0/tests/test_output_contract.py +174 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/tests/test_timestamp_review.py +8 -8
- reg2es-2.2.2/src/reg2es/__about__.py +0 -1
- reg2es-2.2.2/tests/test_output_contract.py +0 -65
- {reg2es-2.2.2 → reg2es-2.3.0}/.devcontainer/Dockerfile +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/.devcontainer/devcontainer.json +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/.devcontainer/docker-compose.yaml +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/.devcontainer/postCreateCommands.fish +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/.github/collect_runtime_licenses.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/.github/workflows/publish-binary-pypi.yaml +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/.github/workflows/publish-release.yaml +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/.github/workflows/test.yaml +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/.gitignore +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/CONTRIBUTING.md +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/LICENSE +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/LICENSES/Apache-2.0.txt +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/pyproject.toml +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/__init__.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/models/ElasticsearchUtils.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/plugins/__init__.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/plugins/antivirus.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/plugins/auditpol.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/plugins/base.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/plugins/compname.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/plugins/env.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/plugins/filedialogmru.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/plugins/gpo.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/plugins/kb.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/plugins/keyboard.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/plugins/lastloggedon.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/plugins/lastshutdown.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/plugins/localgroups.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/plugins/localusers.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/plugins/mndmru.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/plugins/mounteddevices.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/plugins/mstscmru.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/plugins/office_macros.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/plugins/portproxy.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/plugins/printer_history.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/plugins/printer_ports.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/plugins/proxy.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/plugins/putty.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/plugins/rdphint.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/plugins/recentdocs.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/plugins/regtime.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/plugins/run.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/plugins/runmru.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/plugins/services.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/plugins/shimcache.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/plugins/srum.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/plugins/sysinternals.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/plugins/systeminfo.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/plugins/tasks.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/plugins/teamviewer.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/plugins/timezone.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/plugins/typedurls.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/plugins/uninstall.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/plugins/usbstor.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/plugins/userassist.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/plugins/usersids.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/plugins/version.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/plugins/wordwheelquery.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/presenters/Reg2esPresenter.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/presenters/Reg2jsonPresenter.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/views/BaseView.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/views/Reg2esView.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/views/Reg2jsonView.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/tests/__init__.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/tests/conftest.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/tests/regrippy/__init__.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/tests/regrippy/reg_mock.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/tests/regrippy/test_antivirus.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/tests/regrippy/test_compname.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/tests/regrippy/test_env.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/tests/regrippy/test_filedialogmru.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/tests/regrippy/test_keyboard.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/tests/regrippy/test_lastloggedon.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/tests/regrippy/test_lastshutdown.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/tests/regrippy/test_localgroups.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/tests/regrippy/test_localusers.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/tests/regrippy/test_mndmru.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/tests/regrippy/test_mstscmru.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/tests/regrippy/test_portproxy.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/tests/regrippy/test_printer_history.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/tests/regrippy/test_printer_ports.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/tests/regrippy/test_proxy.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/tests/regrippy/test_putty.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/tests/regrippy/test_rdphint.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/tests/regrippy/test_recentdocs.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/tests/regrippy/test_run.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/tests/regrippy/test_runmru.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/tests/regrippy/test_services.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/tests/regrippy/test_shimcache.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/tests/regrippy/test_sysinternals.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/tests/regrippy/test_systeminfo.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/tests/regrippy/test_tasks.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/tests/regrippy/test_timezone.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/tests/regrippy/test_typedurls.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/tests/regrippy/test_uninstall.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/tests/regrippy/test_userassist.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/tests/regrippy/test_usersids.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/tests/regrippy/test_version.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/tests/test_case_plugins.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/tests/test_es_contract.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/tests/test_integration.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/tests/test_plugin_timestamps.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/tests/test_plugins.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/tests/test_runtime_licenses.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/tests/test_shared_output.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/tests/test_shimcache_entries.py +0 -0
- {reg2es-2.2.2 → reg2es-2.3.0}/tests/test_timestamp_section_c.py +0 -0
- {reg2es-2.2.2 → 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,26 +167,13 @@ result: list[dict] = reg2json(
|
|
|
172
167
|
|
|
173
168
|
## Output Format Example
|
|
174
169
|
|
|
175
|
-
Each plugin result becomes one document. Standard `event`,
|
|
176
|
-
`registry`, `log.file`, `tags`, and `@timestamp` fields describe the artifact.
|
|
177
|
-
Lossless plugin-specific data and the original offline-hive location are kept
|
|
178
|
-
under `reg2es`.
|
|
179
|
-
|
|
180
|
-
When a plugin has a reliable artifact time, it uses that time for `@timestamp`.
|
|
181
|
-
Otherwise it uses the registry key's LastWrite; if no valid time exists,
|
|
182
|
-
`@timestamp` is omitted. Check `reg2es.timestamp.source` and
|
|
183
|
-
`reg2es.timestamp.meaning` before treating a result as an activity time.
|
|
184
|
-
Original values and fallback reasons remain in the document.
|
|
185
|
-
|
|
186
|
-
The current ECS output changes `registry.data.bytes` from a byte count to
|
|
187
|
-
Base64, uses `REG_*` names in `registry.data.type`, and includes the value
|
|
188
|
-
name in `registry.path`. Existing Elasticsearch mappings may conflict with
|
|
189
|
-
these fields; use a new index for the updated output.
|
|
190
|
-
|
|
191
170
|
```json
|
|
192
171
|
{
|
|
193
|
-
"@timestamp": "2015-10-30T07:24:57.
|
|
172
|
+
"@timestamp": "2015-10-30T07:24:57.814133Z",
|
|
194
173
|
"event": {
|
|
174
|
+
"provider": "registry",
|
|
175
|
+
"module": "windows",
|
|
176
|
+
"dataset": "windows.registry",
|
|
195
177
|
"kind": "event",
|
|
196
178
|
"category": ["registry"],
|
|
197
179
|
"type": ["info"],
|
|
@@ -201,8 +183,8 @@ these fields; use a new index for the updated output.
|
|
|
201
183
|
"hive": "HKLM",
|
|
202
184
|
"key": "SYSTEM\\ControlSet001\\Control\\ComputerName\\ComputerName",
|
|
203
185
|
"path": "HKLM\\SYSTEM\\ControlSet001\\Control\\ComputerName\\ComputerName\\ComputerName",
|
|
204
|
-
|
|
205
|
-
|
|
186
|
+
"value": "ComputerName",
|
|
187
|
+
"data": {
|
|
206
188
|
"type": "REG_SZ",
|
|
207
189
|
"strings": ["DESKTOP-EXAMPLE"]
|
|
208
190
|
}
|
|
@@ -240,7 +222,7 @@ $ uv add reg2es
|
|
|
240
222
|
|
|
241
223
|
### From GitHub Releases
|
|
242
224
|
|
|
243
|
-
Standalone binaries
|
|
225
|
+
Standalone binaries are available from GitHub Releases for systems without a Python environment.
|
|
244
226
|
|
|
245
227
|
```bash
|
|
246
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,26 +151,13 @@ result: list[dict] = reg2json(
|
|
|
156
151
|
|
|
157
152
|
## Output Format Example
|
|
158
153
|
|
|
159
|
-
Each plugin result becomes one document. Standard `event`,
|
|
160
|
-
`registry`, `log.file`, `tags`, and `@timestamp` fields describe the artifact.
|
|
161
|
-
Lossless plugin-specific data and the original offline-hive location are kept
|
|
162
|
-
under `reg2es`.
|
|
163
|
-
|
|
164
|
-
When a plugin has a reliable artifact time, it uses that time for `@timestamp`.
|
|
165
|
-
Otherwise it uses the registry key's LastWrite; if no valid time exists,
|
|
166
|
-
`@timestamp` is omitted. Check `reg2es.timestamp.source` and
|
|
167
|
-
`reg2es.timestamp.meaning` before treating a result as an activity time.
|
|
168
|
-
Original values and fallback reasons remain in the document.
|
|
169
|
-
|
|
170
|
-
The current ECS output changes `registry.data.bytes` from a byte count to
|
|
171
|
-
Base64, uses `REG_*` names in `registry.data.type`, and includes the value
|
|
172
|
-
name in `registry.path`. Existing Elasticsearch mappings may conflict with
|
|
173
|
-
these fields; use a new index for the updated output.
|
|
174
|
-
|
|
175
154
|
```json
|
|
176
155
|
{
|
|
177
|
-
"@timestamp": "2015-10-30T07:24:57.
|
|
156
|
+
"@timestamp": "2015-10-30T07:24:57.814133Z",
|
|
178
157
|
"event": {
|
|
158
|
+
"provider": "registry",
|
|
159
|
+
"module": "windows",
|
|
160
|
+
"dataset": "windows.registry",
|
|
179
161
|
"kind": "event",
|
|
180
162
|
"category": ["registry"],
|
|
181
163
|
"type": ["info"],
|
|
@@ -185,8 +167,8 @@ these fields; use a new index for the updated output.
|
|
|
185
167
|
"hive": "HKLM",
|
|
186
168
|
"key": "SYSTEM\\ControlSet001\\Control\\ComputerName\\ComputerName",
|
|
187
169
|
"path": "HKLM\\SYSTEM\\ControlSet001\\Control\\ComputerName\\ComputerName\\ComputerName",
|
|
188
|
-
|
|
189
|
-
|
|
170
|
+
"value": "ComputerName",
|
|
171
|
+
"data": {
|
|
190
172
|
"type": "REG_SZ",
|
|
191
173
|
"strings": ["DESKTOP-EXAMPLE"]
|
|
192
174
|
}
|
|
@@ -224,7 +206,7 @@ $ uv add reg2es
|
|
|
224
206
|
|
|
225
207
|
### From GitHub Releases
|
|
226
208
|
|
|
227
|
-
Standalone binaries
|
|
209
|
+
Standalone binaries are available from GitHub Releases for systems without a Python environment.
|
|
228
210
|
|
|
229
211
|
```bash
|
|
230
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
|
|
|
@@ -509,7 +526,7 @@ def plugin_result_to_document(
|
|
|
509
526
|
plugin_name: str,
|
|
510
527
|
hive_name: str,
|
|
511
528
|
hive_path: str,
|
|
512
|
-
additional_tags: Optional[Sequence[str]] = None,
|
|
529
|
+
additional_tags: Optional[Union[str, Sequence[str]]] = None,
|
|
513
530
|
recovery: Optional[dict] = None,
|
|
514
531
|
) -> dict:
|
|
515
532
|
"""Convert a single PluginResult to an ECS-compliant document.
|
|
@@ -570,6 +587,9 @@ def plugin_result_to_document(
|
|
|
570
587
|
registry_path = _join_registry_path(registry_path, result.value_name)
|
|
571
588
|
doc: dict = {
|
|
572
589
|
"event": {
|
|
590
|
+
"provider": "registry",
|
|
591
|
+
"module": "windows",
|
|
592
|
+
"dataset": "windows.registry",
|
|
573
593
|
"kind": "event",
|
|
574
594
|
"category": ["registry"],
|
|
575
595
|
"type": ["info"],
|
|
@@ -585,11 +605,7 @@ def plugin_result_to_document(
|
|
|
585
605
|
"path": (str(Path(hive_path).resolve()) if hive_path != "-" else "-"),
|
|
586
606
|
},
|
|
587
607
|
},
|
|
588
|
-
"tags": list(
|
|
589
|
-
dict.fromkeys(
|
|
590
|
-
["registry", *(additional_tags or [])],
|
|
591
|
-
)
|
|
592
|
-
),
|
|
608
|
+
"tags": list(dict.fromkeys(["registry", *_normalize_tags(additional_tags)])),
|
|
593
609
|
"reg2es": {
|
|
594
610
|
"plugin": {"name": plugin_name},
|
|
595
611
|
"source": {"hive": hive_name, "key_path": result.path},
|
|
@@ -772,7 +788,7 @@ class Reg2es:
|
|
|
772
788
|
plugin_names: Optional[Sequence[str]] = None,
|
|
773
789
|
chunk_size: int = 500,
|
|
774
790
|
error_policy: str = "continue",
|
|
775
|
-
additional_tags: Optional[Sequence[str]] = None,
|
|
791
|
+
additional_tags: Optional[Union[str, Sequence[str]]] = None,
|
|
776
792
|
) -> None:
|
|
777
793
|
if error_policy not in ("continue", "raise"):
|
|
778
794
|
raise ValueError(
|
|
@@ -786,9 +802,7 @@ class Reg2es:
|
|
|
786
802
|
self.input_paths = [Path(path) for path in input_paths]
|
|
787
803
|
self.chunk_size = chunk_size
|
|
788
804
|
self.error_policy = error_policy
|
|
789
|
-
self.additional_tags =
|
|
790
|
-
tag.strip() for tag in (additional_tags or []) if tag and tag.strip()
|
|
791
|
-
]
|
|
805
|
+
self.additional_tags = _normalize_tags(additional_tags)
|
|
792
806
|
|
|
793
807
|
# Discover and validate plugins once.
|
|
794
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"
|
|
@@ -401,6 +409,80 @@ def test_plugin_result_conversion_is_ecs_shaped_and_lossless() -> None:
|
|
|
401
409
|
}
|
|
402
410
|
|
|
403
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
|
+
|
|
404
486
|
def test_recentdocs_promotes_decoded_name_to_ecs_file_field() -> None:
|
|
405
487
|
result = PluginResult()
|
|
406
488
|
result.path = "ROOT\\Software\\Microsoft\\Windows\\RecentDocs"
|
|
@@ -486,7 +568,7 @@ def test_artifact_timestamp_precedes_last_write_and_preserves_both() -> None:
|
|
|
486
568
|
|
|
487
569
|
document = plugin_result_to_document(result, "example", "SYSTEM", "/host/SYSTEM")
|
|
488
570
|
|
|
489
|
-
assert document["@timestamp"] == "2020-01-02T03:04:05.
|
|
571
|
+
assert document["@timestamp"] == "2020-01-02T03:04:05.123456Z"
|
|
490
572
|
assert document["reg2es"]["timestamp"] == {
|
|
491
573
|
"source": "ShutdownTime",
|
|
492
574
|
"meaning": "shutdown",
|
|
@@ -495,12 +577,13 @@ def test_artifact_timestamp_precedes_last_write_and_preserves_both() -> None:
|
|
|
495
577
|
"raw": "132223214451234560",
|
|
496
578
|
}
|
|
497
579
|
assert document["reg2es"]["timestamps"]["modified"] == result.mtime
|
|
498
|
-
assert document["reg2es"]["timestamp_iso"]["modified"].endswith("
|
|
580
|
+
assert document["reg2es"]["timestamp_iso"]["modified"].endswith("Z")
|
|
499
581
|
|
|
500
582
|
|
|
501
583
|
def test_invalid_artifact_time_falls_back_with_reason() -> None:
|
|
502
584
|
key = RegistryKeyMock.build("Control\\Test")
|
|
503
585
|
result = PluginResult(key=key)
|
|
586
|
+
result.mtime = 145.25
|
|
504
587
|
result.set_event_time(
|
|
505
588
|
datetime(2020, 1, 2, 3, 4, 5),
|
|
506
589
|
source="TypedURLsTime",
|
|
@@ -513,8 +596,10 @@ def test_invalid_artifact_time_falls_back_with_reason() -> None:
|
|
|
513
596
|
result, "typedurls", "NTUSER.DAT", "/host/NTUSER.DAT"
|
|
514
597
|
)
|
|
515
598
|
|
|
516
|
-
assert document["@timestamp"]
|
|
599
|
+
assert document["@timestamp"] == "1970-01-01T00:02:25.250000Z"
|
|
517
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"
|
|
518
603
|
assert (
|
|
519
604
|
document["reg2es"]["timestamp"]["fallback_reason"] == "invalid_typedurls_time"
|
|
520
605
|
)
|
|
@@ -526,7 +611,7 @@ def test_invalid_artifact_time_is_rejected_without_plugin_hint(value) -> None:
|
|
|
526
611
|
result.mtime = 100
|
|
527
612
|
result.set_event_time(value, source="test", meaning="test")
|
|
528
613
|
document = plugin_result_to_document(result, "example", "SYSTEM", "-")
|
|
529
|
-
assert document["@timestamp"] == "1970-01-01T00:01:
|
|
614
|
+
assert document["@timestamp"] == "1970-01-01T00:01:40Z"
|
|
530
615
|
assert document["reg2es"]["timestamp"]["fallback_reason"] == "invalid_event_time"
|
|
531
616
|
|
|
532
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.2"
|
|
@@ -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
|