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.
Files changed (82) hide show
  1. {flow.record-3.15.dev15/flow.record.egg-info → flow_record-3.16.dev1}/PKG-INFO +2 -1
  2. {flow.record-3.15.dev15 → flow_record-3.16.dev1}/flow/record/adapter/elastic.py +39 -6
  3. {flow.record-3.15.dev15 → flow_record-3.16.dev1}/flow/record/fieldtypes/__init__.py +120 -7
  4. {flow.record-3.15.dev15 → flow_record-3.16.dev1}/flow/record/jsonpacker.py +5 -0
  5. {flow.record-3.15.dev15 → flow_record-3.16.dev1}/flow/record/version.py +2 -2
  6. {flow.record-3.15.dev15 → flow_record-3.16.dev1}/flow/record/whitelist.py +1 -0
  7. {flow.record-3.15.dev15 → flow_record-3.16.dev1/flow.record.egg-info}/PKG-INFO +2 -1
  8. {flow.record-3.15.dev15 → flow_record-3.16.dev1}/flow.record.egg-info/SOURCES.txt +1 -0
  9. {flow.record-3.15.dev15 → flow_record-3.16.dev1}/flow.record.egg-info/requires.txt +1 -0
  10. {flow.record-3.15.dev15 → flow_record-3.16.dev1}/pyproject.toml +1 -0
  11. flow_record-3.16.dev1/tests/test_elastic_adapter.py +53 -0
  12. {flow.record-3.15.dev15 → flow_record-3.16.dev1}/tests/test_fieldtypes.py +141 -7
  13. {flow.record-3.15.dev15 → flow_record-3.16.dev1}/COPYRIGHT +0 -0
  14. {flow.record-3.15.dev15 → flow_record-3.16.dev1}/LICENSE +0 -0
  15. {flow.record-3.15.dev15 → flow_record-3.16.dev1}/MANIFEST.in +0 -0
  16. {flow.record-3.15.dev15 → flow_record-3.16.dev1}/README.md +0 -0
  17. {flow.record-3.15.dev15 → flow_record-3.16.dev1}/examples/filesystem.py +0 -0
  18. {flow.record-3.15.dev15 → flow_record-3.16.dev1}/examples/passivedns.py +0 -0
  19. {flow.record-3.15.dev15 → flow_record-3.16.dev1}/examples/records.json +0 -0
  20. {flow.record-3.15.dev15 → flow_record-3.16.dev1}/examples/tcpconn.py +0 -0
  21. {flow.record-3.15.dev15 → flow_record-3.16.dev1}/flow/record/__init__.py +0 -0
  22. {flow.record-3.15.dev15 → flow_record-3.16.dev1}/flow/record/adapter/__init__.py +0 -0
  23. {flow.record-3.15.dev15 → flow_record-3.16.dev1}/flow/record/adapter/archive.py +0 -0
  24. {flow.record-3.15.dev15 → flow_record-3.16.dev1}/flow/record/adapter/avro.py +0 -0
  25. {flow.record-3.15.dev15 → flow_record-3.16.dev1}/flow/record/adapter/broker.py +0 -0
  26. {flow.record-3.15.dev15 → flow_record-3.16.dev1}/flow/record/adapter/csvfile.py +0 -0
  27. {flow.record-3.15.dev15 → flow_record-3.16.dev1}/flow/record/adapter/duckdb.py +0 -0
  28. {flow.record-3.15.dev15 → flow_record-3.16.dev1}/flow/record/adapter/jsonfile.py +0 -0
  29. {flow.record-3.15.dev15 → flow_record-3.16.dev1}/flow/record/adapter/line.py +0 -0
  30. {flow.record-3.15.dev15 → flow_record-3.16.dev1}/flow/record/adapter/mongo.py +0 -0
  31. {flow.record-3.15.dev15 → flow_record-3.16.dev1}/flow/record/adapter/split.py +0 -0
  32. {flow.record-3.15.dev15 → flow_record-3.16.dev1}/flow/record/adapter/splunk.py +0 -0
  33. {flow.record-3.15.dev15 → flow_record-3.16.dev1}/flow/record/adapter/sqlite.py +0 -0
  34. {flow.record-3.15.dev15 → flow_record-3.16.dev1}/flow/record/adapter/stream.py +0 -0
  35. {flow.record-3.15.dev15 → flow_record-3.16.dev1}/flow/record/adapter/text.py +0 -0
  36. {flow.record-3.15.dev15 → flow_record-3.16.dev1}/flow/record/adapter/xlsx.py +0 -0
  37. {flow.record-3.15.dev15 → flow_record-3.16.dev1}/flow/record/base.py +0 -0
  38. {flow.record-3.15.dev15 → flow_record-3.16.dev1}/flow/record/exceptions.py +0 -0
  39. {flow.record-3.15.dev15 → flow_record-3.16.dev1}/flow/record/fieldtypes/credential.py +0 -0
  40. {flow.record-3.15.dev15 → flow_record-3.16.dev1}/flow/record/fieldtypes/net/__init__.py +0 -0
  41. {flow.record-3.15.dev15 → flow_record-3.16.dev1}/flow/record/fieldtypes/net/ip.py +0 -0
  42. {flow.record-3.15.dev15 → flow_record-3.16.dev1}/flow/record/fieldtypes/net/ipv4.py +0 -0
  43. {flow.record-3.15.dev15 → flow_record-3.16.dev1}/flow/record/fieldtypes/net/tcp.py +0 -0
  44. {flow.record-3.15.dev15 → flow_record-3.16.dev1}/flow/record/fieldtypes/net/udp.py +0 -0
  45. {flow.record-3.15.dev15 → flow_record-3.16.dev1}/flow/record/packer.py +0 -0
  46. {flow.record-3.15.dev15 → flow_record-3.16.dev1}/flow/record/selector.py +0 -0
  47. {flow.record-3.15.dev15 → flow_record-3.16.dev1}/flow/record/stream.py +0 -0
  48. {flow.record-3.15.dev15 → flow_record-3.16.dev1}/flow/record/tools/__init__.py +0 -0
  49. {flow.record-3.15.dev15 → flow_record-3.16.dev1}/flow/record/tools/geoip.py +0 -0
  50. {flow.record-3.15.dev15 → flow_record-3.16.dev1}/flow/record/tools/rdump.py +0 -0
  51. {flow.record-3.15.dev15 → flow_record-3.16.dev1}/flow/record/utils.py +0 -0
  52. {flow.record-3.15.dev15 → flow_record-3.16.dev1}/flow.record.egg-info/dependency_links.txt +0 -0
  53. {flow.record-3.15.dev15 → flow_record-3.16.dev1}/flow.record.egg-info/entry_points.txt +0 -0
  54. {flow.record-3.15.dev15 → flow_record-3.16.dev1}/flow.record.egg-info/top_level.txt +0 -0
  55. {flow.record-3.15.dev15 → flow_record-3.16.dev1}/setup.cfg +0 -0
  56. {flow.record-3.15.dev15 → flow_record-3.16.dev1}/tests/__init__.py +0 -0
  57. {flow.record-3.15.dev15 → flow_record-3.16.dev1}/tests/_utils.py +0 -0
  58. {flow.record-3.15.dev15 → flow_record-3.16.dev1}/tests/docs/Makefile +0 -0
  59. {flow.record-3.15.dev15 → flow_record-3.16.dev1}/tests/docs/conf.py +0 -0
  60. {flow.record-3.15.dev15 → flow_record-3.16.dev1}/tests/docs/index.rst +0 -0
  61. {flow.record-3.15.dev15 → flow_record-3.16.dev1}/tests/selector_explain_example.py +0 -0
  62. {flow.record-3.15.dev15 → flow_record-3.16.dev1}/tests/standalone_test.py +0 -0
  63. {flow.record-3.15.dev15 → flow_record-3.16.dev1}/tests/test_avro.py +0 -0
  64. {flow.record-3.15.dev15 → flow_record-3.16.dev1}/tests/test_avro_adapter.py +0 -0
  65. {flow.record-3.15.dev15 → flow_record-3.16.dev1}/tests/test_compiled_selector.py +0 -0
  66. {flow.record-3.15.dev15 → flow_record-3.16.dev1}/tests/test_csv_adapter.py +0 -0
  67. {flow.record-3.15.dev15 → flow_record-3.16.dev1}/tests/test_deprecations.py +0 -0
  68. {flow.record-3.15.dev15 → flow_record-3.16.dev1}/tests/test_fieldtype_ip.py +0 -0
  69. {flow.record-3.15.dev15 → flow_record-3.16.dev1}/tests/test_json_packer.py +0 -0
  70. {flow.record-3.15.dev15 → flow_record-3.16.dev1}/tests/test_json_record_adapter.py +0 -0
  71. {flow.record-3.15.dev15 → flow_record-3.16.dev1}/tests/test_multi_timestamp.py +0 -0
  72. {flow.record-3.15.dev15 → flow_record-3.16.dev1}/tests/test_packer.py +0 -0
  73. {flow.record-3.15.dev15 → flow_record-3.16.dev1}/tests/test_rdump.py +0 -0
  74. {flow.record-3.15.dev15 → flow_record-3.16.dev1}/tests/test_record.py +0 -0
  75. {flow.record-3.15.dev15 → flow_record-3.16.dev1}/tests/test_record_adapter.py +0 -0
  76. {flow.record-3.15.dev15 → flow_record-3.16.dev1}/tests/test_record_descriptor.py +0 -0
  77. {flow.record-3.15.dev15 → flow_record-3.16.dev1}/tests/test_regression.py +0 -0
  78. {flow.record-3.15.dev15 → flow_record-3.16.dev1}/tests/test_selector.py +0 -0
  79. {flow.record-3.15.dev15 → flow_record-3.16.dev1}/tests/test_splunk_adapter.py +0 -0
  80. {flow.record-3.15.dev15 → flow_record-3.16.dev1}/tests/test_sqlite_duckdb_adapter.py +0 -0
  81. {flow.record-3.15.dev15 → flow_record-3.16.dev1}/tests/utils_inspect.py +0 -0
  82. {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.15.dev15
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
- self.es = elasticsearch.Elasticsearch(uri, verify_certs=verify_certs, http_compress=http_compress)
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
- # remove _generated field from metadata to ensure determinstic documents
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
- rdict["_record_metadata"] = rdict_meta
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
- self.es = elasticsearch.Elasticsearch(uri, verify_certs=verify_certs, http_compress=http_compress)
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, Tuple
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
- PATH_POSIX = 0
38
- PATH_WINDOWS = 1
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 = PATH_WINDOWS if isinstance(self, windows_path) else PATH_POSIX
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: Tuple[str, str]):
702
+ def _unpack(cls, data: tuple[str, str]):
702
703
  path_, path_type = data
703
- if path_type == PATH_POSIX:
704
+ if path_type == TYPE_POSIX:
704
705
  return posix_path(path_)
705
- elif path_type == PATH_WINDOWS:
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.15.dev15'
16
- __version_tuple__ = version_tuple = (3, 15, 'dev15')
15
+ __version__ = version = '3.16.dev1'
16
+ __version_tuple__ = version_tuple = (3, 16, 'dev1')
@@ -1,5 +1,6 @@
1
1
  WHITELIST = [
2
2
  "boolean",
3
+ "command",
3
4
  "dynamic",
4
5
  "datetime",
5
6
  "filesize",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: flow.record
3
- Version: 3.15.dev15
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 @@ tests/test_avro_adapter.py
59
59
  tests/test_compiled_selector.py
60
60
  tests/test_csv_adapter.py
61
61
  tests/test_deprecations.py
62
+ tests/test_elastic_adapter.py
62
63
  tests/test_fieldtype_ip.py
63
64
  tests/test_fieldtypes.py
64
65
  tests/test_json_packer.py
@@ -29,6 +29,7 @@ httpx
29
29
  [test]
30
30
  flow.record[compression]
31
31
  flow.record[avro]
32
+ flow.record[elastic]
32
33
 
33
34
  [test:platform_python_implementation != "PyPy" and python_version < "3.12"]
34
35
  duckdb
@@ -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 fieldtype_for_value, net, uri, windows_path
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, PATH_POSIX)
651
+ assert test_path._pack() == (posix_path_str, TYPE_POSIX)
643
652
 
644
- test_path = flow.record.fieldtypes.path._unpack((posix_path_str, PATH_POSIX))
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, PATH_WINDOWS)
658
+ assert test_path._pack() == (windows_path_str, TYPE_WINDOWS)
650
659
 
651
- test_path = flow.record.fieldtypes.path._unpack((windows_path_str, PATH_WINDOWS))
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())