synapse 2.175.0__py311-none-any.whl → 2.177.0__py311-none-any.whl
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.
Potentially problematic release.
This version of synapse might be problematic. Click here for more details.
- synapse/axon.py +24 -9
- synapse/cortex.py +330 -168
- synapse/cryotank.py +46 -37
- synapse/datamodel.py +17 -4
- synapse/exc.py +19 -0
- synapse/lib/agenda.py +7 -13
- synapse/lib/ast.py +6 -5
- synapse/lib/auth.py +1520 -0
- synapse/lib/cell.py +255 -53
- synapse/lib/grammar.py +5 -0
- synapse/lib/hive.py +24 -3
- synapse/lib/hiveauth.py +6 -32
- synapse/lib/layer.py +7 -4
- synapse/lib/link.py +21 -17
- synapse/lib/lmdbslab.py +149 -0
- synapse/lib/modelrev.py +1 -1
- synapse/lib/schemas.py +136 -0
- synapse/lib/storm.py +70 -33
- synapse/lib/stormlib/aha.py +1 -1
- synapse/lib/stormlib/auth.py +185 -10
- synapse/lib/stormlib/cortex.py +16 -5
- synapse/lib/stormlib/gen.py +80 -0
- synapse/lib/stormlib/model.py +55 -0
- synapse/lib/stormlib/modelext.py +60 -0
- synapse/lib/stormlib/storm.py +117 -5
- synapse/lib/stormlib/tabular.py +212 -0
- synapse/lib/stormtypes.py +14 -1
- synapse/lib/trigger.py +1 -1
- synapse/lib/version.py +2 -2
- synapse/lib/view.py +55 -28
- synapse/models/base.py +7 -0
- synapse/models/biz.py +4 -0
- synapse/models/files.py +8 -1
- synapse/models/inet.py +31 -0
- synapse/tests/files/changelog/model_2.176.0_16ee721a6b7221344eaf946c3ab4602dda546b1a.yaml.gz +0 -0
- synapse/tests/files/changelog/model_2.176.0_2a25c58bbd344716cd7cbc3f4304d8925b0f4ef2.yaml.gz +0 -0
- synapse/tests/test_axon.py +7 -4
- synapse/tests/test_cortex.py +127 -82
- synapse/tests/test_cryotank.py +4 -4
- synapse/tests/test_datamodel.py +7 -0
- synapse/tests/test_lib_agenda.py +7 -0
- synapse/tests/{test_lib_hiveauth.py → test_lib_auth.py} +314 -11
- synapse/tests/test_lib_cell.py +161 -8
- synapse/tests/test_lib_httpapi.py +18 -14
- synapse/tests/test_lib_layer.py +33 -33
- synapse/tests/test_lib_link.py +42 -1
- synapse/tests/test_lib_lmdbslab.py +68 -0
- synapse/tests/test_lib_nexus.py +4 -4
- synapse/tests/test_lib_node.py +0 -7
- synapse/tests/test_lib_storm.py +45 -0
- synapse/tests/test_lib_stormlib_aha.py +1 -2
- synapse/tests/test_lib_stormlib_auth.py +21 -0
- synapse/tests/test_lib_stormlib_cortex.py +12 -12
- synapse/tests/test_lib_stormlib_gen.py +99 -0
- synapse/tests/test_lib_stormlib_model.py +108 -0
- synapse/tests/test_lib_stormlib_modelext.py +64 -0
- synapse/tests/test_lib_stormlib_storm.py +82 -1
- synapse/tests/test_lib_stormlib_tabular.py +226 -0
- synapse/tests/test_lib_stormsvc.py +4 -1
- synapse/tests/test_lib_stormtypes.py +10 -0
- synapse/tests/test_model_base.py +3 -0
- synapse/tests/test_model_biz.py +3 -0
- synapse/tests/test_model_files.py +12 -2
- synapse/tests/test_model_inet.py +55 -0
- synapse/tests/test_tools_changelog.py +196 -0
- synapse/tests/test_tools_healthcheck.py +4 -3
- synapse/tests/utils.py +1 -1
- synapse/tools/changelog.py +774 -15
- {synapse-2.175.0.dist-info → synapse-2.177.0.dist-info}/METADATA +3 -3
- {synapse-2.175.0.dist-info → synapse-2.177.0.dist-info}/RECORD +73 -67
- {synapse-2.175.0.dist-info → synapse-2.177.0.dist-info}/WHEEL +1 -1
- {synapse-2.175.0.dist-info → synapse-2.177.0.dist-info}/LICENSE +0 -0
- {synapse-2.175.0.dist-info → synapse-2.177.0.dist-info}/top_level.txt +0 -0
synapse/lib/stormlib/model.py
CHANGED
|
@@ -919,8 +919,63 @@ class LibModelMigrations(s_stormtypes.Lib, MigrationEditorMixin):
|
|
|
919
919
|
'desc': 'Do not copy nodedata to the risk:vulnerable node.'},
|
|
920
920
|
),
|
|
921
921
|
'returns': {'type': 'list', 'desc': 'A list of idens for the risk:vulnerable nodes.'}}},
|
|
922
|
+
{'name': 'inetSslCertToTlsServerCert', 'desc': '''
|
|
923
|
+
Create a inet:tls:servercert node from the provided inet:ssl:cert node.
|
|
924
|
+
|
|
925
|
+
Edits will be made to the inet:tls:servercert node in the current write layer.
|
|
926
|
+
|
|
927
|
+
Tags, tag properties, edges, and node data will be copied
|
|
928
|
+
to the inet:tls:servercert node. However, existing tag properties and
|
|
929
|
+
node data will not be overwritten.
|
|
930
|
+
''',
|
|
931
|
+
'type': {'type': 'function', '_funcname': '_storm_query',
|
|
932
|
+
'args': (
|
|
933
|
+
{'name': 'n', 'type': 'node', 'desc': 'The inet:ssl:cert node to migrate.'},
|
|
934
|
+
{'name': 'nodata', 'type': 'bool', 'default': False,
|
|
935
|
+
'desc': 'Do not copy nodedata to the inet:tls:servercert node.'},
|
|
936
|
+
),
|
|
937
|
+
'returns': {'type': 'node', 'desc': 'The newly created inet:tls:servercert node.'}}},
|
|
938
|
+
|
|
922
939
|
)
|
|
923
940
|
_storm_lib_path = ('model', 'migration', 's')
|
|
941
|
+
_storm_query = '''
|
|
942
|
+
function inetSslCertToTlsServerCert(n, nodata=$lib.false) {
|
|
943
|
+
$form = $n.form()
|
|
944
|
+
if ($form != 'inet:ssl:cert') {
|
|
945
|
+
$mesg = `$lib.model.migration.s.inetSslCertToTlsServerCert() only accepts inet:ssl:cert nodes, not {$form}`
|
|
946
|
+
$lib.raise(BadArg, $mesg)
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
$server = $n.props.server
|
|
950
|
+
$sha256 = { yield $n -> file:bytes -> hash:sha256 }
|
|
951
|
+
|
|
952
|
+
if $sha256 {
|
|
953
|
+
|
|
954
|
+
yield $lib.gen.inetTlsServerCertByServerAndSha256($server, $sha256)
|
|
955
|
+
|
|
956
|
+
} else {
|
|
957
|
+
|
|
958
|
+
// File doesn't have a :sha256, try to lift/create a crypto:x509:node based on the file link
|
|
959
|
+
$crypto = { yield $n -> file:bytes -> crypto:x509:cert:file }
|
|
960
|
+
if (not $crypto) {
|
|
961
|
+
$crypto = {[ crypto:x509:cert=($n.props.file,) :file=$n.props.file ]}
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
[ inet:tls:servercert=($server, $crypto) ]
|
|
965
|
+
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
[ .seen ?= $n.props.".seen" ]
|
|
969
|
+
|
|
970
|
+
$lib.model.migration.copyTags($n, $node, overwrite=$lib.false)
|
|
971
|
+
$lib.model.migration.copyEdges($n, $node)
|
|
972
|
+
if (not $nodata) {
|
|
973
|
+
$lib.model.migration.copyData($n, $node, overwrite=$lib.false)
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
return($node)
|
|
977
|
+
}
|
|
978
|
+
'''
|
|
924
979
|
|
|
925
980
|
def getObjLocals(self):
|
|
926
981
|
return {
|
synapse/lib/stormlib/modelext.py
CHANGED
|
@@ -77,6 +77,27 @@ class LibModelExt(s_stormtypes.Lib):
|
|
|
77
77
|
{'name': 'model', 'type': 'dict', 'desc': 'A model dictionary from getExtModel().', },
|
|
78
78
|
),
|
|
79
79
|
'returns': {'type': 'boolean'}}},
|
|
80
|
+
{'name': 'addEdge', 'desc': 'Add an extended edge definition to the data model.',
|
|
81
|
+
'type': {'type': 'function', '_funcname': 'addEdge',
|
|
82
|
+
'args': (
|
|
83
|
+
{'name': 'n1form', 'type': 'str',
|
|
84
|
+
'desc': 'The form of the n1 node. May be "*" or null to specify "any".'},
|
|
85
|
+
{'name': 'verb', 'type': 'str', 'desc': 'The edge verb, which must begin with "_".'},
|
|
86
|
+
{'name': 'n2form', 'type': 'str',
|
|
87
|
+
'desc': 'The form of the n2 node. May be "*" or null to specify "any".'},
|
|
88
|
+
{'name': 'edgeinfo', 'type': 'dict', 'desc': 'A Synapse edge info dictionary.'},
|
|
89
|
+
),
|
|
90
|
+
'returns': {'type': 'null'}}},
|
|
91
|
+
{'name': 'delEdge', 'desc': 'Remove an extended edge definition from the data model.',
|
|
92
|
+
'type': {'type': 'function', '_funcname': 'delEdge',
|
|
93
|
+
'args': (
|
|
94
|
+
{'name': 'n1form', 'type': 'str',
|
|
95
|
+
'desc': 'The form of the n1 node. May be "*" or null to specify "any".'},
|
|
96
|
+
{'name': 'verb', 'type': 'str', 'desc': 'The edge verb, which must begin with "_".'},
|
|
97
|
+
{'name': 'n2form', 'type': 'str',
|
|
98
|
+
'desc': 'The form of the n2 node. May be "*" or null to specify "any".'},
|
|
99
|
+
),
|
|
100
|
+
'returns': {'type': 'null'}}},
|
|
80
101
|
)
|
|
81
102
|
_storm_lib_path = ('model', 'ext')
|
|
82
103
|
|
|
@@ -92,6 +113,8 @@ class LibModelExt(s_stormtypes.Lib):
|
|
|
92
113
|
'delTagProp': self.delTagProp,
|
|
93
114
|
'getExtModel': self.getExtModel,
|
|
94
115
|
'addExtModel': self.addExtModel,
|
|
116
|
+
'addEdge': self.addEdge,
|
|
117
|
+
'delEdge': self.delEdge,
|
|
95
118
|
}
|
|
96
119
|
|
|
97
120
|
# TODO type docs in the new convention
|
|
@@ -163,3 +186,40 @@ class LibModelExt(s_stormtypes.Lib):
|
|
|
163
186
|
async def addExtModel(self, model):
|
|
164
187
|
model = await s_stormtypes.toprim(model)
|
|
165
188
|
return await self.runt.snap.core.addExtModel(model)
|
|
189
|
+
|
|
190
|
+
async def addEdge(self, n1form, verb, n2form, edgeinfo):
|
|
191
|
+
verb = await s_stormtypes.tostr(verb)
|
|
192
|
+
n1form = await s_stormtypes.tostr(n1form, noneok=True)
|
|
193
|
+
n2form = await s_stormtypes.tostr(n2form, noneok=True)
|
|
194
|
+
edgeinfo = await s_stormtypes.toprim(edgeinfo)
|
|
195
|
+
|
|
196
|
+
if not (s_grammar.isEdgeVerb(verb) and verb.islower()):
|
|
197
|
+
mesg = f'Invalid edge verb {verb}'
|
|
198
|
+
raise s_exc.BadEdgeDef(mesg=mesg, n1form=n1form, verb=verb, n2form=n2form)
|
|
199
|
+
|
|
200
|
+
if n1form == '*':
|
|
201
|
+
n1form = None
|
|
202
|
+
|
|
203
|
+
if n2form == '*':
|
|
204
|
+
n2form = None
|
|
205
|
+
|
|
206
|
+
s_stormtypes.confirm(('model', 'edge', 'add'))
|
|
207
|
+
await self.runt.snap.core.addEdge((n1form, verb, n2form), edgeinfo)
|
|
208
|
+
|
|
209
|
+
async def delEdge(self, n1form, verb, n2form):
|
|
210
|
+
verb = await s_stormtypes.tostr(verb)
|
|
211
|
+
n1form = await s_stormtypes.tostr(n1form, noneok=True)
|
|
212
|
+
n2form = await s_stormtypes.tostr(n2form, noneok=True)
|
|
213
|
+
|
|
214
|
+
if not (s_grammar.isEdgeVerb(verb) and verb.islower()):
|
|
215
|
+
mesg = f'Invalid edge verb {verb}'
|
|
216
|
+
raise s_exc.BadEdgeDef(mesg=mesg, n1form=n1form, verb=verb, n2form=n2form)
|
|
217
|
+
|
|
218
|
+
if n1form == '*':
|
|
219
|
+
n1form = None
|
|
220
|
+
|
|
221
|
+
if n2form == '*':
|
|
222
|
+
n2form = None
|
|
223
|
+
|
|
224
|
+
s_stormtypes.confirm(('model', 'edge', 'del'))
|
|
225
|
+
await self.runt.snap.core.delEdge((n1form, verb, n2form))
|
synapse/lib/stormlib/storm.py
CHANGED
|
@@ -1,18 +1,102 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
|
|
3
3
|
import synapse.exc as s_exc
|
|
4
|
+
import synapse.common as s_common
|
|
4
5
|
|
|
6
|
+
import synapse.lib.storm as s_storm
|
|
5
7
|
import synapse.lib.stormtypes as s_stormtypes
|
|
6
8
|
|
|
7
9
|
evaldesc = '''\
|
|
8
|
-
Evaluate a
|
|
10
|
+
Evaluate a Storm runtime value and optionally cast/coerce it.
|
|
9
11
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
+
Note:
|
|
13
|
+
If Storm logging is enabled, the expression being evaluated will be logged
|
|
14
|
+
separately.
|
|
15
|
+
'''
|
|
16
|
+
|
|
17
|
+
rundesc = '''
|
|
18
|
+
Run a Storm query and yield the messages output by the Storm interpreter.
|
|
19
|
+
|
|
20
|
+
Note:
|
|
21
|
+
If Storm logging is enabled, the query being run will be logged separately.
|
|
12
22
|
'''
|
|
13
23
|
|
|
14
24
|
stormlogger = logging.getLogger('synapse.storm')
|
|
15
25
|
|
|
26
|
+
class StormExecCmd(s_storm.Cmd):
|
|
27
|
+
'''
|
|
28
|
+
Execute text or an embedded query object as Storm in the current pipeline.
|
|
29
|
+
|
|
30
|
+
NOTE: It is recommended to avoid using this where possible to avoid potential
|
|
31
|
+
query injection risks. If you must use this, take care to ensure any values
|
|
32
|
+
being executed have been properly sanitized.
|
|
33
|
+
|
|
34
|
+
Examples:
|
|
35
|
+
|
|
36
|
+
// Add nodes using text in a variable
|
|
37
|
+
$query = '[ inet:fqdn=foo.com inet:fqdn=bar.net ]'
|
|
38
|
+
storm.exec $query
|
|
39
|
+
|
|
40
|
+
// Filter nodes in the pipeline using text in a variable
|
|
41
|
+
$filter = '-:asn=10'
|
|
42
|
+
inet:ipv4:asn
|
|
43
|
+
storm.exec $filter
|
|
44
|
+
|
|
45
|
+
// Pivot using an embedded query
|
|
46
|
+
$pivot = ${ -> inet:asn }
|
|
47
|
+
inet:ipv4:asn
|
|
48
|
+
storm.exec $pivot
|
|
49
|
+
'''
|
|
50
|
+
name = 'storm.exec'
|
|
51
|
+
def getArgParser(self):
|
|
52
|
+
pars = s_storm.Cmd.getArgParser(self)
|
|
53
|
+
pars.add_argument('query', help='The Storm to execute.')
|
|
54
|
+
return pars
|
|
55
|
+
|
|
56
|
+
async def execStormCmd(self, runt, genr):
|
|
57
|
+
|
|
58
|
+
if self.runtsafe:
|
|
59
|
+
|
|
60
|
+
text = await s_stormtypes.tostr(self.opts.query)
|
|
61
|
+
query = await runt.getStormQuery(text)
|
|
62
|
+
|
|
63
|
+
extra = await self.runt.snap.core.getLogExtra(text=text, view=self.runt.snap.view.iden)
|
|
64
|
+
stormlogger.info(f'Executing storm query via storm.exec {{{text}}} as [{self.runt.user.name}]', extra=extra)
|
|
65
|
+
|
|
66
|
+
async with runt.getSubRuntime(query) as subr:
|
|
67
|
+
async for subp in subr.execute(genr=genr):
|
|
68
|
+
yield subp
|
|
69
|
+
|
|
70
|
+
else:
|
|
71
|
+
|
|
72
|
+
item = None
|
|
73
|
+
async for item in genr:
|
|
74
|
+
break
|
|
75
|
+
|
|
76
|
+
text = await s_stormtypes.tostr(self.opts.query)
|
|
77
|
+
query = await runt.getStormQuery(text)
|
|
78
|
+
|
|
79
|
+
extra = await self.runt.snap.core.getLogExtra(text=text, view=self.runt.snap.view.iden)
|
|
80
|
+
stormlogger.info(f'Executing storm query via storm.exec {{{text}}} as [{self.runt.user.name}]', extra=extra)
|
|
81
|
+
|
|
82
|
+
async with runt.getSubRuntime(query) as subr:
|
|
83
|
+
async for subp in subr.execute(genr=s_common.agen(item)):
|
|
84
|
+
yield subp
|
|
85
|
+
|
|
86
|
+
async for item in genr:
|
|
87
|
+
text = await s_stormtypes.tostr(self.opts.query)
|
|
88
|
+
query = await runt.getStormQuery(text)
|
|
89
|
+
|
|
90
|
+
subr.runtvars.clear()
|
|
91
|
+
subr.query = query
|
|
92
|
+
subr._initRuntVars(query)
|
|
93
|
+
|
|
94
|
+
extra = await self.runt.snap.core.getLogExtra(text=text, view=self.runt.snap.view.iden)
|
|
95
|
+
stormlogger.info(f'Executing storm query via storm.exec {{{text}}} as [{self.runt.user.name}]', extra=extra)
|
|
96
|
+
|
|
97
|
+
async for subp in subr.execute(genr=s_common.agen(item)):
|
|
98
|
+
yield subp
|
|
99
|
+
|
|
16
100
|
@s_stormtypes.registry.registerLib
|
|
17
101
|
class LibStorm(s_stormtypes.Lib):
|
|
18
102
|
'''
|
|
@@ -25,15 +109,43 @@ class LibStorm(s_stormtypes.Lib):
|
|
|
25
109
|
{'name': 'text', 'type': 'str', 'desc': 'A storm expression string.'},
|
|
26
110
|
{'name': 'cast', 'type': 'str', 'desc': 'A type to cast the result to.', 'default': None},
|
|
27
111
|
),
|
|
28
|
-
'returns': {'type': 'any', 'desc': 'The value of the expression and optional cast.'
|
|
112
|
+
'returns': {'type': 'any', 'desc': 'The value of the expression and optional cast.'}}},
|
|
113
|
+
{'name': 'run', 'desc': rundesc,
|
|
114
|
+
'type': {'type': 'function', '_funcname': '_runStorm',
|
|
115
|
+
'args': (
|
|
116
|
+
{'name': 'query', 'type': 'str', 'desc': 'A Storm query string.'},
|
|
117
|
+
{'name': 'opts', 'type': 'dict', 'desc': 'Storm options dictionary.', 'default': None},
|
|
118
|
+
),
|
|
119
|
+
'returns': {'name': 'yields', 'type': 'list', 'desc': 'The output messages from the Storm runtime.'}}},
|
|
29
120
|
)
|
|
30
121
|
_storm_lib_path = ('storm',)
|
|
31
122
|
|
|
32
123
|
def getObjLocals(self):
|
|
33
124
|
return {
|
|
125
|
+
'run': self._runStorm,
|
|
34
126
|
'eval': self._evalStorm,
|
|
35
127
|
}
|
|
36
128
|
|
|
129
|
+
async def _runStorm(self, query, opts=None):
|
|
130
|
+
|
|
131
|
+
opts = await s_stormtypes.toprim(opts)
|
|
132
|
+
query = await s_stormtypes.tostr(query)
|
|
133
|
+
|
|
134
|
+
if opts is None:
|
|
135
|
+
opts = {}
|
|
136
|
+
|
|
137
|
+
user = opts.get('user')
|
|
138
|
+
if user is None:
|
|
139
|
+
user = opts['user'] = self.runt.user.iden
|
|
140
|
+
|
|
141
|
+
if user != self.runt.user.iden:
|
|
142
|
+
self.runt.confirm(('impersonate',))
|
|
143
|
+
|
|
144
|
+
opts.setdefault('view', self.runt.snap.view.iden)
|
|
145
|
+
|
|
146
|
+
async for mesg in self.runt.snap.view.core.storm(query, opts=opts):
|
|
147
|
+
yield mesg
|
|
148
|
+
|
|
37
149
|
@s_stormtypes.stormfunc(readonly=True)
|
|
38
150
|
async def _evalStorm(self, text, cast=None):
|
|
39
151
|
|
|
@@ -41,7 +153,7 @@ class LibStorm(s_stormtypes.Lib):
|
|
|
41
153
|
cast = await s_stormtypes.tostr(cast, noneok=True)
|
|
42
154
|
|
|
43
155
|
if self.runt.snap.core.stormlog:
|
|
44
|
-
extra = await self.runt.snap.core.getLogExtra(text=text)
|
|
156
|
+
extra = await self.runt.snap.core.getLogExtra(text=text, view=self.runt.snap.view.iden)
|
|
45
157
|
stormlogger.info(f'Executing storm query via $lib.storm.eval() {{{text}}} as [{self.runt.user.name}]', extra=extra)
|
|
46
158
|
|
|
47
159
|
casttype = None
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
import textwrap
|
|
3
|
+
import itertools
|
|
4
|
+
|
|
5
|
+
import synapse.exc as s_exc
|
|
6
|
+
import synapse.common as s_common
|
|
7
|
+
import synapse.lib.schemas as s_schemas
|
|
8
|
+
import synapse.lib.stormtypes as s_stormtypes
|
|
9
|
+
|
|
10
|
+
justers = {
|
|
11
|
+
'left': str.ljust,
|
|
12
|
+
'center': str.center,
|
|
13
|
+
'right': str.rjust,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
@s_stormtypes.registry.registerLib
|
|
17
|
+
class LibTabular(s_stormtypes.Lib):
|
|
18
|
+
'''
|
|
19
|
+
A Storm Library for creating printable tables.
|
|
20
|
+
'''
|
|
21
|
+
_storm_locals = (
|
|
22
|
+
{'name': 'printer', 'desc': '''
|
|
23
|
+
Construct a new printer.
|
|
24
|
+
|
|
25
|
+
Examples:
|
|
26
|
+
Create a simple table using the default separators::
|
|
27
|
+
|
|
28
|
+
$conf = ({
|
|
29
|
+
"columns": [
|
|
30
|
+
{"name": "Year", "width": 4},
|
|
31
|
+
{"name": "Author", "width": 20},
|
|
32
|
+
{"name": "Title", "width": 12},
|
|
33
|
+
]
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
$printer = $lib.tabular.printer($conf)
|
|
37
|
+
|
|
38
|
+
$lib.print($printer.header())
|
|
39
|
+
|
|
40
|
+
for ($year, $author, $title, $publisher) in $data {
|
|
41
|
+
$lib.print($printer.row(($year, $author, $title))
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
Create a configuration with custom separators and column options::
|
|
45
|
+
|
|
46
|
+
$conf = ({
|
|
47
|
+
"separators": {
|
|
48
|
+
"row:outline": true,
|
|
49
|
+
"column:outline": true,
|
|
50
|
+
"header:row": "#",
|
|
51
|
+
"data:row": "*",
|
|
52
|
+
"column": "+",
|
|
53
|
+
},
|
|
54
|
+
"columns": [
|
|
55
|
+
{"name": "Year", "width": 4, "justify": "right"},
|
|
56
|
+
{"name": "Author", "width": 20, "justify": "center"},
|
|
57
|
+
{"name": "Title", "width": 12, "overflow": "wrap"},
|
|
58
|
+
]
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
$printer = $lib.tabular.printer($conf)
|
|
62
|
+
''',
|
|
63
|
+
'type': {'type': 'function', '_funcname': '_methPrinter',
|
|
64
|
+
'args': (
|
|
65
|
+
{'name': 'conf', 'type': 'dict',
|
|
66
|
+
'desc': 'The table configuration dictionary.'},
|
|
67
|
+
),
|
|
68
|
+
'returns': {'type': 'tabular:printer',
|
|
69
|
+
'desc': 'The newly constructed tabular:printer.'}}},
|
|
70
|
+
{'name': 'schema', 'desc': '''
|
|
71
|
+
Get a copy of the table configuration schema.
|
|
72
|
+
|
|
73
|
+
Examples:
|
|
74
|
+
Print a human-readable version of the schema::
|
|
75
|
+
|
|
76
|
+
$schema = $lib.tabular.schema()
|
|
77
|
+
$lib.print($lib.yaml.save($schema))
|
|
78
|
+
''',
|
|
79
|
+
'type': {'type': 'function', '_funcname': '_methSchema',
|
|
80
|
+
'returns': {'type': 'dict',
|
|
81
|
+
'desc': 'The table configuration schema.'}}},
|
|
82
|
+
)
|
|
83
|
+
_storm_lib_path = ('tabular',)
|
|
84
|
+
|
|
85
|
+
def getObjLocals(self):
|
|
86
|
+
return {
|
|
87
|
+
'printer': self._methPrinter,
|
|
88
|
+
'schema': self._methSchema,
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async def _methSchema(self):
|
|
92
|
+
return copy.deepcopy(s_schemas.tabularConfSchema)
|
|
93
|
+
|
|
94
|
+
async def _methPrinter(self, conf):
|
|
95
|
+
conf = await s_stormtypes.toprim(conf)
|
|
96
|
+
conf.setdefault('separators', {})
|
|
97
|
+
conf = s_schemas.reqValidTabularConf(conf)
|
|
98
|
+
return TabularPrinter(self.runt, conf)
|
|
99
|
+
|
|
100
|
+
@s_stormtypes.registry.registerType
|
|
101
|
+
class TabularPrinter(s_stormtypes.StormType):
|
|
102
|
+
'''
|
|
103
|
+
A Storm object for printing tabular data using a defined configuration.
|
|
104
|
+
'''
|
|
105
|
+
_storm_typename = 'tabular:printer'
|
|
106
|
+
_storm_locals = (
|
|
107
|
+
{'name': 'row', 'desc': 'Create a new row string from a data list.',
|
|
108
|
+
'type': {'type': 'function', '_funcname': 'row',
|
|
109
|
+
'args': (
|
|
110
|
+
{'name': 'data', 'type': 'list',
|
|
111
|
+
'desc': 'The data to create the row from; length must match the number of configured columns.'},
|
|
112
|
+
),
|
|
113
|
+
'returns': {'type': 'str', 'desc': 'The row string.'}}},
|
|
114
|
+
{'name': 'header',
|
|
115
|
+
'desc': 'Create a header row string.',
|
|
116
|
+
'type': {'type': 'function', '_funcname': 'header',
|
|
117
|
+
'returns': {'type': 'str', 'desc': 'The header row string.'}}},
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
def __init__(self, runt, conf):
|
|
121
|
+
s_stormtypes.StormType.__init__(self, None)
|
|
122
|
+
self.runt = runt
|
|
123
|
+
self.conf = conf
|
|
124
|
+
|
|
125
|
+
self.firstrow = True
|
|
126
|
+
|
|
127
|
+
self.seprconf = conf['separators']
|
|
128
|
+
|
|
129
|
+
self.colconf = conf['columns']
|
|
130
|
+
self.colcnt = len(self.colconf)
|
|
131
|
+
self.colwidths = [coldef.get('width') or len(coldef['name']) for coldef in self.colconf]
|
|
132
|
+
|
|
133
|
+
self.locls.update({
|
|
134
|
+
'row': self.row,
|
|
135
|
+
'header': self.header,
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
def _formatRowLine(self, lineitems, pad=' '):
|
|
139
|
+
sepr = self.seprconf['column']
|
|
140
|
+
endstr = sepr if self.seprconf['column:outline'] else ''
|
|
141
|
+
return f'{endstr}{pad}{f"{pad}{sepr}{pad}".join(lineitems)}{pad}{endstr}'
|
|
142
|
+
|
|
143
|
+
def _makeSeparatorRowStr(self, rowsepr):
|
|
144
|
+
if rowsepr:
|
|
145
|
+
lineitems = (width * rowsepr for width in self.colwidths)
|
|
146
|
+
return self._formatRowLine(lineitems, pad=rowsepr)
|
|
147
|
+
return None
|
|
148
|
+
|
|
149
|
+
def _makeDataRowStrs(self, values):
|
|
150
|
+
|
|
151
|
+
items = []
|
|
152
|
+
for coldef, valu in zip(self.colconf, values):
|
|
153
|
+
width = coldef.get('width')
|
|
154
|
+
valu = str(valu) if valu is not None else ''
|
|
155
|
+
|
|
156
|
+
if width is None:
|
|
157
|
+
if coldef['newlines'] == 'split':
|
|
158
|
+
items.append(valu.split('\n'))
|
|
159
|
+
else:
|
|
160
|
+
items.append([valu.replace('\n', ' ')])
|
|
161
|
+
continue
|
|
162
|
+
|
|
163
|
+
valu = valu.replace('\n', ' ')
|
|
164
|
+
|
|
165
|
+
if len(valu) <= width:
|
|
166
|
+
items.append([justers[coldef['justify']](valu, width)])
|
|
167
|
+
continue
|
|
168
|
+
|
|
169
|
+
if coldef['overflow'] == 'trim':
|
|
170
|
+
items.append([s_common.trimText(valu, n=width)])
|
|
171
|
+
continue
|
|
172
|
+
|
|
173
|
+
items.append([justers[coldef['justify']](item, width) for item in textwrap.wrap(valu, width)])
|
|
174
|
+
|
|
175
|
+
rowstrs = []
|
|
176
|
+
for lineitems in itertools.zip_longest(*items, fillvalue=None):
|
|
177
|
+
lineitems = ([item or self.colwidths[i] * ' ' for i, item in enumerate(lineitems)])
|
|
178
|
+
rowstrs.append(self._formatRowLine(lineitems))
|
|
179
|
+
|
|
180
|
+
return rowstrs
|
|
181
|
+
|
|
182
|
+
async def header(self):
|
|
183
|
+
rowstrs = self._makeDataRowStrs([col['name'] for col in self.colconf])
|
|
184
|
+
|
|
185
|
+
if seprstr := self._makeSeparatorRowStr(self.seprconf['header:row']):
|
|
186
|
+
rowstrs.append(seprstr)
|
|
187
|
+
if self.seprconf['row:outline']:
|
|
188
|
+
rowstrs.insert(0, seprstr)
|
|
189
|
+
|
|
190
|
+
return '\n'.join(rowstrs)
|
|
191
|
+
|
|
192
|
+
async def row(self, data):
|
|
193
|
+
data = await s_stormtypes.toprim(data)
|
|
194
|
+
|
|
195
|
+
if not isinstance(data, (list, tuple)):
|
|
196
|
+
raise s_exc.BadArg(mesg='tabular:printer row() requires a list argument')
|
|
197
|
+
|
|
198
|
+
if len(data) != self.colcnt:
|
|
199
|
+
mesg = 'tabular:printer row() requires data length to equal column count'
|
|
200
|
+
raise s_exc.BadArg(mesg=mesg, length=len(data), column_length=self.colcnt)
|
|
201
|
+
|
|
202
|
+
rowstrs = self._makeDataRowStrs(data)
|
|
203
|
+
|
|
204
|
+
if seprstr := self._makeSeparatorRowStr(self.seprconf['data:row']):
|
|
205
|
+
if self.seprconf['row:outline']:
|
|
206
|
+
rowstrs.append(seprstr)
|
|
207
|
+
elif self.firstrow:
|
|
208
|
+
self.firstrow = False
|
|
209
|
+
else:
|
|
210
|
+
rowstrs.insert(0, seprstr)
|
|
211
|
+
|
|
212
|
+
return '\n'.join(rowstrs)
|
synapse/lib/stormtypes.py
CHANGED
|
@@ -4138,7 +4138,7 @@ class LibBase64(Lib):
|
|
|
4138
4138
|
if urlsafe:
|
|
4139
4139
|
return base64.urlsafe_b64decode(valu)
|
|
4140
4140
|
return base64.b64decode(valu)
|
|
4141
|
-
except binascii.Error as e:
|
|
4141
|
+
except (binascii.Error, TypeError) as e:
|
|
4142
4142
|
mesg = f'Error during base64 decoding - {str(e)}: {s_common.trimText(repr(valu))}'
|
|
4143
4143
|
raise s_exc.StormRuntimeError(mesg=mesg, urlsafe=urlsafe) from None
|
|
4144
4144
|
|
|
@@ -7532,6 +7532,9 @@ class View(Prim):
|
|
|
7532
7532
|
{'name': 'wipeLayer', 'desc': 'Delete all nodes and nodedata from the write layer. Triggers will be run.',
|
|
7533
7533
|
'type': {'type': 'function', '_funcname': '_methWipeLayer',
|
|
7534
7534
|
'returns': {'type': 'null', }}},
|
|
7535
|
+
{'name': 'swapLayer', 'desc': 'Swaps the top layer for a fresh one and deletes the old layer.',
|
|
7536
|
+
'type': {'type': 'function', '_funcname': '_methSwapLayer',
|
|
7537
|
+
'returns': {'type': 'null', }}},
|
|
7535
7538
|
{'name': 'addNode', 'desc': '''Transactionally add a single node and all it's properties. If any validation fails, no changes are made.''',
|
|
7536
7539
|
'type': {'type': 'function', '_funcname': 'addNode',
|
|
7537
7540
|
'args': (
|
|
@@ -7717,6 +7720,7 @@ class View(Prim):
|
|
|
7717
7720
|
'addNode': self.addNode,
|
|
7718
7721
|
'getEdges': self._methGetEdges,
|
|
7719
7722
|
'wipeLayer': self._methWipeLayer,
|
|
7723
|
+
'swapLayer': self._methSwapLayer,
|
|
7720
7724
|
'addNodeEdits': self._methAddNodeEdits,
|
|
7721
7725
|
'getEdgeVerbs': self._methGetEdgeVerbs,
|
|
7722
7726
|
'getFormCounts': self._methGetFormcount,
|
|
@@ -8017,6 +8021,15 @@ class View(Prim):
|
|
|
8017
8021
|
view = self.runt.snap.core.getView(viewiden)
|
|
8018
8022
|
await view.wipeLayer(useriden=useriden)
|
|
8019
8023
|
|
|
8024
|
+
async def _methSwapLayer(self):
|
|
8025
|
+
|
|
8026
|
+
view = self._reqView()
|
|
8027
|
+
|
|
8028
|
+
self.runt.reqAdmin(gateiden=view.iden)
|
|
8029
|
+
self.runt.confirm(('layer', 'del'), gateiden=view.layers[0].iden)
|
|
8030
|
+
|
|
8031
|
+
await view.swapLayer()
|
|
8032
|
+
|
|
8020
8033
|
async def getMerges(self):
|
|
8021
8034
|
view = self._reqView()
|
|
8022
8035
|
async for merge in view.getMerges():
|
synapse/lib/trigger.py
CHANGED
|
@@ -494,7 +494,7 @@ class Trigger:
|
|
|
494
494
|
await self.view.core.getStormQuery(valu)
|
|
495
495
|
|
|
496
496
|
self.tdef[name] = valu
|
|
497
|
-
|
|
497
|
+
self.view.trigdict.set(self.iden, self.tdef)
|
|
498
498
|
|
|
499
499
|
def get(self, name):
|
|
500
500
|
return self.tdef.get(name)
|
synapse/lib/version.py
CHANGED
|
@@ -223,6 +223,6 @@ def reqVersion(valu, reqver,
|
|
|
223
223
|
##############################################################################
|
|
224
224
|
# The following are touched during the release process by bumpversion.
|
|
225
225
|
# Do not modify these directly.
|
|
226
|
-
version = (2,
|
|
226
|
+
version = (2, 177, 0)
|
|
227
227
|
verstring = '.'.join([str(x) for x in version])
|
|
228
|
-
commit = '
|
|
228
|
+
commit = 'b7da795aeb690020e38e99dc5aa1f505cdc2d659'
|