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.
Files changed (117) hide show
  1. {reg2es-2.2.2 → reg2es-2.3.0}/PKG-INFO +15 -33
  2. {reg2es-2.2.2 → reg2es-2.3.0}/README.md +14 -32
  3. reg2es-2.3.0/src/reg2es/__about__.py +1 -0
  4. {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/models/Reg2es.py +28 -14
  5. {reg2es-2.2.2 → reg2es-2.3.0}/tests/test_model.py +91 -6
  6. reg2es-2.3.0/tests/test_output_contract.py +174 -0
  7. {reg2es-2.2.2 → reg2es-2.3.0}/tests/test_timestamp_review.py +8 -8
  8. reg2es-2.2.2/src/reg2es/__about__.py +0 -1
  9. reg2es-2.2.2/tests/test_output_contract.py +0 -65
  10. {reg2es-2.2.2 → reg2es-2.3.0}/.devcontainer/Dockerfile +0 -0
  11. {reg2es-2.2.2 → reg2es-2.3.0}/.devcontainer/devcontainer.json +0 -0
  12. {reg2es-2.2.2 → reg2es-2.3.0}/.devcontainer/docker-compose.yaml +0 -0
  13. {reg2es-2.2.2 → reg2es-2.3.0}/.devcontainer/postCreateCommands.fish +0 -0
  14. {reg2es-2.2.2 → reg2es-2.3.0}/.github/collect_runtime_licenses.py +0 -0
  15. {reg2es-2.2.2 → reg2es-2.3.0}/.github/workflows/publish-binary-pypi.yaml +0 -0
  16. {reg2es-2.2.2 → reg2es-2.3.0}/.github/workflows/publish-release.yaml +0 -0
  17. {reg2es-2.2.2 → reg2es-2.3.0}/.github/workflows/test.yaml +0 -0
  18. {reg2es-2.2.2 → reg2es-2.3.0}/.gitignore +0 -0
  19. {reg2es-2.2.2 → reg2es-2.3.0}/CONTRIBUTING.md +0 -0
  20. {reg2es-2.2.2 → reg2es-2.3.0}/LICENSE +0 -0
  21. {reg2es-2.2.2 → reg2es-2.3.0}/LICENSES/Apache-2.0.txt +0 -0
  22. {reg2es-2.2.2 → reg2es-2.3.0}/pyproject.toml +0 -0
  23. {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/__init__.py +0 -0
  24. {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/models/ElasticsearchUtils.py +0 -0
  25. {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/plugins/__init__.py +0 -0
  26. {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/plugins/antivirus.py +0 -0
  27. {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/plugins/auditpol.py +0 -0
  28. {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/plugins/base.py +0 -0
  29. {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/plugins/compname.py +0 -0
  30. {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/plugins/env.py +0 -0
  31. {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/plugins/filedialogmru.py +0 -0
  32. {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/plugins/gpo.py +0 -0
  33. {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/plugins/kb.py +0 -0
  34. {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/plugins/keyboard.py +0 -0
  35. {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/plugins/lastloggedon.py +0 -0
  36. {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/plugins/lastshutdown.py +0 -0
  37. {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/plugins/localgroups.py +0 -0
  38. {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/plugins/localusers.py +0 -0
  39. {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/plugins/mndmru.py +0 -0
  40. {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/plugins/mounteddevices.py +0 -0
  41. {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/plugins/mstscmru.py +0 -0
  42. {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/plugins/office_macros.py +0 -0
  43. {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/plugins/portproxy.py +0 -0
  44. {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/plugins/printer_history.py +0 -0
  45. {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/plugins/printer_ports.py +0 -0
  46. {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/plugins/proxy.py +0 -0
  47. {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/plugins/putty.py +0 -0
  48. {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/plugins/rdphint.py +0 -0
  49. {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/plugins/recentdocs.py +0 -0
  50. {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/plugins/regtime.py +0 -0
  51. {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/plugins/run.py +0 -0
  52. {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/plugins/runmru.py +0 -0
  53. {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/plugins/services.py +0 -0
  54. {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/plugins/shimcache.py +0 -0
  55. {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/plugins/srum.py +0 -0
  56. {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/plugins/sysinternals.py +0 -0
  57. {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/plugins/systeminfo.py +0 -0
  58. {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/plugins/tasks.py +0 -0
  59. {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/plugins/teamviewer.py +0 -0
  60. {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/plugins/timezone.py +0 -0
  61. {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/plugins/typedurls.py +0 -0
  62. {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/plugins/uninstall.py +0 -0
  63. {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/plugins/usbstor.py +0 -0
  64. {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/plugins/userassist.py +0 -0
  65. {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/plugins/usersids.py +0 -0
  66. {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/plugins/version.py +0 -0
  67. {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/plugins/wordwheelquery.py +0 -0
  68. {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/presenters/Reg2esPresenter.py +0 -0
  69. {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/presenters/Reg2jsonPresenter.py +0 -0
  70. {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/views/BaseView.py +0 -0
  71. {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/views/Reg2esView.py +0 -0
  72. {reg2es-2.2.2 → reg2es-2.3.0}/src/reg2es/views/Reg2jsonView.py +0 -0
  73. {reg2es-2.2.2 → reg2es-2.3.0}/tests/__init__.py +0 -0
  74. {reg2es-2.2.2 → reg2es-2.3.0}/tests/conftest.py +0 -0
  75. {reg2es-2.2.2 → reg2es-2.3.0}/tests/regrippy/__init__.py +0 -0
  76. {reg2es-2.2.2 → reg2es-2.3.0}/tests/regrippy/reg_mock.py +0 -0
  77. {reg2es-2.2.2 → reg2es-2.3.0}/tests/regrippy/test_antivirus.py +0 -0
  78. {reg2es-2.2.2 → reg2es-2.3.0}/tests/regrippy/test_compname.py +0 -0
  79. {reg2es-2.2.2 → reg2es-2.3.0}/tests/regrippy/test_env.py +0 -0
  80. {reg2es-2.2.2 → reg2es-2.3.0}/tests/regrippy/test_filedialogmru.py +0 -0
  81. {reg2es-2.2.2 → reg2es-2.3.0}/tests/regrippy/test_keyboard.py +0 -0
  82. {reg2es-2.2.2 → reg2es-2.3.0}/tests/regrippy/test_lastloggedon.py +0 -0
  83. {reg2es-2.2.2 → reg2es-2.3.0}/tests/regrippy/test_lastshutdown.py +0 -0
  84. {reg2es-2.2.2 → reg2es-2.3.0}/tests/regrippy/test_localgroups.py +0 -0
  85. {reg2es-2.2.2 → reg2es-2.3.0}/tests/regrippy/test_localusers.py +0 -0
  86. {reg2es-2.2.2 → reg2es-2.3.0}/tests/regrippy/test_mndmru.py +0 -0
  87. {reg2es-2.2.2 → reg2es-2.3.0}/tests/regrippy/test_mstscmru.py +0 -0
  88. {reg2es-2.2.2 → reg2es-2.3.0}/tests/regrippy/test_portproxy.py +0 -0
  89. {reg2es-2.2.2 → reg2es-2.3.0}/tests/regrippy/test_printer_history.py +0 -0
  90. {reg2es-2.2.2 → reg2es-2.3.0}/tests/regrippy/test_printer_ports.py +0 -0
  91. {reg2es-2.2.2 → reg2es-2.3.0}/tests/regrippy/test_proxy.py +0 -0
  92. {reg2es-2.2.2 → reg2es-2.3.0}/tests/regrippy/test_putty.py +0 -0
  93. {reg2es-2.2.2 → reg2es-2.3.0}/tests/regrippy/test_rdphint.py +0 -0
  94. {reg2es-2.2.2 → reg2es-2.3.0}/tests/regrippy/test_recentdocs.py +0 -0
  95. {reg2es-2.2.2 → reg2es-2.3.0}/tests/regrippy/test_run.py +0 -0
  96. {reg2es-2.2.2 → reg2es-2.3.0}/tests/regrippy/test_runmru.py +0 -0
  97. {reg2es-2.2.2 → reg2es-2.3.0}/tests/regrippy/test_services.py +0 -0
  98. {reg2es-2.2.2 → reg2es-2.3.0}/tests/regrippy/test_shimcache.py +0 -0
  99. {reg2es-2.2.2 → reg2es-2.3.0}/tests/regrippy/test_sysinternals.py +0 -0
  100. {reg2es-2.2.2 → reg2es-2.3.0}/tests/regrippy/test_systeminfo.py +0 -0
  101. {reg2es-2.2.2 → reg2es-2.3.0}/tests/regrippy/test_tasks.py +0 -0
  102. {reg2es-2.2.2 → reg2es-2.3.0}/tests/regrippy/test_timezone.py +0 -0
  103. {reg2es-2.2.2 → reg2es-2.3.0}/tests/regrippy/test_typedurls.py +0 -0
  104. {reg2es-2.2.2 → reg2es-2.3.0}/tests/regrippy/test_uninstall.py +0 -0
  105. {reg2es-2.2.2 → reg2es-2.3.0}/tests/regrippy/test_userassist.py +0 -0
  106. {reg2es-2.2.2 → reg2es-2.3.0}/tests/regrippy/test_usersids.py +0 -0
  107. {reg2es-2.2.2 → reg2es-2.3.0}/tests/regrippy/test_version.py +0 -0
  108. {reg2es-2.2.2 → reg2es-2.3.0}/tests/test_case_plugins.py +0 -0
  109. {reg2es-2.2.2 → reg2es-2.3.0}/tests/test_es_contract.py +0 -0
  110. {reg2es-2.2.2 → reg2es-2.3.0}/tests/test_integration.py +0 -0
  111. {reg2es-2.2.2 → reg2es-2.3.0}/tests/test_plugin_timestamps.py +0 -0
  112. {reg2es-2.2.2 → reg2es-2.3.0}/tests/test_plugins.py +0 -0
  113. {reg2es-2.2.2 → reg2es-2.3.0}/tests/test_runtime_licenses.py +0 -0
  114. {reg2es-2.2.2 → reg2es-2.3.0}/tests/test_shared_output.py +0 -0
  115. {reg2es-2.2.2 → reg2es-2.3.0}/tests/test_shimcache_entries.py +0 -0
  116. {reg2es-2.2.2 → reg2es-2.3.0}/tests/test_timestamp_section_c.py +0 -0
  117. {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.2.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
- 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,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.814133+00:00",
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
- "value": "ComputerName",
205
- "data": {
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 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.
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
- 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,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.814133+00:00",
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
- "value": "ComputerName",
189
- "data": {
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 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.
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.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
 
@@ -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: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"
@@ -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.123456+00:00"
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("+00:00")
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:40+00:00"
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.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.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