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.
Files changed (117) hide show
  1. {reg2es-2.2.1 → reg2es-2.3.0}/PKG-INFO +15 -35
  2. {reg2es-2.2.1 → reg2es-2.3.0}/README.md +14 -34
  3. reg2es-2.3.0/src/reg2es/__about__.py +1 -0
  4. {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/models/Reg2es.py +28 -18
  5. {reg2es-2.2.1 → reg2es-2.3.0}/tests/test_model.py +91 -7
  6. reg2es-2.3.0/tests/test_output_contract.py +174 -0
  7. {reg2es-2.2.1 → reg2es-2.3.0}/tests/test_timestamp_review.py +8 -8
  8. reg2es-2.2.1/src/reg2es/__about__.py +0 -1
  9. reg2es-2.2.1/tests/test_output_contract.py +0 -65
  10. {reg2es-2.2.1 → reg2es-2.3.0}/.devcontainer/Dockerfile +0 -0
  11. {reg2es-2.2.1 → reg2es-2.3.0}/.devcontainer/devcontainer.json +0 -0
  12. {reg2es-2.2.1 → reg2es-2.3.0}/.devcontainer/docker-compose.yaml +0 -0
  13. {reg2es-2.2.1 → reg2es-2.3.0}/.devcontainer/postCreateCommands.fish +0 -0
  14. {reg2es-2.2.1 → reg2es-2.3.0}/.github/collect_runtime_licenses.py +0 -0
  15. {reg2es-2.2.1 → reg2es-2.3.0}/.github/workflows/publish-binary-pypi.yaml +0 -0
  16. {reg2es-2.2.1 → reg2es-2.3.0}/.github/workflows/publish-release.yaml +0 -0
  17. {reg2es-2.2.1 → reg2es-2.3.0}/.github/workflows/test.yaml +0 -0
  18. {reg2es-2.2.1 → reg2es-2.3.0}/.gitignore +0 -0
  19. {reg2es-2.2.1 → reg2es-2.3.0}/CONTRIBUTING.md +0 -0
  20. {reg2es-2.2.1 → reg2es-2.3.0}/LICENSE +0 -0
  21. {reg2es-2.2.1 → reg2es-2.3.0}/LICENSES/Apache-2.0.txt +0 -0
  22. {reg2es-2.2.1 → reg2es-2.3.0}/pyproject.toml +0 -0
  23. {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/__init__.py +0 -0
  24. {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/models/ElasticsearchUtils.py +0 -0
  25. {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/plugins/__init__.py +0 -0
  26. {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/plugins/antivirus.py +0 -0
  27. {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/plugins/auditpol.py +0 -0
  28. {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/plugins/base.py +0 -0
  29. {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/plugins/compname.py +0 -0
  30. {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/plugins/env.py +0 -0
  31. {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/plugins/filedialogmru.py +0 -0
  32. {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/plugins/gpo.py +0 -0
  33. {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/plugins/kb.py +0 -0
  34. {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/plugins/keyboard.py +0 -0
  35. {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/plugins/lastloggedon.py +0 -0
  36. {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/plugins/lastshutdown.py +0 -0
  37. {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/plugins/localgroups.py +0 -0
  38. {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/plugins/localusers.py +0 -0
  39. {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/plugins/mndmru.py +0 -0
  40. {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/plugins/mounteddevices.py +0 -0
  41. {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/plugins/mstscmru.py +0 -0
  42. {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/plugins/office_macros.py +0 -0
  43. {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/plugins/portproxy.py +0 -0
  44. {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/plugins/printer_history.py +0 -0
  45. {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/plugins/printer_ports.py +0 -0
  46. {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/plugins/proxy.py +0 -0
  47. {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/plugins/putty.py +0 -0
  48. {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/plugins/rdphint.py +0 -0
  49. {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/plugins/recentdocs.py +0 -0
  50. {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/plugins/regtime.py +0 -0
  51. {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/plugins/run.py +0 -0
  52. {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/plugins/runmru.py +0 -0
  53. {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/plugins/services.py +0 -0
  54. {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/plugins/shimcache.py +0 -0
  55. {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/plugins/srum.py +0 -0
  56. {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/plugins/sysinternals.py +0 -0
  57. {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/plugins/systeminfo.py +0 -0
  58. {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/plugins/tasks.py +0 -0
  59. {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/plugins/teamviewer.py +0 -0
  60. {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/plugins/timezone.py +0 -0
  61. {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/plugins/typedurls.py +0 -0
  62. {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/plugins/uninstall.py +0 -0
  63. {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/plugins/usbstor.py +0 -0
  64. {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/plugins/userassist.py +0 -0
  65. {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/plugins/usersids.py +0 -0
  66. {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/plugins/version.py +0 -0
  67. {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/plugins/wordwheelquery.py +0 -0
  68. {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/presenters/Reg2esPresenter.py +0 -0
  69. {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/presenters/Reg2jsonPresenter.py +0 -0
  70. {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/views/BaseView.py +0 -0
  71. {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/views/Reg2esView.py +0 -0
  72. {reg2es-2.2.1 → reg2es-2.3.0}/src/reg2es/views/Reg2jsonView.py +0 -0
  73. {reg2es-2.2.1 → reg2es-2.3.0}/tests/__init__.py +0 -0
  74. {reg2es-2.2.1 → reg2es-2.3.0}/tests/conftest.py +0 -0
  75. {reg2es-2.2.1 → reg2es-2.3.0}/tests/regrippy/__init__.py +0 -0
  76. {reg2es-2.2.1 → reg2es-2.3.0}/tests/regrippy/reg_mock.py +0 -0
  77. {reg2es-2.2.1 → reg2es-2.3.0}/tests/regrippy/test_antivirus.py +0 -0
  78. {reg2es-2.2.1 → reg2es-2.3.0}/tests/regrippy/test_compname.py +0 -0
  79. {reg2es-2.2.1 → reg2es-2.3.0}/tests/regrippy/test_env.py +0 -0
  80. {reg2es-2.2.1 → reg2es-2.3.0}/tests/regrippy/test_filedialogmru.py +0 -0
  81. {reg2es-2.2.1 → reg2es-2.3.0}/tests/regrippy/test_keyboard.py +0 -0
  82. {reg2es-2.2.1 → reg2es-2.3.0}/tests/regrippy/test_lastloggedon.py +0 -0
  83. {reg2es-2.2.1 → reg2es-2.3.0}/tests/regrippy/test_lastshutdown.py +0 -0
  84. {reg2es-2.2.1 → reg2es-2.3.0}/tests/regrippy/test_localgroups.py +0 -0
  85. {reg2es-2.2.1 → reg2es-2.3.0}/tests/regrippy/test_localusers.py +0 -0
  86. {reg2es-2.2.1 → reg2es-2.3.0}/tests/regrippy/test_mndmru.py +0 -0
  87. {reg2es-2.2.1 → reg2es-2.3.0}/tests/regrippy/test_mstscmru.py +0 -0
  88. {reg2es-2.2.1 → reg2es-2.3.0}/tests/regrippy/test_portproxy.py +0 -0
  89. {reg2es-2.2.1 → reg2es-2.3.0}/tests/regrippy/test_printer_history.py +0 -0
  90. {reg2es-2.2.1 → reg2es-2.3.0}/tests/regrippy/test_printer_ports.py +0 -0
  91. {reg2es-2.2.1 → reg2es-2.3.0}/tests/regrippy/test_proxy.py +0 -0
  92. {reg2es-2.2.1 → reg2es-2.3.0}/tests/regrippy/test_putty.py +0 -0
  93. {reg2es-2.2.1 → reg2es-2.3.0}/tests/regrippy/test_rdphint.py +0 -0
  94. {reg2es-2.2.1 → reg2es-2.3.0}/tests/regrippy/test_recentdocs.py +0 -0
  95. {reg2es-2.2.1 → reg2es-2.3.0}/tests/regrippy/test_run.py +0 -0
  96. {reg2es-2.2.1 → reg2es-2.3.0}/tests/regrippy/test_runmru.py +0 -0
  97. {reg2es-2.2.1 → reg2es-2.3.0}/tests/regrippy/test_services.py +0 -0
  98. {reg2es-2.2.1 → reg2es-2.3.0}/tests/regrippy/test_shimcache.py +0 -0
  99. {reg2es-2.2.1 → reg2es-2.3.0}/tests/regrippy/test_sysinternals.py +0 -0
  100. {reg2es-2.2.1 → reg2es-2.3.0}/tests/regrippy/test_systeminfo.py +0 -0
  101. {reg2es-2.2.1 → reg2es-2.3.0}/tests/regrippy/test_tasks.py +0 -0
  102. {reg2es-2.2.1 → reg2es-2.3.0}/tests/regrippy/test_timezone.py +0 -0
  103. {reg2es-2.2.1 → reg2es-2.3.0}/tests/regrippy/test_typedurls.py +0 -0
  104. {reg2es-2.2.1 → reg2es-2.3.0}/tests/regrippy/test_uninstall.py +0 -0
  105. {reg2es-2.2.1 → reg2es-2.3.0}/tests/regrippy/test_userassist.py +0 -0
  106. {reg2es-2.2.1 → reg2es-2.3.0}/tests/regrippy/test_usersids.py +0 -0
  107. {reg2es-2.2.1 → reg2es-2.3.0}/tests/regrippy/test_version.py +0 -0
  108. {reg2es-2.2.1 → reg2es-2.3.0}/tests/test_case_plugins.py +0 -0
  109. {reg2es-2.2.1 → reg2es-2.3.0}/tests/test_es_contract.py +0 -0
  110. {reg2es-2.2.1 → reg2es-2.3.0}/tests/test_integration.py +0 -0
  111. {reg2es-2.2.1 → reg2es-2.3.0}/tests/test_plugin_timestamps.py +0 -0
  112. {reg2es-2.2.1 → reg2es-2.3.0}/tests/test_plugins.py +0 -0
  113. {reg2es-2.2.1 → reg2es-2.3.0}/tests/test_runtime_licenses.py +0 -0
  114. {reg2es-2.2.1 → reg2es-2.3.0}/tests/test_shared_output.py +0 -0
  115. {reg2es-2.2.1 → reg2es-2.3.0}/tests/test_shimcache_entries.py +0 -0
  116. {reg2es-2.2.1 → reg2es-2.3.0}/tests/test_timestamp_section_c.py +0 -0
  117. {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.2.1
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
- The original plugin set is based on
28
- [airbus-cert/regrippy](https://github.com/airbus-cert/regrippy); the package
29
- also includes reg2es-specific additions. The project also draws inspiration
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 the full current interface.
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
- `reg2json` also supports line-delimited output. `--format jsonl` (or `ndjson`)
138
- writes one record per line without holding the entire dataset in memory. When
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
- "ecs": {"version": "8.17.0"},
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
- "value": "ComputerName",
207
- "data": {
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 built with Nuitka are available from GitHub Releases for systems without a Python environment.
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
- The original plugin set is based on
12
- [airbus-cert/regrippy](https://github.com/airbus-cert/regrippy); the package
13
- also includes reg2es-specific additions. The project also draws inspiration
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 the full current interface.
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
- `reg2json` also supports line-delimited output. `--format jsonl` (or `ndjson`)
122
- writes one record per line without holding the entire dataset in memory. When
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
- "ecs": {"version": "8.17.0"},
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
- "value": "ComputerName",
191
- "data": {
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 built with Nuitka are available from GitHub Releases for systems without a Python environment.
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.isoformat()
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 an aware datetime or positive Unix timestamp to UTC ISO 8601."""
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.isoformat()
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).isoformat()
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:00+00: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.123456+00:00"
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("+00:00")
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:40+00:00"
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.123456+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.123456+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.123456+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:00+00:00"
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.123456+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.123456+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.123456+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.123456+00:00" for doc in docs)
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