stormlibpp 0.1.2__tar.gz → 0.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: stormlibpp
3
- Version: 0.1.2
3
+ Version: 0.3.0
4
4
  Summary: The stormlibpp Python package
5
5
  Author: John Gorman
6
6
  License: MIT License
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "stormlibpp"
7
- version = "0.1.2"
7
+ version = "0.3.0"
8
8
  description = "The stormlibpp Python package"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -0,0 +1,38 @@
1
+ """The stormlibpp Python Package.
2
+
3
+ StormLib++ provides a number of Python objects to make life as a Synapse/Storm
4
+ developer easier.
5
+
6
+ StormLib++ provides several useful utility functions in ``utils``.
7
+
8
+ StormLib++ errors can be found in ``errors``. Each componenent of StormLib++
9
+ uses these errors their own way. Information our how they're used can be found
10
+ in those components.
11
+
12
+ ``StormPkg`` is a helper class that represents a Storm package in memory. It
13
+ can read a package prototype from a default (or configured) location, convert
14
+ it to a package definition, optionally check the Storm code's syntax (enabled
15
+ by default), and store a Python dict containing the Storm package's definition.
16
+
17
+ ``StormPkg`` also provides some helpful properties to describe the Storm
18
+ package's configuration (`cmds`, `mods`, `pkg_guid`, `pkg_name`, `pkg_ver`, `storm`).
19
+
20
+ There are also several submodules not directly imported by ``stormlibpp`` at
21
+ init. However, some classes/functions from these submodules are. The submodules
22
+ are briefly described below:
23
+
24
+ - ``telepath`` has methods and classes for working with Telepath endpoints.
25
+ It currently contains mostly classes for describing standard Telepath API
26
+ return values.
27
+
28
+ - ``stormpkg`` defines the ``StormPkg`` class.
29
+
30
+ """
31
+
32
+ __version__ = "0.3.0"
33
+
34
+ from . import errors
35
+ from . import utils
36
+ from .node import StormNode
37
+ from .stormpkg import StormPkg
38
+ from .telepath import (genDefaultTelepathRetn, TelepathRetn)
@@ -0,0 +1,197 @@
1
+ """Work with storm:node objects in Python."""
2
+
3
+
4
+ from types import SimpleNamespace
5
+ from typing import Any, Self, TypedDict
6
+
7
+
8
+ NodeForm = str
9
+ """A valid syn:form value."""
10
+
11
+
12
+ class NodeVals(TypedDict):
13
+ """The items a Node can have in the second part of packed NodeTuple."""
14
+
15
+ iden: str
16
+ nodedata: dict[str, Any]
17
+ props: dict[str, Any]
18
+ tags: dict[str, Any]
19
+ tagprops: dict[str, Any]
20
+
21
+
22
+ NodeDef = tuple[NodeForm, str]
23
+ """A nodes definition, the node's form and primary value."""
24
+
25
+ NodeTuple = tuple[NodeDef, NodeVals]
26
+ """A packed node, this is what get's sent from Storm to a Telepath endpoint.
27
+
28
+ Or what get's returned from a Cortex's Python APIs.
29
+ """
30
+
31
+
32
+ class ItemContainer:
33
+ """Hold key/value items in a SimpleNamespace.
34
+
35
+ Keys must be strings.
36
+
37
+ Supports ``obj['item']`` (and ``obj['item'] = 'val'``) notation.
38
+
39
+ When "getting" an item with ``__getitem__``, that doesn't exist in the
40
+ container, ``None`` is returned instead of raising an exception.
41
+
42
+ Calling ``dir`` on this object will only return names for items this object
43
+ "holds" instead of all parts of this object.
44
+
45
+ Supports checking if an item is "held" via the ``in`` operator.
46
+
47
+ Supports iteration of items in the SimpleNamespace. Each iteration returns
48
+ a tuple of the item name followed by the item value.
49
+
50
+ Implements only magic methods, and all of them operate on the underlying
51
+ SimpleNamespace. To convert this object to a ``dict`` of the items it holds,
52
+ call ``vars`` on an instance.
53
+ """
54
+
55
+ def __init__(self, /, **kwargs) -> None:
56
+ self.__ns = SimpleNamespace(**kwargs)
57
+
58
+ def __contains__(self, __name: str) -> bool:
59
+ return __name in dir(self)
60
+
61
+ @property
62
+ def __dict__(self):
63
+ return self.__ns.__dict__
64
+
65
+ def __dir__(self) -> list[str]:
66
+ return [val for val in dir(self.__ns) if not val.startswith("_")]
67
+
68
+ def __iter__(self) -> Any:
69
+ for item in dir(self):
70
+ yield (item, self[item])
71
+
72
+ def __getitem__(self, __name: str) -> Any:
73
+ return getattr(self.__ns, __name, None)
74
+
75
+ def __repr__(self) -> str:
76
+ return repr(self.__ns).replace("namespace", self.__class__.__name__)
77
+
78
+ def __setitem__(self, __name: str, __value: str) -> None:
79
+ setattr(self.__ns, __name, __value)
80
+
81
+
82
+ class Props(ItemContainer):
83
+ """Properties of a StormNode."""
84
+
85
+
86
+ class Tags(ItemContainer):
87
+ """Tags of a StormNode."""
88
+
89
+
90
+
91
+ class StormNode:
92
+ """A storm:node Storm object that is in a Python runtime.
93
+
94
+ It supports "unpacking" the node into this object, edits to the object, and
95
+ repacking of the node to be sent back to Storm.
96
+
97
+ This node can be created purely from Python code rather than a packed
98
+ ``storm:node`` object. Use the ``__init__`` directly instead of ``unpack``.
99
+ This allows the creation of a new node in Python that can be passed back to
100
+ Storm.
101
+
102
+ The intent is to represent an object that can be read from and written back
103
+ to a Cortex. It isn't meant to provide the same API that a ``storm:node``
104
+ has in Storm.
105
+ """
106
+
107
+ def __init__(
108
+ self,
109
+ form: NodeForm,
110
+ value: Any,
111
+ /,
112
+ iden: str = "",
113
+ tags: dict = {},
114
+ props: dict = {},
115
+ nodedata: dict = {},
116
+ tagprops: dict = {},
117
+ **kwargs,
118
+ ) -> None:
119
+
120
+ self._form = form
121
+ self._value = value
122
+
123
+ self._tags = Props(**tags)
124
+ self._props = Tags(**props)
125
+
126
+ self.iden = iden
127
+ self.nodedata = nodedata
128
+ self.tagprops = tagprops
129
+
130
+ # Not really sure if this is needed, but saving it for now.
131
+ self._xtras = kwargs
132
+
133
+ @property
134
+ def form(self) -> str:
135
+ """The form of this Node."""
136
+
137
+ return self._form
138
+
139
+ @property
140
+ def value(self) -> str:
141
+ """The value of this Node."""
142
+
143
+ return self._value
144
+
145
+ @property
146
+ def tags(self):
147
+ """The tags of this Node."""
148
+
149
+ return self._tags
150
+
151
+ @property
152
+ def props(self):
153
+ """The properties of this Node."""
154
+
155
+ return self._props
156
+
157
+ @property
158
+ def nodevals(self):
159
+ """The NodeVals dict this Node would have in a NodeTuple."""
160
+
161
+ return NodeVals(
162
+ iden=self.iden,
163
+ tagprops=self.tagprops,
164
+ nodedata=self.nodedata,
165
+ props=vars(self.props),
166
+ tags=vars(self.tags),
167
+ )
168
+
169
+ @classmethod
170
+ def unpack(cls, packedtup: NodeTuple) -> Self:
171
+ form, name = packedtup[0]
172
+ vals = NodeVals(**packedtup[1])
173
+ return cls(
174
+ form,
175
+ name,
176
+ **vals
177
+ )
178
+
179
+ def pack(self) -> NodeTuple:
180
+ return ((self.form, self.value), self.nodevals)
181
+
182
+ def addTag(self, tag, timestamps: tuple = (None, None)) -> None:
183
+ if tag in self._tags and timestamps != (None, None) and self._tags[tag] != timestamps:
184
+ self._tags[tag] = timestamps
185
+ elif tag not in self._tags:
186
+ self._tags[tag] = timestamps
187
+
188
+ def addTags(self, tags, timestamps: tuple = (None, None)) -> None:
189
+ for tag in tags:
190
+ self.addTag(tag, timestamps=timestamps)
191
+
192
+ def addProp(self, propname: str, propval: Any) -> None:
193
+ self._props[propname] = propval
194
+
195
+ def addProps(self, props: dict[str, Any]) -> None:
196
+ for propname, propval in props.items():
197
+ self._props[propname] = propval
@@ -5,8 +5,11 @@ import json
5
5
  import os
6
6
  import sys
7
7
 
8
+ import synapse.common as s_common
9
+ import synapse.exc as s_exc
10
+ import synapse.tools.genpkg as s_genpkg
11
+
8
12
  from . import errors
9
- from . import s_common, s_exc, s_genpkg
10
13
  from . import utils
11
14
 
12
15
 
@@ -50,6 +53,10 @@ class StormPkg:
50
53
 
51
54
  - Stores the package definition ``dict`` in the ``pkdef`` property.
52
55
 
56
+ - If ``check_syntax`` is True, all Storm code that is loaded in the ``pkdef``
57
+ is passed to Synapse's Storm parser to check for Storm syntax errors.
58
+ Any errors are raised as a ``StormPkgSyntaxError`` exception.
59
+
53
60
  Parameters
54
61
  ----------
55
62
  check_syntax : bool, optional
@@ -62,6 +69,9 @@ class StormPkg:
62
69
 
63
70
  Properties
64
71
  ----------
72
+ default_name : str
73
+ The Yaml basename used if ``proto_name`` is empty (not set).
74
+ Set to the value of ``self.__class__.__name__.lower()``.
65
75
  pkgdef : dict
66
76
  The loaded Storm package definition.
67
77
  proto_dir : str
@@ -71,12 +81,11 @@ class StormPkg:
71
81
  at init, finding the object this resolves to in ``sys.modules``,
72
82
  and reading the module's ``__file__``. Then joining this file's
73
83
  dir name with the value ``pkgproto``.
74
- proto_name : str | None
84
+ proto_name : str
75
85
  A class property containing the name of the Storm package's proto
76
86
  Yaml file (without extension). Subclasses may override this class
77
87
  property to set a custom proto Yaml file basename. If they don't,
78
- this property is automatically set to the value of
79
- ``self.__class__.__name__.lower()``.
88
+ this property is automatically set to the value of ``self.default_name``.
80
89
 
81
90
  Raises
82
91
  ------
@@ -84,16 +93,19 @@ class StormPkg:
84
93
  If the package proto either has an invalid schema or bad value.
85
94
  StormPkgNoProtoError
86
95
  If the proto Yaml file cannot be loaded. This can happen if the file name
87
- will not match ``name``/``pkg_name`` but ``proto_name`` is not passed.
96
+ will not match ``self.default_name`` but ``proto_name`` is not set.
88
97
  StormPkgResolveError
89
98
  If the default package proto dir cannot be resolved. This can be the case
90
99
  if a `StormPkg` is created and doesn't expect to use the default proto dir
91
100
  but a custom ``proto_dir`` is not passed.
101
+ StormPkgSyntaxError
102
+ If ``check_syntax`` is True and the Storm code in this package has syntax errors.
92
103
  RuntimeError
93
104
  If this class is instantiated directly and not subclassed.
94
105
  """
95
106
 
96
107
  proto_name: str | None = None
108
+ """The basename of this Storm package's Yaml proto file."""
97
109
 
98
110
  def __init__(
99
111
  self,
@@ -103,6 +115,10 @@ class StormPkg:
103
115
  if StormPkg not in self.__class__.__bases__:
104
116
  raise RuntimeError("StormPkg must be subclassed.")
105
117
 
118
+ self.default_name = self.__class__.__name__.lower()
119
+ if not self.proto_name:
120
+ self.proto_name = self.default_name
121
+
106
122
  if proto_dir:
107
123
  self.proto_dir = proto_dir
108
124
  else:
@@ -119,10 +135,10 @@ class StormPkg:
119
135
  "pkgproto",
120
136
  )
121
137
 
122
- if not self.proto_name:
123
- self.proto_name = self.__class__.__name__.lower()
124
-
125
- self.proto = os.path.join(self.proto_dir, f"{self.proto_name}.yaml",)
138
+ self.proto = os.path.join(
139
+ self.proto_dir,
140
+ f"{self.proto_name}.yaml",
141
+ )
126
142
 
127
143
  self.pkgdef = self._load_proto()
128
144
  """A Python dict containing the full Storm package definition."""
@@ -156,15 +172,6 @@ class StormPkg:
156
172
 
157
173
  return pkgdef
158
174
 
159
- def _search_def(self, primkey: str, seckey: str) -> list[str]:
160
- """Search the modules or commands of the pkgdef for a secondary key."""
161
-
162
- vals = []
163
- for val in self.pkgdef.get(primkey, []):
164
- if seckey in val:
165
- vals.append(val[seckey])
166
- return vals
167
-
168
175
  def asdict(self):
169
176
  """Return this objects full Storm package definition as a Python dict.
170
177
 
@@ -193,19 +200,19 @@ class StormPkg:
193
200
  f"syntax error: {err}"
194
201
  ) from err
195
202
 
196
- def cmds(self) -> list[str]:
197
- """The commands this package defines."""
203
+ def cmds(self) -> list[dict]:
204
+ """The full definitions of the commands this package defines."""
198
205
 
199
- return self._search_def("commands", "name")
206
+ return self.pkgdef.get("commands", [])
200
207
 
201
- def mods(self) -> list[str]:
202
- """The modules this package defines."""
208
+ def mods(self) -> list[dict]:
209
+ """The full definitions of the modules this package defines."""
203
210
 
204
- return self._search_def("modules", "name")
211
+ return self.pkgdef.get("modules", [])
205
212
 
206
213
  def storm(self) -> dict[str, str]:
207
214
  """The Storm code this package defines.
208
-
215
+
209
216
  Returns
210
217
  -------
211
218
  dict[str, str]
@@ -213,10 +220,37 @@ class StormPkg:
213
220
  """
214
221
 
215
222
  storm = {}
216
- for mod in self.pkgdef.get("modules", []):
217
- if "name" in mod and "storm" in mod:
218
- storm[mod["name"]] = mod["storm"]
219
- for cmd in self.pkgdef.get("commands", []):
220
- if "name" in cmd and "storm" in cmd:
221
- storm[cmd["name"]] = cmd["storm"]
223
+ for item in self.cmds() + self.mods():
224
+ if "name" in item and "storm" in item:
225
+ storm[item["name"]] = item["storm"]
222
226
  return storm
227
+
228
+ @property
229
+ def cmdnames(self) -> list[str]:
230
+ """The names of the commands this package defines, if any."""
231
+
232
+ return [cmd.get("name") for cmd in self.cmds() if "name" in cmd]
233
+
234
+ @property
235
+ def modnames(self) -> list[str]:
236
+ """The names of the modules this package defines, if any."""
237
+
238
+ return [mod.get("name") for mod in self.mods() if "name" in mod]
239
+
240
+ @property
241
+ def pkg_guid(self) -> str:
242
+ """The package's guid, None if not set."""
243
+
244
+ return self.pkgdef.get("guid")
245
+
246
+ @property
247
+ def pkg_name(self) -> str:
248
+ """The package's name, None if not set."""
249
+
250
+ return self.pkgdef.get("name")
251
+
252
+ @property
253
+ def pkg_ver(self) -> str:
254
+ """The package's version, None if not set."""
255
+
256
+ return self.pkgdef.get("version")
@@ -36,3 +36,8 @@ def genDefaultTelepathRetn(obj=TelepathRetn, /, default_data=None) -> TelepathRe
36
36
  """
37
37
 
38
38
  return obj(status=True, mesg="", data=default_data)
39
+
40
+
41
+ class BoolRetn(TelepathRetn):
42
+ """A TelepathRetn where ``data`` is a boolean value."""
43
+ data: bool
@@ -3,8 +3,10 @@
3
3
 
4
4
  import os
5
5
 
6
+ import synapse.exc as s_exc
7
+ import synapse.lib.parser as s_parser
8
+
6
9
  from . import errors
7
- from . import s_exc, s_parser
8
10
 
9
11
 
10
12
  def absjoin(*paths: str) -> str:
@@ -12,7 +14,7 @@ def absjoin(*paths: str) -> str:
12
14
 
13
15
  Parameters
14
16
  ----------
15
- *paths : str
17
+ paths : list[str]
16
18
  The path strings to join together, as would be passed to ``os.path.join``.
17
19
 
18
20
  Returns
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: stormlibpp
3
- Version: 0.1.2
3
+ Version: 0.3.0
4
4
  Summary: The stormlibpp Python package
5
5
  Author: John Gorman
6
6
  License: MIT License
@@ -3,6 +3,7 @@ README.md
3
3
  pyproject.toml
4
4
  src/stormlibpp/__init__.py
5
5
  src/stormlibpp/errors.py
6
+ src/stormlibpp/node.py
6
7
  src/stormlibpp/stormpkg.py
7
8
  src/stormlibpp/telepath.py
8
9
  src/stormlibpp/utils.py
@@ -11,6 +12,7 @@ src/stormlibpp.egg-info/SOURCES.txt
11
12
  src/stormlibpp.egg-info/dependency_links.txt
12
13
  src/stormlibpp.egg-info/requires.txt
13
14
  src/stormlibpp.egg-info/top_level.txt
15
+ tests/test_node.py
14
16
  tests/test_stormpkg.py
15
17
  tests/test_telepath.py
16
18
  tests/test_utils.py
@@ -0,0 +1,43 @@
1
+ """Tests for stormlibpp.node."""
2
+
3
+
4
+ import stormlibpp
5
+
6
+
7
+ PACKED_NODE = (('it:app:yara:rule', '5d0292d99a7da59a3a119e193f140143'), {'iden': '57ce731580ed6976b608e87dfc7f2ece81790f2cb8cdaf958013c738b28cd56e', 'tags': {'test': (None, None), 'test.tag': (None, None)}, 'props': {'.created': 1687720565352, 'text': 'rule dummy { condition: true }', 'name': 'dummy', 'enabled': 1}, 'tagprops': {}, 'nodedata': {}})
8
+
9
+
10
+ class TestNode:
11
+
12
+ def test_packing(self):
13
+ node = stormlibpp.StormNode.unpack(PACKED_NODE)
14
+ assert node.pack() == PACKED_NODE
15
+
16
+ def test_add_tags(self):
17
+ tags = ["#test.othertag", "#atag"]
18
+ node = stormlibpp.StormNode.unpack(PACKED_NODE)
19
+ node.addTags(tags)
20
+
21
+ for tag in tags:
22
+ assert tag in node.tags
23
+
24
+ assert dir(node.tags) == [item[0] for item in iter(node.tags)]
25
+
26
+ def test_add_props(self):
27
+ props = {'prop': 'test', 'other': 'value'}
28
+ node = stormlibpp.StormNode.unpack(PACKED_NODE)
29
+ node.addProps(props)
30
+
31
+ for prop in props:
32
+ assert prop in node.props
33
+
34
+ for propname, propval in node.props:
35
+ if propname in props:
36
+ assert props[propname] == propval
37
+
38
+ def test_values(self):
39
+ node = stormlibpp.StormNode.unpack(PACKED_NODE)
40
+
41
+ assert node.form == PACKED_NODE[0][0]
42
+ assert node.value == PACKED_NODE[0][1]
43
+ assert node.nodevals == PACKED_NODE[1]
@@ -47,6 +47,7 @@ class TestStormPkg:
47
47
 
48
48
  assert isinstance(test.asdict(), dict)
49
49
  assert isinstance(othertest.pkgdef, dict)
50
+ assert "othertest.lib" in othertest.modnames
50
51
 
51
52
  def test_storm_parse(self):
52
53
  class BadStorm(stormlibpp.StormPkg):
@@ -59,4 +60,4 @@ class TestStormPkg:
59
60
  class BadStorm(stormlibpp.StormPkg):
60
61
  pass
61
62
 
62
- assert "badstorm" in BadStorm(check_syntax=False).cmds()
63
+ assert "badstorm" in BadStorm(check_syntax=False).cmdnames
@@ -16,3 +16,11 @@ class TestTelepathRetn:
16
16
  def test_gen_telepath_ret_opts(self):
17
17
  ret = stormlibpp.genDefaultTelepathRetn(SubTeleRet, default_data="default")
18
18
  assert ret == dict(status=True, mesg="", data="default")
19
+
20
+
21
+ class TestCustomRetns:
22
+ def test_gen_boolretn(self):
23
+ ret = stormlibpp.genDefaultTelepathRetn(
24
+ stormlibpp.telepath.BoolRetn, default_data=False
25
+ )
26
+ assert ret == dict(status=True, mesg="", data=False)
@@ -1,25 +0,0 @@
1
- """The stormlibpp Python Package.
2
-
3
- StormLib++ provides a number of Python objects to make life as a Synapse/Storm
4
- developer easier.
5
-
6
- It also exposes a few of Synapses submodles with the following name mappings::
7
-
8
- synapse.common -> s_common
9
- synapse.exc -> s_exc
10
- synapse.genpkg -> s_genpkg
11
-
12
- """
13
-
14
- __version__ = "0.1.2"
15
-
16
-
17
- import synapse.common as s_common
18
- import synapse.exc as s_exc
19
- import synapse.tools.genpkg as s_genpkg
20
- import synapse.lib.parser as s_parser
21
-
22
- from . import errors
23
- from . import utils
24
- from .stormpkg import StormPkg
25
- from .telepath import (genDefaultTelepathRetn, TelepathRetn)
File without changes
File without changes
File without changes