flow.record 3.15.dev15__tar.gz → 3.16.dev1__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.
- {flow.record-3.15.dev15/flow.record.egg-info → flow_record-3.16.dev1}/PKG-INFO +2 -1
- {flow.record-3.15.dev15 → flow_record-3.16.dev1}/flow/record/adapter/elastic.py +39 -6
- {flow.record-3.15.dev15 → flow_record-3.16.dev1}/flow/record/fieldtypes/__init__.py +120 -7
- {flow.record-3.15.dev15 → flow_record-3.16.dev1}/flow/record/jsonpacker.py +5 -0
- {flow.record-3.15.dev15 → flow_record-3.16.dev1}/flow/record/version.py +2 -2
- {flow.record-3.15.dev15 → flow_record-3.16.dev1}/flow/record/whitelist.py +1 -0
- {flow.record-3.15.dev15 → flow_record-3.16.dev1/flow.record.egg-info}/PKG-INFO +2 -1
- {flow.record-3.15.dev15 → flow_record-3.16.dev1}/flow.record.egg-info/SOURCES.txt +1 -0
- {flow.record-3.15.dev15 → flow_record-3.16.dev1}/flow.record.egg-info/requires.txt +1 -0
- {flow.record-3.15.dev15 → flow_record-3.16.dev1}/pyproject.toml +1 -0
- flow_record-3.16.dev1/tests/test_elastic_adapter.py +53 -0
- {flow.record-3.15.dev15 → flow_record-3.16.dev1}/tests/test_fieldtypes.py +141 -7
- {flow.record-3.15.dev15 → flow_record-3.16.dev1}/COPYRIGHT +0 -0
- {flow.record-3.15.dev15 → flow_record-3.16.dev1}/LICENSE +0 -0
- {flow.record-3.15.dev15 → flow_record-3.16.dev1}/MANIFEST.in +0 -0
- {flow.record-3.15.dev15 → flow_record-3.16.dev1}/README.md +0 -0
- {flow.record-3.15.dev15 → flow_record-3.16.dev1}/examples/filesystem.py +0 -0
- {flow.record-3.15.dev15 → flow_record-3.16.dev1}/examples/passivedns.py +0 -0
- {flow.record-3.15.dev15 → flow_record-3.16.dev1}/examples/records.json +0 -0
- {flow.record-3.15.dev15 → flow_record-3.16.dev1}/examples/tcpconn.py +0 -0
- {flow.record-3.15.dev15 → flow_record-3.16.dev1}/flow/record/__init__.py +0 -0
- {flow.record-3.15.dev15 → flow_record-3.16.dev1}/flow/record/adapter/__init__.py +0 -0
- {flow.record-3.15.dev15 → flow_record-3.16.dev1}/flow/record/adapter/archive.py +0 -0
- {flow.record-3.15.dev15 → flow_record-3.16.dev1}/flow/record/adapter/avro.py +0 -0
- {flow.record-3.15.dev15 → flow_record-3.16.dev1}/flow/record/adapter/broker.py +0 -0
- {flow.record-3.15.dev15 → flow_record-3.16.dev1}/flow/record/adapter/csvfile.py +0 -0
- {flow.record-3.15.dev15 → flow_record-3.16.dev1}/flow/record/adapter/duckdb.py +0 -0
- {flow.record-3.15.dev15 → flow_record-3.16.dev1}/flow/record/adapter/jsonfile.py +0 -0
- {flow.record-3.15.dev15 → flow_record-3.16.dev1}/flow/record/adapter/line.py +0 -0
- {flow.record-3.15.dev15 → flow_record-3.16.dev1}/flow/record/adapter/mongo.py +0 -0
- {flow.record-3.15.dev15 → flow_record-3.16.dev1}/flow/record/adapter/split.py +0 -0
- {flow.record-3.15.dev15 → flow_record-3.16.dev1}/flow/record/adapter/splunk.py +0 -0
- {flow.record-3.15.dev15 → flow_record-3.16.dev1}/flow/record/adapter/sqlite.py +0 -0
- {flow.record-3.15.dev15 → flow_record-3.16.dev1}/flow/record/adapter/stream.py +0 -0
- {flow.record-3.15.dev15 → flow_record-3.16.dev1}/flow/record/adapter/text.py +0 -0
- {flow.record-3.15.dev15 → flow_record-3.16.dev1}/flow/record/adapter/xlsx.py +0 -0
- {flow.record-3.15.dev15 → flow_record-3.16.dev1}/flow/record/base.py +0 -0
- {flow.record-3.15.dev15 → flow_record-3.16.dev1}/flow/record/exceptions.py +0 -0
- {flow.record-3.15.dev15 → flow_record-3.16.dev1}/flow/record/fieldtypes/credential.py +0 -0
- {flow.record-3.15.dev15 → flow_record-3.16.dev1}/flow/record/fieldtypes/net/__init__.py +0 -0
- {flow.record-3.15.dev15 → flow_record-3.16.dev1}/flow/record/fieldtypes/net/ip.py +0 -0
- {flow.record-3.15.dev15 → flow_record-3.16.dev1}/flow/record/fieldtypes/net/ipv4.py +0 -0
- {flow.record-3.15.dev15 → flow_record-3.16.dev1}/flow/record/fieldtypes/net/tcp.py +0 -0
- {flow.record-3.15.dev15 → flow_record-3.16.dev1}/flow/record/fieldtypes/net/udp.py +0 -0
- {flow.record-3.15.dev15 → flow_record-3.16.dev1}/flow/record/packer.py +0 -0
- {flow.record-3.15.dev15 → flow_record-3.16.dev1}/flow/record/selector.py +0 -0
- {flow.record-3.15.dev15 → flow_record-3.16.dev1}/flow/record/stream.py +0 -0
- {flow.record-3.15.dev15 → flow_record-3.16.dev1}/flow/record/tools/__init__.py +0 -0
- {flow.record-3.15.dev15 → flow_record-3.16.dev1}/flow/record/tools/geoip.py +0 -0
- {flow.record-3.15.dev15 → flow_record-3.16.dev1}/flow/record/tools/rdump.py +0 -0
- {flow.record-3.15.dev15 → flow_record-3.16.dev1}/flow/record/utils.py +0 -0
- {flow.record-3.15.dev15 → flow_record-3.16.dev1}/flow.record.egg-info/dependency_links.txt +0 -0
- {flow.record-3.15.dev15 → flow_record-3.16.dev1}/flow.record.egg-info/entry_points.txt +0 -0
- {flow.record-3.15.dev15 → flow_record-3.16.dev1}/flow.record.egg-info/top_level.txt +0 -0
- {flow.record-3.15.dev15 → flow_record-3.16.dev1}/setup.cfg +0 -0
- {flow.record-3.15.dev15 → flow_record-3.16.dev1}/tests/__init__.py +0 -0
- {flow.record-3.15.dev15 → flow_record-3.16.dev1}/tests/_utils.py +0 -0
- {flow.record-3.15.dev15 → flow_record-3.16.dev1}/tests/docs/Makefile +0 -0
- {flow.record-3.15.dev15 → flow_record-3.16.dev1}/tests/docs/conf.py +0 -0
- {flow.record-3.15.dev15 → flow_record-3.16.dev1}/tests/docs/index.rst +0 -0
- {flow.record-3.15.dev15 → flow_record-3.16.dev1}/tests/selector_explain_example.py +0 -0
- {flow.record-3.15.dev15 → flow_record-3.16.dev1}/tests/standalone_test.py +0 -0
- {flow.record-3.15.dev15 → flow_record-3.16.dev1}/tests/test_avro.py +0 -0
- {flow.record-3.15.dev15 → flow_record-3.16.dev1}/tests/test_avro_adapter.py +0 -0
- {flow.record-3.15.dev15 → flow_record-3.16.dev1}/tests/test_compiled_selector.py +0 -0
- {flow.record-3.15.dev15 → flow_record-3.16.dev1}/tests/test_csv_adapter.py +0 -0
- {flow.record-3.15.dev15 → flow_record-3.16.dev1}/tests/test_deprecations.py +0 -0
- {flow.record-3.15.dev15 → flow_record-3.16.dev1}/tests/test_fieldtype_ip.py +0 -0
- {flow.record-3.15.dev15 → flow_record-3.16.dev1}/tests/test_json_packer.py +0 -0
- {flow.record-3.15.dev15 → flow_record-3.16.dev1}/tests/test_json_record_adapter.py +0 -0
- {flow.record-3.15.dev15 → flow_record-3.16.dev1}/tests/test_multi_timestamp.py +0 -0
- {flow.record-3.15.dev15 → flow_record-3.16.dev1}/tests/test_packer.py +0 -0
- {flow.record-3.15.dev15 → flow_record-3.16.dev1}/tests/test_rdump.py +0 -0
- {flow.record-3.15.dev15 → flow_record-3.16.dev1}/tests/test_record.py +0 -0
- {flow.record-3.15.dev15 → flow_record-3.16.dev1}/tests/test_record_adapter.py +0 -0
- {flow.record-3.15.dev15 → flow_record-3.16.dev1}/tests/test_record_descriptor.py +0 -0
- {flow.record-3.15.dev15 → flow_record-3.16.dev1}/tests/test_regression.py +0 -0
- {flow.record-3.15.dev15 → flow_record-3.16.dev1}/tests/test_selector.py +0 -0
- {flow.record-3.15.dev15 → flow_record-3.16.dev1}/tests/test_splunk_adapter.py +0 -0
- {flow.record-3.15.dev15 → flow_record-3.16.dev1}/tests/test_sqlite_duckdb_adapter.py +0 -0
- {flow.record-3.15.dev15 → flow_record-3.16.dev1}/tests/utils_inspect.py +0 -0
- {flow.record-3.15.dev15 → flow_record-3.16.dev1}/tox.ini +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: flow.record
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.16.dev1
|
|
4
4
|
Summary: A library for defining and creating structured data (called records) that can be streamed to disk or piped to other tools that use flow.record
|
|
5
5
|
Author-email: Dissect Team <dissect@fox-it.com>
|
|
6
6
|
License: Affero General Public License v3
|
|
@@ -40,6 +40,7 @@ Requires-Dist: httpx; extra == "splunk"
|
|
|
40
40
|
Provides-Extra: test
|
|
41
41
|
Requires-Dist: flow.record[compression]; extra == "test"
|
|
42
42
|
Requires-Dist: flow.record[avro]; extra == "test"
|
|
43
|
+
Requires-Dist: flow.record[elastic]; extra == "test"
|
|
43
44
|
Requires-Dist: duckdb; (platform_python_implementation != "PyPy" and python_version < "3.12") and extra == "test"
|
|
44
45
|
Requires-Dist: pytz; (platform_python_implementation != "PyPy" and python_version < "3.12") and extra == "test"
|
|
45
46
|
|
|
@@ -2,7 +2,7 @@ import hashlib
|
|
|
2
2
|
import logging
|
|
3
3
|
import queue
|
|
4
4
|
import threading
|
|
5
|
-
from typing import Iterator, Union
|
|
5
|
+
from typing import Iterator, Optional, Union
|
|
6
6
|
|
|
7
7
|
import elasticsearch
|
|
8
8
|
import elasticsearch.helpers
|
|
@@ -22,9 +22,11 @@ Read usage: rdump elastic+[PROTOCOL]://[IP]:[PORT]?index=[INDEX]
|
|
|
22
22
|
[PROTOCOL]: http or https. Defaults to https when "+[PROTOCOL]" is omitted
|
|
23
23
|
|
|
24
24
|
Optional arguments:
|
|
25
|
+
[API_KEY]: base64 encoded api key to authenticate with (default: False)
|
|
25
26
|
[INDEX]: name of the index to use (default: records)
|
|
26
27
|
[VERIFY_CERTS]: verify certs of Elasticsearch instance (default: True)
|
|
27
28
|
[HASH_RECORD]: make record unique by hashing record [slow] (default: False)
|
|
29
|
+
[_META_*]: record metadata fields (default: None)
|
|
28
30
|
"""
|
|
29
31
|
|
|
30
32
|
log = logging.getLogger(__name__)
|
|
@@ -38,6 +40,7 @@ class ElasticWriter(AbstractWriter):
|
|
|
38
40
|
verify_certs: Union[str, bool] = True,
|
|
39
41
|
http_compress: Union[str, bool] = True,
|
|
40
42
|
hash_record: Union[str, bool] = False,
|
|
43
|
+
api_key: Optional[str] = None,
|
|
41
44
|
**kwargs,
|
|
42
45
|
) -> None:
|
|
43
46
|
self.index = index
|
|
@@ -45,7 +48,17 @@ class ElasticWriter(AbstractWriter):
|
|
|
45
48
|
verify_certs = str(verify_certs).lower() in ("1", "true")
|
|
46
49
|
http_compress = str(http_compress).lower() in ("1", "true")
|
|
47
50
|
self.hash_record = str(hash_record).lower() in ("1", "true")
|
|
48
|
-
|
|
51
|
+
|
|
52
|
+
if not uri.lower().startswith(("http://", "https://")):
|
|
53
|
+
uri = "http://" + uri
|
|
54
|
+
|
|
55
|
+
self.es = elasticsearch.Elasticsearch(
|
|
56
|
+
uri,
|
|
57
|
+
verify_certs=verify_certs,
|
|
58
|
+
http_compress=http_compress,
|
|
59
|
+
api_key=api_key,
|
|
60
|
+
)
|
|
61
|
+
|
|
49
62
|
self.json_packer = JsonRecordPacker()
|
|
50
63
|
self.queue: queue.Queue[Union[Record, StopIteration]] = queue.Queue()
|
|
51
64
|
self.event = threading.Event()
|
|
@@ -58,25 +71,34 @@ class ElasticWriter(AbstractWriter):
|
|
|
58
71
|
|
|
59
72
|
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
|
60
73
|
|
|
74
|
+
self.metadata_fields = {}
|
|
75
|
+
for arg_key, arg_val in kwargs.items():
|
|
76
|
+
if arg_key.startswith("_meta_"):
|
|
77
|
+
self.metadata_fields[arg_key[6:]] = arg_val
|
|
78
|
+
|
|
61
79
|
def record_to_document(self, record: Record, index: str) -> dict:
|
|
62
80
|
"""Convert a record to a Elasticsearch compatible document dictionary"""
|
|
63
81
|
rdict = record._asdict()
|
|
64
82
|
|
|
65
|
-
# Store record metadata under `_record_metadata
|
|
83
|
+
# Store record metadata under `_record_metadata`.
|
|
66
84
|
rdict_meta = {
|
|
67
85
|
"descriptor": {
|
|
68
86
|
"name": record._desc.name,
|
|
69
87
|
"hash": record._desc.descriptor_hash,
|
|
70
88
|
},
|
|
71
89
|
}
|
|
90
|
+
|
|
72
91
|
# Move all dunder fields to `_record_metadata` to avoid naming clash with ES.
|
|
73
92
|
dunder_keys = [key for key in rdict if key.startswith("_")]
|
|
74
93
|
for key in dunder_keys:
|
|
75
94
|
rdict_meta[key.lstrip("_")] = rdict.pop(key)
|
|
76
|
-
|
|
95
|
+
|
|
96
|
+
# Remove _generated field from metadata to ensure determinstic documents.
|
|
77
97
|
if self.hash_record:
|
|
78
98
|
rdict_meta.pop("generated", None)
|
|
79
|
-
|
|
99
|
+
|
|
100
|
+
rdict["_record_metadata"] = rdict_meta.copy()
|
|
101
|
+
rdict["_record_metadata"].update(self.metadata_fields)
|
|
80
102
|
|
|
81
103
|
document = {
|
|
82
104
|
"_index": index,
|
|
@@ -106,6 +128,7 @@ class ElasticWriter(AbstractWriter):
|
|
|
106
128
|
):
|
|
107
129
|
if not ok:
|
|
108
130
|
log.error("Failed to insert %r", item)
|
|
131
|
+
|
|
109
132
|
self.event.set()
|
|
110
133
|
|
|
111
134
|
def write(self, record: Record) -> None:
|
|
@@ -129,6 +152,7 @@ class ElasticReader(AbstractReader):
|
|
|
129
152
|
verify_certs: Union[str, bool] = True,
|
|
130
153
|
http_compress: Union[str, bool] = True,
|
|
131
154
|
selector: Union[None, Selector, CompiledSelector] = None,
|
|
155
|
+
api_key: Optional[str] = None,
|
|
132
156
|
**kwargs,
|
|
133
157
|
) -> None:
|
|
134
158
|
self.index = index
|
|
@@ -136,7 +160,16 @@ class ElasticReader(AbstractReader):
|
|
|
136
160
|
self.selector = selector
|
|
137
161
|
verify_certs = str(verify_certs).lower() in ("1", "true")
|
|
138
162
|
http_compress = str(http_compress).lower() in ("1", "true")
|
|
139
|
-
|
|
163
|
+
|
|
164
|
+
if not uri.lower().startswith(("http://", "https://")):
|
|
165
|
+
uri = "http://" + uri
|
|
166
|
+
|
|
167
|
+
self.es = elasticsearch.Elasticsearch(
|
|
168
|
+
uri,
|
|
169
|
+
verify_certs=verify_certs,
|
|
170
|
+
http_compress=http_compress,
|
|
171
|
+
api_key=api_key,
|
|
172
|
+
)
|
|
140
173
|
|
|
141
174
|
if not verify_certs:
|
|
142
175
|
# Disable InsecureRequestWarning of urllib3, caused by the verify_certs flag.
|
|
@@ -5,13 +5,14 @@ import math
|
|
|
5
5
|
import os
|
|
6
6
|
import pathlib
|
|
7
7
|
import re
|
|
8
|
+
import shlex
|
|
8
9
|
import sys
|
|
9
10
|
import warnings
|
|
10
11
|
from binascii import a2b_hex, b2a_hex
|
|
11
12
|
from datetime import datetime as _dt
|
|
12
13
|
from datetime import timezone
|
|
13
14
|
from posixpath import basename, dirname
|
|
14
|
-
from typing import Any, Optional
|
|
15
|
+
from typing import Any, Optional
|
|
15
16
|
from urllib.parse import urlparse
|
|
16
17
|
|
|
17
18
|
try:
|
|
@@ -34,8 +35,8 @@ UTC = timezone.utc
|
|
|
34
35
|
PY_311 = sys.version_info >= (3, 11, 0)
|
|
35
36
|
PY_312 = sys.version_info >= (3, 12, 0)
|
|
36
37
|
|
|
37
|
-
|
|
38
|
-
|
|
38
|
+
TYPE_POSIX = 0
|
|
39
|
+
TYPE_WINDOWS = 1
|
|
39
40
|
|
|
40
41
|
string_type = str
|
|
41
42
|
varint_type = int
|
|
@@ -694,15 +695,15 @@ class path(pathlib.PurePath, FieldType):
|
|
|
694
695
|
return repr(str(self))
|
|
695
696
|
|
|
696
697
|
def _pack(self):
|
|
697
|
-
path_type =
|
|
698
|
+
path_type = TYPE_WINDOWS if isinstance(self, windows_path) else TYPE_POSIX
|
|
698
699
|
return (str(self), path_type)
|
|
699
700
|
|
|
700
701
|
@classmethod
|
|
701
|
-
def _unpack(cls, data:
|
|
702
|
+
def _unpack(cls, data: tuple[str, str]):
|
|
702
703
|
path_, path_type = data
|
|
703
|
-
if path_type ==
|
|
704
|
+
if path_type == TYPE_POSIX:
|
|
704
705
|
return posix_path(path_)
|
|
705
|
-
elif path_type ==
|
|
706
|
+
elif path_type == TYPE_WINDOWS:
|
|
706
707
|
return windows_path(path_)
|
|
707
708
|
else:
|
|
708
709
|
# Catch all: default to posix_path
|
|
@@ -734,3 +735,115 @@ class windows_path(pathlib.PureWindowsPath, path):
|
|
|
734
735
|
quote = '"'
|
|
735
736
|
|
|
736
737
|
return f"{quote}{s}{quote}"
|
|
738
|
+
|
|
739
|
+
|
|
740
|
+
class command(FieldType):
|
|
741
|
+
executable: Optional[path] = None
|
|
742
|
+
args: Optional[list[str]] = None
|
|
743
|
+
|
|
744
|
+
_path_type: type[path] = None
|
|
745
|
+
_posix: bool
|
|
746
|
+
|
|
747
|
+
def __new__(cls, value: str) -> command:
|
|
748
|
+
if cls is not command:
|
|
749
|
+
return super().__new__(cls)
|
|
750
|
+
|
|
751
|
+
if not isinstance(value, str):
|
|
752
|
+
raise ValueError(f"Expected a value of type 'str' not {type(value)}")
|
|
753
|
+
|
|
754
|
+
# pre checking for windows like paths
|
|
755
|
+
# This checks for windows like starts of a path:
|
|
756
|
+
# an '%' for an environment variable
|
|
757
|
+
# r'\\' for a UNC path
|
|
758
|
+
# the strip and check for ":" on the second line is for `<drive_letter>:`
|
|
759
|
+
windows = value.startswith((r"\\", "%")) or value.lstrip("\"'")[1] == ":"
|
|
760
|
+
|
|
761
|
+
if windows:
|
|
762
|
+
cls = windows_command
|
|
763
|
+
else:
|
|
764
|
+
cls = posix_command
|
|
765
|
+
return super().__new__(cls)
|
|
766
|
+
|
|
767
|
+
def __init__(self, value: str | tuple[str, tuple[str]] | None):
|
|
768
|
+
if value is None:
|
|
769
|
+
return
|
|
770
|
+
|
|
771
|
+
if isinstance(value, str):
|
|
772
|
+
self.executable, self.args = self._split(value)
|
|
773
|
+
return
|
|
774
|
+
|
|
775
|
+
executable, self.args = value
|
|
776
|
+
self.executable = self._path_type(executable)
|
|
777
|
+
self.args = list(self.args)
|
|
778
|
+
|
|
779
|
+
def __repr__(self) -> str:
|
|
780
|
+
return f"(executable={self.executable!r}, args={self.args})"
|
|
781
|
+
|
|
782
|
+
def __eq__(self, other: Any) -> bool:
|
|
783
|
+
if isinstance(other, command):
|
|
784
|
+
return self.executable == other.executable and self.args == other.args
|
|
785
|
+
elif isinstance(other, str):
|
|
786
|
+
return self._join() == other
|
|
787
|
+
elif isinstance(other, (tuple, list)):
|
|
788
|
+
return self.executable == other[0] and self.args == list(other[1:])
|
|
789
|
+
|
|
790
|
+
return False
|
|
791
|
+
|
|
792
|
+
def _split(self, value: str) -> tuple[str, list[str]]:
|
|
793
|
+
executable, *args = shlex.split(value, posix=self._posix)
|
|
794
|
+
executable = executable.strip("'\" ")
|
|
795
|
+
|
|
796
|
+
return self._path_type(executable), args
|
|
797
|
+
|
|
798
|
+
def _join(self) -> str:
|
|
799
|
+
return shlex.join([str(self.executable)] + self.args)
|
|
800
|
+
|
|
801
|
+
def _pack(self) -> tuple[tuple[str, list], str]:
|
|
802
|
+
command_type = TYPE_WINDOWS if isinstance(self, windows_command) else TYPE_POSIX
|
|
803
|
+
if self.executable:
|
|
804
|
+
_exec, _ = self.executable._pack()
|
|
805
|
+
return ((_exec, self.args), command_type)
|
|
806
|
+
else:
|
|
807
|
+
return (None, command_type)
|
|
808
|
+
|
|
809
|
+
@classmethod
|
|
810
|
+
def _unpack(cls, data: tuple[tuple[str, tuple] | None, int]) -> command:
|
|
811
|
+
_value, _type = data
|
|
812
|
+
if _type == TYPE_WINDOWS:
|
|
813
|
+
return windows_command(_value)
|
|
814
|
+
|
|
815
|
+
return posix_command(_value)
|
|
816
|
+
|
|
817
|
+
@classmethod
|
|
818
|
+
def from_posix(cls, value: str) -> command:
|
|
819
|
+
return posix_command(value)
|
|
820
|
+
|
|
821
|
+
@classmethod
|
|
822
|
+
def from_windows(cls, value: str) -> command:
|
|
823
|
+
return windows_command(value)
|
|
824
|
+
|
|
825
|
+
|
|
826
|
+
class posix_command(command):
|
|
827
|
+
_posix = True
|
|
828
|
+
_path_type = posix_path
|
|
829
|
+
|
|
830
|
+
|
|
831
|
+
class windows_command(command):
|
|
832
|
+
_posix = False
|
|
833
|
+
_path_type = windows_path
|
|
834
|
+
|
|
835
|
+
def _split(self, value: str) -> tuple[str, list[str]]:
|
|
836
|
+
executable, args = super()._split(value)
|
|
837
|
+
if args:
|
|
838
|
+
args = [" ".join(args)]
|
|
839
|
+
|
|
840
|
+
return executable, args
|
|
841
|
+
|
|
842
|
+
def _join(self) -> str:
|
|
843
|
+
arg = f" {self.args[0]}" if self.args else ""
|
|
844
|
+
executable_str = str(self.executable)
|
|
845
|
+
|
|
846
|
+
if " " in executable_str:
|
|
847
|
+
return f"'{executable_str}'{arg}"
|
|
848
|
+
|
|
849
|
+
return f"{executable_str}{arg}"
|
|
@@ -72,6 +72,11 @@ class JsonRecordPacker:
|
|
|
72
72
|
return base64.b64encode(obj).decode()
|
|
73
73
|
if isinstance(obj, fieldtypes.path):
|
|
74
74
|
return str(obj)
|
|
75
|
+
if isinstance(obj, fieldtypes.command):
|
|
76
|
+
return {
|
|
77
|
+
"executable": obj.executable,
|
|
78
|
+
"args": obj.args,
|
|
79
|
+
}
|
|
75
80
|
|
|
76
81
|
raise Exception("Unpackable type " + str(type(obj)))
|
|
77
82
|
|
|
@@ -12,5 +12,5 @@ __version__: str
|
|
|
12
12
|
__version_tuple__: VERSION_TUPLE
|
|
13
13
|
version_tuple: VERSION_TUPLE
|
|
14
14
|
|
|
15
|
-
__version__ = version = '3.
|
|
16
|
-
__version_tuple__ = version_tuple = (3,
|
|
15
|
+
__version__ = version = '3.16.dev1'
|
|
16
|
+
__version_tuple__ = version_tuple = (3, 16, 'dev1')
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: flow.record
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.16.dev1
|
|
4
4
|
Summary: A library for defining and creating structured data (called records) that can be streamed to disk or piped to other tools that use flow.record
|
|
5
5
|
Author-email: Dissect Team <dissect@fox-it.com>
|
|
6
6
|
License: Affero General Public License v3
|
|
@@ -40,6 +40,7 @@ Requires-Dist: httpx; extra == "splunk"
|
|
|
40
40
|
Provides-Extra: test
|
|
41
41
|
Requires-Dist: flow.record[compression]; extra == "test"
|
|
42
42
|
Requires-Dist: flow.record[avro]; extra == "test"
|
|
43
|
+
Requires-Dist: flow.record[elastic]; extra == "test"
|
|
43
44
|
Requires-Dist: duckdb; (platform_python_implementation != "PyPy" and python_version < "3.12") and extra == "test"
|
|
44
45
|
Requires-Dist: pytz; (platform_python_implementation != "PyPy" and python_version < "3.12") and extra == "test"
|
|
45
46
|
|
|
@@ -59,6 +59,7 @@ splunk = [
|
|
|
59
59
|
test = [
|
|
60
60
|
"flow.record[compression]",
|
|
61
61
|
"flow.record[avro]",
|
|
62
|
+
"flow.record[elastic]",
|
|
62
63
|
"duckdb; platform_python_implementation != 'PyPy' and python_version < '3.12'", # duckdb
|
|
63
64
|
"pytz; platform_python_implementation != 'PyPy' and python_version < '3.12'", # duckdb
|
|
64
65
|
]
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from flow.record import RecordDescriptor
|
|
6
|
+
from flow.record.adapter.elastic import ElasticWriter
|
|
7
|
+
|
|
8
|
+
MyRecord = RecordDescriptor(
|
|
9
|
+
"my/record",
|
|
10
|
+
[
|
|
11
|
+
("string", "field_one"),
|
|
12
|
+
("string", "field_two"),
|
|
13
|
+
],
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@pytest.mark.parametrize(
|
|
18
|
+
"record",
|
|
19
|
+
[
|
|
20
|
+
MyRecord("first", "record"),
|
|
21
|
+
MyRecord("second", "record"),
|
|
22
|
+
],
|
|
23
|
+
)
|
|
24
|
+
def test_elastic_writer_metadata(record):
|
|
25
|
+
options = {
|
|
26
|
+
"_meta_foo": "some value",
|
|
27
|
+
"_meta_bar": "another value",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
with ElasticWriter(uri="elasticsearch:9200", **options) as writer:
|
|
31
|
+
assert writer.metadata_fields == {"foo": "some value", "bar": "another value"}
|
|
32
|
+
|
|
33
|
+
assert writer.record_to_document(record, "some-index") == {
|
|
34
|
+
"_index": "some-index",
|
|
35
|
+
"_source": json.dumps(
|
|
36
|
+
{
|
|
37
|
+
"field_one": record.field_one,
|
|
38
|
+
"field_two": record.field_two,
|
|
39
|
+
"_record_metadata": {
|
|
40
|
+
"descriptor": {
|
|
41
|
+
"name": "my/record",
|
|
42
|
+
"hash": record._desc.descriptor_hash,
|
|
43
|
+
},
|
|
44
|
+
"source": None,
|
|
45
|
+
"classification": None,
|
|
46
|
+
"generated": record._generated.isoformat(),
|
|
47
|
+
"version": 1,
|
|
48
|
+
"foo": "some value",
|
|
49
|
+
"bar": "another value",
|
|
50
|
+
},
|
|
51
|
+
}
|
|
52
|
+
),
|
|
53
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# coding: utf-8
|
|
2
|
+
from __future__ import annotations
|
|
2
3
|
|
|
3
4
|
import hashlib
|
|
4
5
|
import os
|
|
@@ -12,14 +13,22 @@ import pytest
|
|
|
12
13
|
import flow.record.fieldtypes
|
|
13
14
|
from flow.record import RecordDescriptor, RecordReader, RecordWriter
|
|
14
15
|
from flow.record.fieldtypes import (
|
|
15
|
-
PATH_POSIX,
|
|
16
|
-
PATH_WINDOWS,
|
|
17
16
|
PY_312,
|
|
17
|
+
TYPE_POSIX,
|
|
18
|
+
TYPE_WINDOWS,
|
|
18
19
|
_is_posixlike_path,
|
|
19
20
|
_is_windowslike_path,
|
|
21
|
+
command,
|
|
20
22
|
)
|
|
21
23
|
from flow.record.fieldtypes import datetime as dt
|
|
22
|
-
from flow.record.fieldtypes import
|
|
24
|
+
from flow.record.fieldtypes import (
|
|
25
|
+
fieldtype_for_value,
|
|
26
|
+
net,
|
|
27
|
+
posix_command,
|
|
28
|
+
uri,
|
|
29
|
+
windows_command,
|
|
30
|
+
windows_path,
|
|
31
|
+
)
|
|
23
32
|
|
|
24
33
|
UTC = timezone.utc
|
|
25
34
|
|
|
@@ -639,16 +648,16 @@ def test_path():
|
|
|
639
648
|
assert isinstance(test_path, flow.record.fieldtypes.windows_path)
|
|
640
649
|
|
|
641
650
|
test_path = flow.record.fieldtypes.path.from_posix(posix_path_str)
|
|
642
|
-
assert test_path._pack() == (posix_path_str,
|
|
651
|
+
assert test_path._pack() == (posix_path_str, TYPE_POSIX)
|
|
643
652
|
|
|
644
|
-
test_path = flow.record.fieldtypes.path._unpack((posix_path_str,
|
|
653
|
+
test_path = flow.record.fieldtypes.path._unpack((posix_path_str, TYPE_POSIX))
|
|
645
654
|
assert str(test_path) == posix_path_str
|
|
646
655
|
assert isinstance(test_path, flow.record.fieldtypes.posix_path)
|
|
647
656
|
|
|
648
657
|
test_path = flow.record.fieldtypes.path.from_windows(windows_path_str)
|
|
649
|
-
assert test_path._pack() == (windows_path_str,
|
|
658
|
+
assert test_path._pack() == (windows_path_str, TYPE_WINDOWS)
|
|
650
659
|
|
|
651
|
-
test_path = flow.record.fieldtypes.path._unpack((windows_path_str,
|
|
660
|
+
test_path = flow.record.fieldtypes.path._unpack((windows_path_str, TYPE_WINDOWS))
|
|
652
661
|
assert str(test_path) == windows_path_str
|
|
653
662
|
assert isinstance(test_path, flow.record.fieldtypes.windows_path)
|
|
654
663
|
|
|
@@ -998,5 +1007,130 @@ def test_datetime_comparisions():
|
|
|
998
1007
|
assert dt("2023-01-02") != datetime(2023, 3, 4, tzinfo=UTC)
|
|
999
1008
|
|
|
1000
1009
|
|
|
1010
|
+
def test_command_record() -> None:
|
|
1011
|
+
TestRecord = RecordDescriptor(
|
|
1012
|
+
"test/command",
|
|
1013
|
+
[
|
|
1014
|
+
("command", "commando"),
|
|
1015
|
+
],
|
|
1016
|
+
)
|
|
1017
|
+
|
|
1018
|
+
record = TestRecord(commando="help.exe -h")
|
|
1019
|
+
assert isinstance(record.commando, posix_command)
|
|
1020
|
+
assert record.commando.executable == "help.exe"
|
|
1021
|
+
assert record.commando.args == ["-h"]
|
|
1022
|
+
|
|
1023
|
+
record = TestRecord(commando="something.so -h -q -something")
|
|
1024
|
+
assert isinstance(record.commando, posix_command)
|
|
1025
|
+
assert record.commando.executable == "something.so"
|
|
1026
|
+
assert record.commando.args == ["-h", "-q", "-something"]
|
|
1027
|
+
|
|
1028
|
+
|
|
1029
|
+
def test_command_integration(tmp_path: pathlib.Path) -> None:
|
|
1030
|
+
TestRecord = RecordDescriptor(
|
|
1031
|
+
"test/command",
|
|
1032
|
+
[
|
|
1033
|
+
("command", "commando"),
|
|
1034
|
+
],
|
|
1035
|
+
)
|
|
1036
|
+
|
|
1037
|
+
with RecordWriter(tmp_path / "command_record") as writer:
|
|
1038
|
+
record = TestRecord(commando=r"\\.\\?\some_command.exe -h,help /d quiet")
|
|
1039
|
+
writer.write(record)
|
|
1040
|
+
assert record.commando.executable == r"\\.\\?\some_command.exe"
|
|
1041
|
+
assert record.commando.args == [r"-h,help /d quiet"]
|
|
1042
|
+
|
|
1043
|
+
with RecordReader(tmp_path / "command_record") as reader:
|
|
1044
|
+
for record in reader:
|
|
1045
|
+
assert record.commando.executable == r"\\.\\?\some_command.exe"
|
|
1046
|
+
assert record.commando.args == [r"-h,help /d quiet"]
|
|
1047
|
+
|
|
1048
|
+
|
|
1049
|
+
def test_command_integration_none(tmp_path: pathlib.Path) -> None:
|
|
1050
|
+
TestRecord = RecordDescriptor(
|
|
1051
|
+
"test/command",
|
|
1052
|
+
[
|
|
1053
|
+
("command", "commando"),
|
|
1054
|
+
],
|
|
1055
|
+
)
|
|
1056
|
+
|
|
1057
|
+
with RecordWriter(tmp_path / "command_record") as writer:
|
|
1058
|
+
record = TestRecord(commando=command.from_posix(None))
|
|
1059
|
+
writer.write(record)
|
|
1060
|
+
with RecordReader(tmp_path / "command_record") as reader:
|
|
1061
|
+
for record in reader:
|
|
1062
|
+
assert record.commando.executable is None
|
|
1063
|
+
assert record.commando.args is None
|
|
1064
|
+
|
|
1065
|
+
|
|
1066
|
+
@pytest.mark.parametrize(
|
|
1067
|
+
"command_string, expected_executable, expected_argument",
|
|
1068
|
+
[
|
|
1069
|
+
# Test relative windows paths
|
|
1070
|
+
("windows.exe something,or,somethingelse", "windows.exe", ["something,or,somethingelse"]),
|
|
1071
|
+
# Test weird command strings for windows
|
|
1072
|
+
("windows.dll something,or,somethingelse", "windows.dll", ["something,or,somethingelse"]),
|
|
1073
|
+
# Test environment variables
|
|
1074
|
+
(r"%WINDIR%\\windows.dll something,or,somethingelse", r"%WINDIR%\\windows.dll", ["something,or,somethingelse"]),
|
|
1075
|
+
# Test a quoted path
|
|
1076
|
+
(r"'c:\path to some exe' /d /a", r"c:\path to some exe", [r"/d /a"]),
|
|
1077
|
+
# Test a unquoted path
|
|
1078
|
+
(r"'c:\Program Files\hello.exe'", r"c:\Program Files\hello.exe", []),
|
|
1079
|
+
# Test an unquoted path with a path as argument
|
|
1080
|
+
(r"'c:\Program Files\hello.exe' c:\startmepls.exe", r"c:\Program Files\hello.exe", [r"c:\startmepls.exe"]),
|
|
1081
|
+
(None, None, None),
|
|
1082
|
+
],
|
|
1083
|
+
)
|
|
1084
|
+
def test_command_windows(command_string: str, expected_executable: str, expected_argument: list[str]) -> None:
|
|
1085
|
+
cmd = windows_command(command_string)
|
|
1086
|
+
|
|
1087
|
+
assert cmd.executable == expected_executable
|
|
1088
|
+
assert cmd.args == expected_argument
|
|
1089
|
+
|
|
1090
|
+
|
|
1091
|
+
@pytest.mark.parametrize(
|
|
1092
|
+
"command_string, expected_executable, expected_argument",
|
|
1093
|
+
[
|
|
1094
|
+
# Test relative posix command
|
|
1095
|
+
("some_file.so -h asdsad -f asdsadas", "some_file.so", ["-h", "asdsad", "-f", "asdsadas"]),
|
|
1096
|
+
# Test command with spaces
|
|
1097
|
+
(r"/bin/hello\ world -h -word", r"/bin/hello world", ["-h", "-word"]),
|
|
1098
|
+
],
|
|
1099
|
+
)
|
|
1100
|
+
def test_command_posix(command_string: str, expected_executable: str, expected_argument: list[str]) -> None:
|
|
1101
|
+
cmd = posix_command(command_string)
|
|
1102
|
+
|
|
1103
|
+
assert cmd.executable == expected_executable
|
|
1104
|
+
assert cmd.args == expected_argument
|
|
1105
|
+
|
|
1106
|
+
|
|
1107
|
+
def test_command_equal() -> None:
|
|
1108
|
+
assert command("hello.so -h") == command("hello.so -h")
|
|
1109
|
+
assert command("hello.so -h") != command("hello.so")
|
|
1110
|
+
|
|
1111
|
+
# Test different types with the comparitor
|
|
1112
|
+
assert command("hello.so -h") == ["hello.so", "-h"]
|
|
1113
|
+
assert command("hello.so -h") == ("hello.so", "-h")
|
|
1114
|
+
assert command("hello.so -h") == "hello.so -h"
|
|
1115
|
+
assert command("c:\\hello.dll -h -b") == "c:\\hello.dll -h -b"
|
|
1116
|
+
|
|
1117
|
+
# Compare paths that contain spaces
|
|
1118
|
+
assert command("'/home/some folder/file' -h") == "'/home/some folder/file' -h"
|
|
1119
|
+
assert command("'c:\\Program files\\some.dll' -h -q") == "'c:\\Program files\\some.dll' -h -q"
|
|
1120
|
+
assert command("'c:\\program files\\some.dll' -h -q") == ["c:\\program files\\some.dll", "-h -q"]
|
|
1121
|
+
assert command("'c:\\Program files\\some.dll' -h -q") == ("c:\\Program files\\some.dll", "-h -q")
|
|
1122
|
+
|
|
1123
|
+
# Test failure conditions
|
|
1124
|
+
assert command("hello.so -h") != 1
|
|
1125
|
+
assert command("hello.so") != "hello.so -h"
|
|
1126
|
+
assert command("hello.so") != ["hello.so", ""]
|
|
1127
|
+
assert command("hello.so") != ("hello.so", "")
|
|
1128
|
+
|
|
1129
|
+
|
|
1130
|
+
def test_command_failed() -> None:
|
|
1131
|
+
with pytest.raises(ValueError):
|
|
1132
|
+
command(b"failed")
|
|
1133
|
+
|
|
1134
|
+
|
|
1001
1135
|
if __name__ == "__main__":
|
|
1002
1136
|
__import__("standalone_test").main(globals())
|
|
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
|