synapse 2.192.0__py311-none-any.whl → 2.194.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/common.py +15 -0
- synapse/cortex.py +19 -25
- synapse/datamodel.py +6 -3
- synapse/exc.py +6 -1
- synapse/lib/agenda.py +17 -6
- synapse/lib/ast.py +242 -97
- synapse/lib/auth.py +1 -0
- synapse/lib/cell.py +31 -85
- synapse/lib/cli.py +20 -11
- synapse/lib/parser.py +5 -1
- synapse/lib/snap.py +44 -15
- synapse/lib/storm.lark +16 -1
- synapse/lib/storm.py +40 -21
- synapse/lib/storm_format.py +1 -0
- synapse/lib/stormctrl.py +88 -6
- synapse/lib/stormlib/cache.py +6 -2
- synapse/lib/stormlib/json.py +5 -2
- synapse/lib/stormlib/scrape.py +1 -1
- synapse/lib/stormlib/stix.py +8 -8
- synapse/lib/stormtypes.py +32 -5
- synapse/lib/version.py +2 -2
- synapse/lib/view.py +20 -3
- synapse/models/geopol.py +1 -0
- synapse/models/geospace.py +1 -0
- synapse/models/inet.py +20 -1
- synapse/models/infotech.py +24 -6
- synapse/models/orgs.py +7 -2
- synapse/models/person.py +15 -4
- synapse/models/risk.py +19 -2
- synapse/models/telco.py +10 -3
- synapse/tests/test_axon.py +6 -6
- synapse/tests/test_cortex.py +133 -14
- synapse/tests/test_exc.py +4 -0
- synapse/tests/test_lib_agenda.py +282 -2
- synapse/tests/test_lib_aha.py +13 -6
- synapse/tests/test_lib_ast.py +301 -10
- synapse/tests/test_lib_auth.py +6 -7
- synapse/tests/test_lib_cell.py +71 -1
- synapse/tests/test_lib_grammar.py +14 -0
- synapse/tests/test_lib_layer.py +1 -1
- synapse/tests/test_lib_lmdbslab.py +3 -3
- synapse/tests/test_lib_storm.py +273 -55
- synapse/tests/test_lib_stormctrl.py +65 -0
- synapse/tests/test_lib_stormhttp.py +5 -5
- synapse/tests/test_lib_stormlib_auth.py +5 -5
- synapse/tests/test_lib_stormlib_cache.py +38 -6
- synapse/tests/test_lib_stormlib_json.py +20 -0
- synapse/tests/test_lib_stormlib_modelext.py +3 -3
- synapse/tests/test_lib_stormlib_scrape.py +6 -6
- synapse/tests/test_lib_stormlib_spooled.py +1 -1
- synapse/tests/test_lib_stormlib_xml.py +5 -5
- synapse/tests/test_lib_stormtypes.py +54 -57
- synapse/tests/test_lib_view.py +1 -1
- synapse/tests/test_model_base.py +1 -2
- synapse/tests/test_model_geopol.py +4 -0
- synapse/tests/test_model_geospace.py +6 -0
- synapse/tests/test_model_inet.py +43 -5
- synapse/tests/test_model_infotech.py +10 -1
- synapse/tests/test_model_orgs.py +17 -2
- synapse/tests/test_model_person.py +23 -1
- synapse/tests/test_model_risk.py +13 -0
- synapse/tests/test_tools_healthcheck.py +4 -4
- synapse/tests/test_tools_storm.py +95 -0
- synapse/tests/test_utils.py +17 -18
- synapse/tests/test_utils_getrefs.py +1 -1
- synapse/tests/utils.py +0 -35
- synapse/tools/changelog.py +6 -4
- synapse/tools/storm.py +1 -1
- synapse/utils/getrefs.py +14 -3
- synapse/vendor/cpython/lib/http/__init__.py +0 -0
- synapse/vendor/cpython/lib/http/cookies.py +59 -0
- synapse/vendor/cpython/lib/test/test_http_cookies.py +49 -0
- {synapse-2.192.0.dist-info → synapse-2.194.0.dist-info}/METADATA +6 -6
- {synapse-2.192.0.dist-info → synapse-2.194.0.dist-info}/RECORD +77 -73
- {synapse-2.192.0.dist-info → synapse-2.194.0.dist-info}/WHEEL +1 -1
- {synapse-2.192.0.dist-info → synapse-2.194.0.dist-info}/LICENSE +0 -0
- {synapse-2.192.0.dist-info → synapse-2.194.0.dist-info}/top_level.txt +0 -0
|
@@ -18,6 +18,7 @@ class GeoPolModelTest(s_t_utils.SynTest):
|
|
|
18
18
|
]
|
|
19
19
|
''')
|
|
20
20
|
self.len(1, nodes)
|
|
21
|
+
node = nodes[0]
|
|
21
22
|
self.eq('visiland', nodes[0].get('name'))
|
|
22
23
|
self.eq(('visitopia',), nodes[0].get('names'))
|
|
23
24
|
self.eq(1640995200000, nodes[0].get('founded'))
|
|
@@ -29,6 +30,9 @@ class GeoPolModelTest(s_t_utils.SynTest):
|
|
|
29
30
|
self.len(2, await core.nodes('pol:country -> geo:name'))
|
|
30
31
|
self.len(3, await core.nodes('pol:country -> econ:currency'))
|
|
31
32
|
|
|
33
|
+
self.len(1, nodes := await core.nodes('[ pol:country=({"name": "visitopia"}) ]'))
|
|
34
|
+
self.eq(node.ndef, nodes[0].ndef)
|
|
35
|
+
|
|
32
36
|
nodes = await core.nodes('''
|
|
33
37
|
[ pol:vitals=*
|
|
34
38
|
:country={pol:country:name=visiland}
|
|
@@ -281,6 +281,12 @@ class GeoTest(s_t_utils.SynTest):
|
|
|
281
281
|
nodes = await core.nodes('[ geo:place=(hehe, haha) :names=("Foo Bar ", baz) ] -> geo:name')
|
|
282
282
|
self.eq(('baz', 'foo bar'), [n.ndef[1] for n in nodes])
|
|
283
283
|
|
|
284
|
+
nodes = await core.nodes('geo:place=(hehe, haha)')
|
|
285
|
+
node = nodes[0]
|
|
286
|
+
|
|
287
|
+
self.len(1, nodes := await core.nodes('[ geo:place=({"name": "baz"}) ]'))
|
|
288
|
+
self.eq(node.ndef, nodes[0].ndef)
|
|
289
|
+
|
|
284
290
|
async def test_eq(self):
|
|
285
291
|
|
|
286
292
|
async with self.getTestCore() as core:
|
synapse/tests/test_model_inet.py
CHANGED
|
@@ -10,17 +10,40 @@ class InetModelTest(s_t_utils.SynTest):
|
|
|
10
10
|
|
|
11
11
|
async def test_model_inet_basics(self):
|
|
12
12
|
async with self.getTestCore() as core:
|
|
13
|
+
self.len(1, await core.nodes('[ inet:web:hashtag="#🫠" ]'))
|
|
14
|
+
self.len(1, await core.nodes('[ inet:web:hashtag="#🫠🫠" ]'))
|
|
15
|
+
self.len(1, await core.nodes('[ inet:web:hashtag="#·bar"]'))
|
|
16
|
+
self.len(1, await core.nodes('[ inet:web:hashtag="#foo·"]'))
|
|
17
|
+
self.len(1, await core.nodes('[ inet:web:hashtag="#foo〜"]'))
|
|
13
18
|
self.len(1, await core.nodes('[ inet:web:hashtag="#hehe" ]'))
|
|
14
19
|
self.len(1, await core.nodes('[ inet:web:hashtag="#foo·bar"]')) # note the interpunct
|
|
20
|
+
self.len(1, await core.nodes('[ inet:web:hashtag="#foo〜bar"]')) # note the wave dash
|
|
15
21
|
self.len(1, await core.nodes('[ inet:web:hashtag="#fo·o·······b·ar"]'))
|
|
16
22
|
with self.raises(s_exc.BadTypeValu):
|
|
17
23
|
await core.nodes('[ inet:web:hashtag="foo" ]')
|
|
24
|
+
|
|
18
25
|
with self.raises(s_exc.BadTypeValu):
|
|
19
|
-
await core.nodes('[ inet:web:hashtag="#foo
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
26
|
+
await core.nodes('[ inet:web:hashtag="#foo#bar" ]')
|
|
27
|
+
|
|
28
|
+
# All unicode whitespace from:
|
|
29
|
+
# https://www.compart.com/en/unicode/category/Zl
|
|
30
|
+
# https://www.compart.com/en/unicode/category/Zp
|
|
31
|
+
# https://www.compart.com/en/unicode/category/Zs
|
|
32
|
+
whitespace = [
|
|
33
|
+
'\u0020', '\u00a0', '\u1680', '\u2000', '\u2001', '\u2002', '\u2003', '\u2004',
|
|
34
|
+
'\u2005', '\u2006', '\u2007', '\u2008', '\u2009', '\u200a', '\u202f', '\u205f',
|
|
35
|
+
'\u3000', '\u2028', '\u2029',
|
|
36
|
+
]
|
|
37
|
+
for char in whitespace:
|
|
38
|
+
with self.raises(s_exc.BadTypeValu):
|
|
39
|
+
await core.callStorm(f'[ inet:web:hashtag="#foo{char}bar" ]')
|
|
40
|
+
|
|
41
|
+
with self.raises(s_exc.BadTypeValu):
|
|
42
|
+
await core.callStorm(f'[ inet:web:hashtag="#{char}bar" ]')
|
|
43
|
+
|
|
44
|
+
# These are allowed because strip=True
|
|
45
|
+
await core.callStorm(f'[ inet:web:hashtag="#foo{char}" ]')
|
|
46
|
+
await core.callStorm(f'[ inet:web:hashtag=" #foo{char}" ]')
|
|
24
47
|
|
|
25
48
|
nodes = await core.nodes('''
|
|
26
49
|
[ inet:web:instance=(foo,)
|
|
@@ -457,6 +480,7 @@ class InetModelTest(s_t_utils.SynTest):
|
|
|
457
480
|
:raw=((10), (20))
|
|
458
481
|
:src:txfiles={[ file:attachment=* :name=foo.exe ]}
|
|
459
482
|
:dst:txfiles={[ file:attachment=* :name=bar.exe ]}
|
|
483
|
+
:capture:host=*
|
|
460
484
|
)]'''
|
|
461
485
|
nodes = await core.nodes(q, opts={'vars': {'valu': valu, 'p': props}})
|
|
462
486
|
self.len(1, nodes)
|
|
@@ -500,11 +524,13 @@ class InetModelTest(s_t_utils.SynTest):
|
|
|
500
524
|
self.eq(node.get('src:rdp:hostname'), 'syncoder')
|
|
501
525
|
self.eq(node.get('src:rdp:keyboard:layout'), 'azerty')
|
|
502
526
|
self.eq(node.get('raw'), (10, 20))
|
|
527
|
+
self.nn(node.get('capture:host'))
|
|
503
528
|
self.len(2, await core.nodes('inet:flow -> crypto:x509:cert'))
|
|
504
529
|
self.len(1, await core.nodes('inet:flow :src:ssh:key -> crypto:key'))
|
|
505
530
|
self.len(1, await core.nodes('inet:flow :dst:ssh:key -> crypto:key'))
|
|
506
531
|
self.len(1, await core.nodes('inet:flow :src:txfiles -> file:attachment +:name=foo.exe'))
|
|
507
532
|
self.len(1, await core.nodes('inet:flow :dst:txfiles -> file:attachment +:name=bar.exe'))
|
|
533
|
+
self.len(1, await core.nodes('inet:flow :capture:host -> it:host'))
|
|
508
534
|
|
|
509
535
|
async def test_fqdn(self):
|
|
510
536
|
formname = 'inet:fqdn'
|
|
@@ -2746,6 +2772,7 @@ class InetModelTest(s_t_utils.SynTest):
|
|
|
2746
2772
|
q = '''
|
|
2747
2773
|
[
|
|
2748
2774
|
inet:email:message="*"
|
|
2775
|
+
:id="Woot-12345 "
|
|
2749
2776
|
:to=woot@woot.com
|
|
2750
2777
|
:from=visi@vertex.link
|
|
2751
2778
|
:replyto=root@root.com
|
|
@@ -2767,6 +2794,7 @@ class InetModelTest(s_t_utils.SynTest):
|
|
|
2767
2794
|
nodes = await core.nodes(q, opts={'vars': {'flow': flow}})
|
|
2768
2795
|
self.len(1, nodes)
|
|
2769
2796
|
|
|
2797
|
+
self.eq(nodes[0].get('id'), 'Woot-12345')
|
|
2770
2798
|
self.eq(nodes[0].get('cc'), ('baz@faz.org', 'foo@bar.com'))
|
|
2771
2799
|
self.eq(nodes[0].get('received:from:ipv6'), '::1')
|
|
2772
2800
|
self.eq(nodes[0].get('received:from:ipv4'), 0x01020304)
|
|
@@ -2847,6 +2875,7 @@ class InetModelTest(s_t_utils.SynTest):
|
|
|
2847
2875
|
nodes = await core.nodes('''
|
|
2848
2876
|
[ inet:egress=*
|
|
2849
2877
|
:host = *
|
|
2878
|
+
:host:iface = *
|
|
2850
2879
|
:client=1.2.3.4
|
|
2851
2880
|
:client:ipv6="::1"
|
|
2852
2881
|
]
|
|
@@ -2854,10 +2883,14 @@ class InetModelTest(s_t_utils.SynTest):
|
|
|
2854
2883
|
|
|
2855
2884
|
self.len(1, nodes)
|
|
2856
2885
|
self.nn(nodes[0].get('host'))
|
|
2886
|
+
self.nn(nodes[0].get('host:iface'))
|
|
2857
2887
|
self.eq(nodes[0].get('client'), 'tcp://1.2.3.4')
|
|
2858
2888
|
self.eq(nodes[0].get('client:ipv4'), 0x01020304)
|
|
2859
2889
|
self.eq(nodes[0].get('client:ipv6'), '::1')
|
|
2860
2890
|
|
|
2891
|
+
self.len(1, await core.nodes('inet:egress -> it:host'))
|
|
2892
|
+
self.len(1, await core.nodes('inet:egress -> inet:iface'))
|
|
2893
|
+
|
|
2861
2894
|
async def test_model_inet_tls_handshake(self):
|
|
2862
2895
|
|
|
2863
2896
|
async with self.getTestCore() as core:
|
|
@@ -2976,6 +3009,7 @@ class InetModelTest(s_t_utils.SynTest):
|
|
|
2976
3009
|
(inet:service:account=(blackout, account, vertex, slack)
|
|
2977
3010
|
:id=U7RN51U1J
|
|
2978
3011
|
:user=blackout
|
|
3012
|
+
:url=https://vertex.link/users/blackout
|
|
2979
3013
|
:email=blackout@vertex.link
|
|
2980
3014
|
:profile={ gen.ps.contact.email vertex.employee blackout@vertex.link }
|
|
2981
3015
|
:tenant={[ inet:service:tenant=({"id": "VS-31337"}) ]}
|
|
@@ -3003,6 +3037,7 @@ class InetModelTest(s_t_utils.SynTest):
|
|
|
3003
3037
|
self.eq(accounts[0].ndef, ('inet:service:account', s_common.guid(('blackout', 'account', 'vertex', 'slack'))))
|
|
3004
3038
|
self.eq(accounts[0].get('id'), 'U7RN51U1J')
|
|
3005
3039
|
self.eq(accounts[0].get('user'), 'blackout')
|
|
3040
|
+
self.eq(accounts[0].get('url'), 'https://vertex.link/users/blackout')
|
|
3006
3041
|
self.eq(accounts[0].get('email'), 'blackout@vertex.link')
|
|
3007
3042
|
self.eq(accounts[0].get('profile'), blckprof.ndef[1])
|
|
3008
3043
|
|
|
@@ -3207,6 +3242,7 @@ class InetModelTest(s_t_utils.SynTest):
|
|
|
3207
3242
|
:type=chat.group
|
|
3208
3243
|
:group=$devsiden
|
|
3209
3244
|
:public=$lib.false
|
|
3245
|
+
:repost=*
|
|
3210
3246
|
)
|
|
3211
3247
|
|
|
3212
3248
|
(inet:service:message=(blackout, visi, 1715856900000, vertex, slack)
|
|
@@ -3256,6 +3292,7 @@ class InetModelTest(s_t_utils.SynTest):
|
|
|
3256
3292
|
self.nn(node.get('place'))
|
|
3257
3293
|
self.eq(node.get('place:name'), 'nyc')
|
|
3258
3294
|
|
|
3295
|
+
self.nn(nodes[0].get('repost'))
|
|
3259
3296
|
self.eq(nodes[0].get('group'), devsgrp.ndef[1])
|
|
3260
3297
|
self.false(nodes[0].get('public'))
|
|
3261
3298
|
self.eq(nodes[0].get('type'), 'chat.group.')
|
|
@@ -3285,6 +3322,7 @@ class InetModelTest(s_t_utils.SynTest):
|
|
|
3285
3322
|
nodes = await core.nodes('inet:service:message:type:taxonomy=chat.channel -> inet:service:message')
|
|
3286
3323
|
self.len(1, nodes)
|
|
3287
3324
|
self.eq(nodes[0].ndef, ('inet:service:message', 'c0d64c559e2f42d57b37b558458c068b'))
|
|
3325
|
+
self.len(1, await core.nodes('inet:service:message:repost :repost -> inet:service:message'))
|
|
3288
3326
|
|
|
3289
3327
|
q = '''
|
|
3290
3328
|
[ inet:service:resource=(web, api, vertex, slack)
|
|
@@ -761,7 +761,7 @@ class InfotechModelTest(s_t_utils.SynTest):
|
|
|
761
761
|
'techniques': teqs,
|
|
762
762
|
'url': url0,
|
|
763
763
|
}
|
|
764
|
-
q = '''[(it:prod:soft=$valu :name=$p.name :type=$p.type :names=$p.names
|
|
764
|
+
q = '''[(it:prod:soft=$valu :id="Foo " :name=$p.name :type=$p.type :names=$p.names
|
|
765
765
|
:desc=$p.desc :desc:short=$p."desc:short" :author:org=$p."author:org" :author:email=$p."author:email"
|
|
766
766
|
:author:acct=$p."author:acct" :author:person=$p."author:person"
|
|
767
767
|
:techniques=$p.techniques :url=$p.url )]'''
|
|
@@ -769,6 +769,7 @@ class InfotechModelTest(s_t_utils.SynTest):
|
|
|
769
769
|
self.len(1, nodes)
|
|
770
770
|
node = nodes[0]
|
|
771
771
|
self.eq(node.ndef, ('it:prod:soft', prod0))
|
|
772
|
+
self.eq(node.get('id'), 'Foo')
|
|
772
773
|
self.eq(node.get('name'), 'balloon maker')
|
|
773
774
|
self.eq(node.get('desc'), "Pennywise's patented balloon blower upper")
|
|
774
775
|
self.eq(node.get('desc:short'), 'balloon blower')
|
|
@@ -786,6 +787,10 @@ class InfotechModelTest(s_t_utils.SynTest):
|
|
|
786
787
|
self.eq(node.get('url'), url0)
|
|
787
788
|
self.len(1, await core.nodes('it:prod:soft:name="balloon maker" -> it:prod:soft:taxonomy'))
|
|
788
789
|
self.len(2, await core.nodes('it:prod:softname="balloon maker" -> it:prod:soft -> it:prod:softname'))
|
|
790
|
+
|
|
791
|
+
self.len(1, nodes := await core.nodes('[ it:prod:soft=({"name": "clowns inc"}) ]'))
|
|
792
|
+
self.eq(node.ndef, nodes[0].ndef)
|
|
793
|
+
|
|
789
794
|
# it:prod:softver - this does test a bunch of property related callbacks
|
|
790
795
|
ver0 = s_common.guid()
|
|
791
796
|
url1 = 'https://vertex.link/products/balloonmaker/release_101-beta.exe'
|
|
@@ -819,6 +824,10 @@ class InfotechModelTest(s_t_utils.SynTest):
|
|
|
819
824
|
self.eq(node.get('url'), url1)
|
|
820
825
|
self.eq(node.get('name'), 'balloonmaker')
|
|
821
826
|
self.eq(node.get('desc'), 'makes balloons')
|
|
827
|
+
|
|
828
|
+
self.len(1, nodes := await core.nodes('[ it:prod:softver=({"name": "clowns inc"}) ]'))
|
|
829
|
+
self.eq(node.ndef, nodes[0].ndef)
|
|
830
|
+
|
|
822
831
|
# callback node creation checks
|
|
823
832
|
self.len(1, await core.nodes('it:dev:str=V1.0.1-beta+exp.sha.5114f85'))
|
|
824
833
|
self.len(1, await core.nodes('it:dev:str=amd64'))
|
synapse/tests/test_model_orgs.py
CHANGED
|
@@ -60,6 +60,9 @@ class OuModelTest(s_t_utils.SynTest):
|
|
|
60
60
|
self.eq(node.get('desc'), 'MyDesc')
|
|
61
61
|
self.eq(node.get('prev'), goal)
|
|
62
62
|
|
|
63
|
+
self.len(1, nodes := await core.nodes('[ ou:goal=({"name": "foo goal"}) ]'))
|
|
64
|
+
self.eq(node.ndef, nodes[0].ndef)
|
|
65
|
+
|
|
63
66
|
nodes = await core.nodes('[(ou:hasgoal=$valu :stated=$lib.true :window="2019,2020")]',
|
|
64
67
|
opts={'vars': {'valu': (org0, goal)}})
|
|
65
68
|
self.len(1, nodes)
|
|
@@ -69,12 +72,13 @@ class OuModelTest(s_t_utils.SynTest):
|
|
|
69
72
|
self.eq(node.get('stated'), True)
|
|
70
73
|
self.eq(node.get('window'), (1546300800000, 1577836800000))
|
|
71
74
|
|
|
75
|
+
altgoal = s_common.guid()
|
|
72
76
|
timeline = s_common.guid()
|
|
73
77
|
|
|
74
78
|
props = {
|
|
75
79
|
'org': org0,
|
|
76
80
|
'goal': goal,
|
|
77
|
-
'goals': (goal,),
|
|
81
|
+
'goals': (goal, altgoal),
|
|
78
82
|
'actors': (acto,),
|
|
79
83
|
'camptype': 'get.pizza',
|
|
80
84
|
'name': 'MyName',
|
|
@@ -103,7 +107,7 @@ class OuModelTest(s_t_utils.SynTest):
|
|
|
103
107
|
self.eq(node.get('tag'), 'cno.camp.31337')
|
|
104
108
|
self.eq(node.get('org'), org0)
|
|
105
109
|
self.eq(node.get('goal'), goal)
|
|
106
|
-
self.eq(node.get('goals'), (goal,))
|
|
110
|
+
self.eq(node.get('goals'), sorted((goal, altgoal)))
|
|
107
111
|
self.eq(node.get('actors'), (acto,))
|
|
108
112
|
self.eq(node.get('name'), 'myname')
|
|
109
113
|
self.eq(node.get('names'), ('bar', 'foo'))
|
|
@@ -120,6 +124,10 @@ class OuModelTest(s_t_utils.SynTest):
|
|
|
120
124
|
self.eq(node.get('mitre:attack:campaign'), 'C0011')
|
|
121
125
|
self.eq(node.get('slogan'), 'for the people')
|
|
122
126
|
|
|
127
|
+
opts = {'vars': {'altgoal': altgoal}}
|
|
128
|
+
self.len(1, nodes := await core.nodes('[ ou:campaign=({"name": "foo", "goal": $altgoal}) ]', opts=opts))
|
|
129
|
+
self.eq(node.ndef, nodes[0].ndef)
|
|
130
|
+
|
|
123
131
|
self.len(1, await core.nodes(f'ou:campaign={camp} :slogan -> lang:phrase'))
|
|
124
132
|
nodes = await core.nodes(f'ou:campaign={camp} -> it:mitre:attack:campaign')
|
|
125
133
|
self.len(1, nodes)
|
|
@@ -405,6 +413,9 @@ class OuModelTest(s_t_utils.SynTest):
|
|
|
405
413
|
self.eq(node.get('place'), place0)
|
|
406
414
|
self.eq(node.get('url'), 'http://arrowcon.org/2018')
|
|
407
415
|
|
|
416
|
+
self.len(1, nodes := await core.nodes('[ ou:conference=({"name": "arrcon18"}) ]'))
|
|
417
|
+
self.eq(node.ndef, nodes[0].ndef)
|
|
418
|
+
|
|
408
419
|
props = {
|
|
409
420
|
'arrived': '201803010800',
|
|
410
421
|
'departed': '201803021500',
|
|
@@ -870,6 +881,7 @@ class OuModelTest(s_t_utils.SynTest):
|
|
|
870
881
|
] '''
|
|
871
882
|
nodes = await core.nodes(q)
|
|
872
883
|
self.len(1, nodes)
|
|
884
|
+
node = nodes[0]
|
|
873
885
|
self.nn(nodes[0].get('reporter'))
|
|
874
886
|
self.eq('foo bar', nodes[0].get('name'))
|
|
875
887
|
self.eq('vertex', nodes[0].get('reporter:name'))
|
|
@@ -884,6 +896,9 @@ class OuModelTest(s_t_utils.SynTest):
|
|
|
884
896
|
self.len(3, nodes)
|
|
885
897
|
self.len(3, await core.nodes('ou:industryname=baz -> ou:industry -> ou:industryname'))
|
|
886
898
|
|
|
899
|
+
self.len(1, nodes := await core.nodes('[ ou:industry=({"name": "faz"}) ]'))
|
|
900
|
+
self.eq(node.ndef, nodes[0].ndef)
|
|
901
|
+
|
|
887
902
|
async def test_ou_opening(self):
|
|
888
903
|
|
|
889
904
|
async with self.getTestCore() as core:
|
|
@@ -60,6 +60,9 @@ class PsModelTest(s_t_utils.SynTest):
|
|
|
60
60
|
self.eq(node.get('names'), ['billy bob'])
|
|
61
61
|
self.eq(node.get('photo'), file0)
|
|
62
62
|
|
|
63
|
+
self.len(1, nodes := await core.nodes('[ ps:person=({"name": "billy bob"}) ]'))
|
|
64
|
+
self.eq(node.ndef, nodes[0].ndef)
|
|
65
|
+
|
|
63
66
|
props = {
|
|
64
67
|
'dob': '2000',
|
|
65
68
|
'img': file0,
|
|
@@ -147,9 +150,11 @@ class PsModelTest(s_t_utils.SynTest):
|
|
|
147
150
|
'id:numbers': (('*', 'asdf'), ('*', 'qwer')),
|
|
148
151
|
'users': ('visi', 'invisigoth'),
|
|
149
152
|
'crypto:address': 'btc/1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2',
|
|
153
|
+
'langs': (lang00 := s_common.guid(),),
|
|
150
154
|
}
|
|
151
155
|
opts = {'vars': {'valu': con0, 'p': props}}
|
|
152
156
|
q = '''[(ps:contact=$valu
|
|
157
|
+
:bio="I am ironman."
|
|
153
158
|
:org=$p.org :asof=$p.asof :person=$p.person
|
|
154
159
|
:place=$p.place :place:name=$p."place:name" :name=$p.name
|
|
155
160
|
:title=$p.title :orgname=$p.orgname :user=$p.user
|
|
@@ -165,7 +170,7 @@ class PsModelTest(s_t_utils.SynTest):
|
|
|
165
170
|
:birth:place:name=$p."birth:place:name"
|
|
166
171
|
:death:place=$p."death:place" :death:place:loc=$p."death:place:loc"
|
|
167
172
|
:death:place:name=$p."death:place:name"
|
|
168
|
-
:service:accounts=(*, *)
|
|
173
|
+
:service:accounts=(*, *) :langs=$p.langs
|
|
169
174
|
)]'''
|
|
170
175
|
nodes = await core.nodes(q, opts=opts)
|
|
171
176
|
self.len(1, nodes)
|
|
@@ -178,6 +183,7 @@ class PsModelTest(s_t_utils.SynTest):
|
|
|
178
183
|
self.eq(node.get('place'), place)
|
|
179
184
|
self.eq(node.get('place:name'), 'the shire')
|
|
180
185
|
self.eq(node.get('name'), 'tony stark')
|
|
186
|
+
self.eq(node.get('bio'), 'I am ironman.')
|
|
181
187
|
self.eq(node.get('title'), 'ceo')
|
|
182
188
|
self.eq(node.get('titles'), ('haha', 'hehe'))
|
|
183
189
|
self.eq(node.get('orgname'), 'stark industries, inc')
|
|
@@ -211,6 +217,22 @@ class PsModelTest(s_t_utils.SynTest):
|
|
|
211
217
|
self.len(1, await core.nodes('ps:contact :death:place -> geo:place'))
|
|
212
218
|
self.len(2, await core.nodes('ps:contact :service:accounts -> inet:service:account'))
|
|
213
219
|
|
|
220
|
+
opts = {
|
|
221
|
+
'vars': {
|
|
222
|
+
'ctor': {
|
|
223
|
+
'email': 'v@vtx.lk',
|
|
224
|
+
'id:number': node.get('id:numbers')[0],
|
|
225
|
+
'lang': lang00,
|
|
226
|
+
'name': 'vi',
|
|
227
|
+
'orgname': 'vertex',
|
|
228
|
+
'title': 'haha',
|
|
229
|
+
'user': 'invisigoth',
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
}
|
|
233
|
+
self.len(1, nodes := await core.nodes('[ ps:contact=$ctor ]', opts=opts))
|
|
234
|
+
self.eq(node.ndef, nodes[0].ndef)
|
|
235
|
+
|
|
214
236
|
nodes = await core.nodes('''[
|
|
215
237
|
ps:achievement=*
|
|
216
238
|
:award=*
|
synapse/tests/test_model_risk.py
CHANGED
|
@@ -253,6 +253,9 @@ class RiskModelTest(s_t_utils.SynTest):
|
|
|
253
253
|
self.len(1, await core.nodes('risk:attack :target -> ps:contact'))
|
|
254
254
|
self.len(1, await core.nodes('risk:attack :attacker -> ps:contact'))
|
|
255
255
|
|
|
256
|
+
self.len(1, nodes := await core.nodes('[ risk:vuln=({"name": "hehe"}) ]'))
|
|
257
|
+
self.eq(node.ndef, nodes[0].ndef)
|
|
258
|
+
|
|
256
259
|
node = await addNode(f'''[
|
|
257
260
|
risk:hasvuln={hasv}
|
|
258
261
|
:vuln={vuln}
|
|
@@ -399,6 +402,7 @@ class RiskModelTest(s_t_utils.SynTest):
|
|
|
399
402
|
]
|
|
400
403
|
''')
|
|
401
404
|
self.len(1, nodes)
|
|
405
|
+
node = nodes[0]
|
|
402
406
|
self.eq('vtx-apt1', nodes[0].get('name'))
|
|
403
407
|
self.eq('VTX-APT1', nodes[0].get('desc'))
|
|
404
408
|
self.eq(40, nodes[0].get('activity'))
|
|
@@ -424,12 +428,16 @@ class RiskModelTest(s_t_utils.SynTest):
|
|
|
424
428
|
self.len(1, await core.nodes('risk:threat:merged:isnow -> risk:threat'))
|
|
425
429
|
self.len(1, await core.nodes('risk:threat -> it:mitre:attack:group'))
|
|
426
430
|
|
|
431
|
+
self.len(1, nodes := await core.nodes('[ risk:threat=({"org:name": "comment crew"}) ]'))
|
|
432
|
+
self.eq(node.ndef, nodes[0].ndef)
|
|
433
|
+
|
|
427
434
|
nodes = await core.nodes('''[ risk:leak=*
|
|
428
435
|
:name="WikiLeaks ACME Leak"
|
|
429
436
|
:desc="WikiLeaks leaked ACME stuff."
|
|
430
437
|
:disclosed=20231102
|
|
431
438
|
:owner={ gen.ou.org.hq acme }
|
|
432
439
|
:leaker={ gen.ou.org.hq wikileaks }
|
|
440
|
+
:recipient={ gen.ou.org.hq everyone }
|
|
433
441
|
:type=public
|
|
434
442
|
:goal={[ ou:goal=* :name=publicity ]}
|
|
435
443
|
:compromise={[ risk:compromise=* :target={ gen.ou.org.hq acme } ]}
|
|
@@ -458,6 +466,7 @@ class RiskModelTest(s_t_utils.SynTest):
|
|
|
458
466
|
self.len(1, await core.nodes('risk:leak -> risk:leak:type:taxonomy'))
|
|
459
467
|
self.len(1, await core.nodes('risk:leak :owner -> ps:contact +:orgname=acme'))
|
|
460
468
|
self.len(1, await core.nodes('risk:leak :leaker -> ps:contact +:orgname=wikileaks'))
|
|
469
|
+
self.len(1, await core.nodes('risk:leak :recipient -> ps:contact +:orgname=everyone'))
|
|
461
470
|
self.len(1, await core.nodes('risk:leak -> ou:goal +:name=publicity'))
|
|
462
471
|
self.len(1, await core.nodes('risk:leak -> risk:compromise :target -> ps:contact +:orgname=acme'))
|
|
463
472
|
self.len(1, await core.nodes('risk:leak :reporter -> ou:org +:name=vertex'))
|
|
@@ -616,6 +625,7 @@ class RiskModelTest(s_t_utils.SynTest):
|
|
|
616
625
|
]
|
|
617
626
|
''')
|
|
618
627
|
self.len(1, nodes)
|
|
628
|
+
node = nodes[0]
|
|
619
629
|
self.nn(nodes[0].get('soft'))
|
|
620
630
|
|
|
621
631
|
self.nn(nodes[0].get('reporter'))
|
|
@@ -638,6 +648,9 @@ class RiskModelTest(s_t_utils.SynTest):
|
|
|
638
648
|
self.len(1, await core.nodes('risk:tool:software -> syn:tag'))
|
|
639
649
|
self.len(1, await core.nodes('risk:tool:software -> it:mitre:attack:software'))
|
|
640
650
|
|
|
651
|
+
self.len(1, nodes := await core.nodes('[ risk:tool:software=({"soft:name": "beacon"}) ]'))
|
|
652
|
+
self.eq(node.ndef, nodes[0].ndef)
|
|
653
|
+
|
|
641
654
|
nodes = await core.nodes('''
|
|
642
655
|
[ risk:vuln:soft:range=*
|
|
643
656
|
:vuln={[ risk:vuln=* :name=woot ]}
|
|
@@ -45,7 +45,7 @@ class HealthcheckTest(s_t_utils.SynTest):
|
|
|
45
45
|
await asyncio.sleep(0.6)
|
|
46
46
|
core.addHealthFunc(sleep)
|
|
47
47
|
outp.clear()
|
|
48
|
-
retn = await s_t_healthcheck.main(['-c', curl, '-t', '0.
|
|
48
|
+
retn = await s_t_healthcheck.main(['-c', curl, '-t', '0.4'], outp)
|
|
49
49
|
self.eq(retn, 1)
|
|
50
50
|
resp = json.loads(str(outp))
|
|
51
51
|
self.eq(resp.get('components')[0].get('name'), 'error')
|
|
@@ -58,7 +58,7 @@ class HealthcheckTest(s_t_utils.SynTest):
|
|
|
58
58
|
_, port = await core.dmon.listen('tcp://127.0.0.1:0')
|
|
59
59
|
root = await core.auth.getUserByName('root')
|
|
60
60
|
await root.setPasswd('secret')
|
|
61
|
-
retn = await s_t_healthcheck.main(['-c', f'tcp://root:newp@127.0.0.1:{port}/cortex', '-t', '0.
|
|
61
|
+
retn = await s_t_healthcheck.main(['-c', f'tcp://root:newp@127.0.0.1:{port}/cortex', '-t', '0.4'], outp)
|
|
62
62
|
self.eq(retn, 1)
|
|
63
63
|
resp = json.loads(str(outp))
|
|
64
64
|
self.eq(resp.get('components')[0].get('name'), 'error')
|
|
@@ -70,7 +70,7 @@ class HealthcheckTest(s_t_utils.SynTest):
|
|
|
70
70
|
|
|
71
71
|
logger.info('Checking without perms')
|
|
72
72
|
outp.clear()
|
|
73
|
-
retn = await s_t_healthcheck.main(['-c', f'tcp://visi:secret@127.0.0.1:{port}/cortex', '-t', '0.
|
|
73
|
+
retn = await s_t_healthcheck.main(['-c', f'tcp://visi:secret@127.0.0.1:{port}/cortex', '-t', '0.4'], outp)
|
|
74
74
|
self.eq(retn, 1)
|
|
75
75
|
resp = json.loads(str(outp))
|
|
76
76
|
self.eq(resp.get('components')[0].get('name'), 'error')
|
|
@@ -83,7 +83,7 @@ class HealthcheckTest(s_t_utils.SynTest):
|
|
|
83
83
|
await core.fini()
|
|
84
84
|
await asyncio.sleep(0)
|
|
85
85
|
outp.clear()
|
|
86
|
-
retn = await s_t_healthcheck.main(['-c', curl, '-t', '0.
|
|
86
|
+
retn = await s_t_healthcheck.main(['-c', curl, '-t', '0.4'], outp)
|
|
87
87
|
self.eq(retn, 1)
|
|
88
88
|
resp = json.loads(str(outp))
|
|
89
89
|
self.eq(resp.get('components')[0].get('name'), 'error')
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import os
|
|
2
|
+
import sys
|
|
3
|
+
import signal
|
|
4
|
+
import asyncio
|
|
5
|
+
import multiprocessing
|
|
6
|
+
|
|
2
7
|
import synapse.tests.utils as s_test
|
|
3
8
|
|
|
4
9
|
from prompt_toolkit.document import Document
|
|
@@ -6,10 +11,49 @@ from prompt_toolkit.completion import Completion, CompleteEvent
|
|
|
6
11
|
|
|
7
12
|
import synapse.exc as s_exc
|
|
8
13
|
import synapse.common as s_common
|
|
14
|
+
import synapse.telepath as s_telepath
|
|
15
|
+
|
|
16
|
+
import synapse.lib.coro as s_coro
|
|
9
17
|
import synapse.lib.output as s_output
|
|
10
18
|
import synapse.lib.msgpack as s_msgpack
|
|
11
19
|
import synapse.tools.storm as s_t_storm
|
|
12
20
|
|
|
21
|
+
def run_cli_till_print(url, evt1):
|
|
22
|
+
'''
|
|
23
|
+
Run the stormCLI until we get a print mesg then set the event.
|
|
24
|
+
|
|
25
|
+
This is a Process target.
|
|
26
|
+
'''
|
|
27
|
+
async def main():
|
|
28
|
+
outp = s_output.OutPutStr() # Capture output instead of sending it to stdout
|
|
29
|
+
async with await s_telepath.openurl(url) as proxy:
|
|
30
|
+
async with await s_t_storm.StormCli.anit(proxy, outp=outp) as scli:
|
|
31
|
+
cmdqueue = asyncio.Queue()
|
|
32
|
+
await cmdqueue.put('while (true) { $lib.print(go) $lib.time.sleep(1) }')
|
|
33
|
+
await cmdqueue.put('!quit')
|
|
34
|
+
|
|
35
|
+
async def fake_prompt():
|
|
36
|
+
return await cmdqueue.get()
|
|
37
|
+
|
|
38
|
+
scli.prompt = fake_prompt
|
|
39
|
+
|
|
40
|
+
d = {'evt1': False}
|
|
41
|
+
async def onmesg(event):
|
|
42
|
+
if d.get('evt1'):
|
|
43
|
+
return
|
|
44
|
+
mesg = event[1].get('mesg')
|
|
45
|
+
if mesg[0] != 'print':
|
|
46
|
+
return
|
|
47
|
+
evt1.set()
|
|
48
|
+
d['evt1'] = True
|
|
49
|
+
|
|
50
|
+
with scli.onWith('storm:mesg', onmesg):
|
|
51
|
+
await scli.addSignalHandlers()
|
|
52
|
+
await scli.runCmdLoop()
|
|
53
|
+
|
|
54
|
+
asyncio.run(main())
|
|
55
|
+
sys.exit(137)
|
|
56
|
+
|
|
13
57
|
class StormCliTest(s_test.SynTest):
|
|
14
58
|
|
|
15
59
|
async def test_tools_storm(self):
|
|
@@ -378,3 +422,54 @@ class StormCliTest(s_test.SynTest):
|
|
|
378
422
|
),
|
|
379
423
|
vals
|
|
380
424
|
)
|
|
425
|
+
|
|
426
|
+
async def test_storm_cmdloop_interrupt(self):
|
|
427
|
+
'''
|
|
428
|
+
Test interrupting a long-running query in the command loop
|
|
429
|
+
'''
|
|
430
|
+
async with self.getTestCore() as core:
|
|
431
|
+
|
|
432
|
+
async with core.getLocalProxy() as proxy:
|
|
433
|
+
|
|
434
|
+
outp = s_test.TstOutPut()
|
|
435
|
+
async with await s_t_storm.StormCli.anit(proxy, outp=outp) as scli:
|
|
436
|
+
|
|
437
|
+
cmdqueue = asyncio.Queue()
|
|
438
|
+
await cmdqueue.put('while (true) { $lib.time.sleep(1) }')
|
|
439
|
+
await cmdqueue.put('!quit')
|
|
440
|
+
|
|
441
|
+
async def fake_prompt():
|
|
442
|
+
return await cmdqueue.get()
|
|
443
|
+
scli.prompt = fake_prompt
|
|
444
|
+
|
|
445
|
+
cmdloop_task = asyncio.create_task(scli.runCmdLoop())
|
|
446
|
+
await asyncio.sleep(0.1)
|
|
447
|
+
|
|
448
|
+
if scli.cmdtask is not None:
|
|
449
|
+
scli.cmdtask.cancel()
|
|
450
|
+
|
|
451
|
+
await cmdloop_task
|
|
452
|
+
|
|
453
|
+
outp.expect('<ctrl-c>')
|
|
454
|
+
outp.expect('o/')
|
|
455
|
+
self.true(scli.isfini)
|
|
456
|
+
|
|
457
|
+
async def test_storm_cmdloop_sigint(self):
|
|
458
|
+
'''
|
|
459
|
+
Test interrupting a long-running query in the command loop with a process target and SIGINT.
|
|
460
|
+
'''
|
|
461
|
+
|
|
462
|
+
async with self.getTestCore() as core:
|
|
463
|
+
url = core.getLocalUrl()
|
|
464
|
+
|
|
465
|
+
ctx = multiprocessing.get_context('spawn')
|
|
466
|
+
|
|
467
|
+
evt1 = ctx.Event()
|
|
468
|
+
|
|
469
|
+
proc = ctx.Process(target=run_cli_till_print, args=(url, evt1,))
|
|
470
|
+
proc.start()
|
|
471
|
+
|
|
472
|
+
self.true(await s_coro.executor(evt1.wait, timeout=30))
|
|
473
|
+
os.kill(proc.pid, signal.SIGINT)
|
|
474
|
+
proc.join(timeout=30)
|
|
475
|
+
self.eq(proc.exitcode, 137)
|
synapse/tests/test_utils.py
CHANGED
|
@@ -244,24 +244,6 @@ class TestUtils(s_t_utils.SynTest):
|
|
|
244
244
|
with self.raises(AssertionError):
|
|
245
245
|
self.stormHasNoWarnErr(msgs)
|
|
246
246
|
|
|
247
|
-
async def test_stable_uids(self):
|
|
248
|
-
with self.withStableUids():
|
|
249
|
-
guid = s_common.guid()
|
|
250
|
-
self.eq('000000', guid[:6])
|
|
251
|
-
guid2 = s_common.guid()
|
|
252
|
-
self.ne(guid, guid2)
|
|
253
|
-
|
|
254
|
-
guid = s_common.guid(42)
|
|
255
|
-
self.ne('000000', guid[:6])
|
|
256
|
-
|
|
257
|
-
buid = s_common.buid()
|
|
258
|
-
self.eq(b'\00\00\00\00\00\00', buid[:6])
|
|
259
|
-
buid2 = s_common.buid()
|
|
260
|
-
self.ne(buid, buid2)
|
|
261
|
-
|
|
262
|
-
buid = s_common.buid(42)
|
|
263
|
-
self.ne(b'\00\00\00\00\00\00', buid[:6])
|
|
264
|
-
|
|
265
247
|
def test_utils_certdir(self):
|
|
266
248
|
oldcertdirn = s_certdir.getCertDirn()
|
|
267
249
|
oldcertdir = s_certdir.getCertDir()
|
|
@@ -297,3 +279,20 @@ class TestUtils(s_t_utils.SynTest):
|
|
|
297
279
|
# Patch is removed and singleton behavior is restored
|
|
298
280
|
self.true(oldcertdir is s_certdir.getCertDir())
|
|
299
281
|
self.eq(oldcertdirn, s_certdir.getCertDirn())
|
|
282
|
+
|
|
283
|
+
async def test_checknode(self):
|
|
284
|
+
async with self.getTestCore() as core:
|
|
285
|
+
nodes = await core.nodes('[test:comp=(1, test)]')
|
|
286
|
+
self.len(1, nodes)
|
|
287
|
+
self.checkNode(nodes[0], (('test:comp', (1, 'test')), {'hehe': 1, 'haha': 'test'}))
|
|
288
|
+
with self.raises(AssertionError):
|
|
289
|
+
self.checkNode(nodes[0], (('test:comp', (1, 'newp')), {'hehe': 1, 'haha': 'test'}))
|
|
290
|
+
with self.raises(AssertionError):
|
|
291
|
+
self.checkNode(nodes[0], (('test:comp', (1, 'test')), {'hehe': 1, 'haha': 'newp'}))
|
|
292
|
+
with self.getAsyncLoggerStream('synapse.tests.utils', 'untested properties') as stream:
|
|
293
|
+
self.checkNode(nodes[0], (('test:comp', (1, 'test')), {'hehe': 1}))
|
|
294
|
+
self.true(await stream.wait(timeout=12))
|
|
295
|
+
|
|
296
|
+
await self.checkNodes(core, [('test:comp', (1, 'test')),])
|
|
297
|
+
with self.raises(AssertionError):
|
|
298
|
+
await self.checkNodes(core, [('test:comp', (1, 'newp')),])
|
synapse/tests/utils.py
CHANGED
|
@@ -1005,8 +1005,6 @@ class SynTest(unittest.TestCase):
|
|
|
1005
1005
|
'''
|
|
1006
1006
|
def __init__(self, *args, **kwargs):
|
|
1007
1007
|
unittest.TestCase.__init__(self, *args, **kwargs)
|
|
1008
|
-
self._NextBuid = 0
|
|
1009
|
-
self._NextGuid = 0
|
|
1010
1008
|
|
|
1011
1009
|
for s in dir(self):
|
|
1012
1010
|
attr = getattr(self, s, None)
|
|
@@ -2377,39 +2375,6 @@ class SynTest(unittest.TestCase):
|
|
|
2377
2375
|
|
|
2378
2376
|
yield hive
|
|
2379
2377
|
|
|
2380
|
-
def stablebuid(self, valu=None):
|
|
2381
|
-
'''
|
|
2382
|
-
A stable buid generation for testing purposes
|
|
2383
|
-
'''
|
|
2384
|
-
if valu is None:
|
|
2385
|
-
retn = self._NextBuid.to_bytes(32, 'big')
|
|
2386
|
-
self._NextBuid += 1
|
|
2387
|
-
return retn
|
|
2388
|
-
|
|
2389
|
-
byts = s_msgpack.en(valu)
|
|
2390
|
-
return hashlib.sha256(byts).digest()
|
|
2391
|
-
|
|
2392
|
-
def stableguid(self, valu=None):
|
|
2393
|
-
'''
|
|
2394
|
-
A stable guid generation for testing purposes
|
|
2395
|
-
'''
|
|
2396
|
-
if valu is None:
|
|
2397
|
-
retn = s_common.ehex(self._NextGuid.to_bytes(16, 'big'))
|
|
2398
|
-
self._NextGuid += 1
|
|
2399
|
-
return retn
|
|
2400
|
-
|
|
2401
|
-
byts = s_msgpack.en(valu)
|
|
2402
|
-
return hashlib.md5(byts, usedforsecurity=False).hexdigest()
|
|
2403
|
-
|
|
2404
|
-
@contextlib.contextmanager
|
|
2405
|
-
def withStableUids(self):
|
|
2406
|
-
'''
|
|
2407
|
-
A context manager that generates guids and buids in sequence so that successive test runs use the same
|
|
2408
|
-
data
|
|
2409
|
-
'''
|
|
2410
|
-
with mock.patch('synapse.common.guid', self.stableguid), mock.patch('synapse.common.buid', self.stablebuid):
|
|
2411
|
-
yield
|
|
2412
|
-
|
|
2413
2378
|
async def runCoreNodes(self, core, query, opts=None):
|
|
2414
2379
|
'''
|
|
2415
2380
|
Run a storm query through a Cortex as a SchedCoro and return the results.
|