synapse 2.152.0__py311-none-any.whl → 2.154.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.

Files changed (87) hide show
  1. synapse/axon.py +19 -16
  2. synapse/cortex.py +203 -15
  3. synapse/exc.py +0 -2
  4. synapse/lib/ast.py +42 -23
  5. synapse/lib/autodoc.py +2 -2
  6. synapse/lib/cache.py +16 -1
  7. synapse/lib/cell.py +5 -5
  8. synapse/lib/httpapi.py +198 -2
  9. synapse/lib/layer.py +5 -2
  10. synapse/lib/modelrev.py +36 -3
  11. synapse/lib/node.py +2 -5
  12. synapse/lib/parser.py +1 -1
  13. synapse/lib/schemas.py +51 -0
  14. synapse/lib/snap.py +10 -0
  15. synapse/lib/storm.lark +24 -4
  16. synapse/lib/storm.py +98 -19
  17. synapse/lib/storm_format.py +1 -1
  18. synapse/lib/stormhttp.py +11 -4
  19. synapse/lib/stormlib/auth.py +16 -2
  20. synapse/lib/stormlib/backup.py +1 -0
  21. synapse/lib/stormlib/basex.py +2 -0
  22. synapse/lib/stormlib/cell.py +7 -0
  23. synapse/lib/stormlib/compression.py +3 -0
  24. synapse/lib/stormlib/cortex.py +1168 -0
  25. synapse/lib/stormlib/ethereum.py +1 -0
  26. synapse/lib/stormlib/graph.py +2 -0
  27. synapse/lib/stormlib/hashes.py +5 -0
  28. synapse/lib/stormlib/hex.py +6 -0
  29. synapse/lib/stormlib/infosec.py +6 -1
  30. synapse/lib/stormlib/ipv6.py +1 -0
  31. synapse/lib/stormlib/iters.py +58 -1
  32. synapse/lib/stormlib/json.py +5 -0
  33. synapse/lib/stormlib/mime.py +1 -0
  34. synapse/lib/stormlib/model.py +19 -3
  35. synapse/lib/stormlib/modelext.py +1 -0
  36. synapse/lib/stormlib/notifications.py +2 -0
  37. synapse/lib/stormlib/pack.py +2 -0
  38. synapse/lib/stormlib/random.py +1 -0
  39. synapse/lib/stormlib/smtp.py +0 -7
  40. synapse/lib/stormlib/stats.py +223 -0
  41. synapse/lib/stormlib/stix.py +8 -0
  42. synapse/lib/stormlib/storm.py +1 -0
  43. synapse/lib/stormlib/version.py +3 -0
  44. synapse/lib/stormlib/xml.py +3 -0
  45. synapse/lib/stormlib/yaml.py +2 -0
  46. synapse/lib/stormtypes.py +250 -170
  47. synapse/lib/trigger.py +180 -4
  48. synapse/lib/types.py +1 -1
  49. synapse/lib/version.py +2 -2
  50. synapse/lib/view.py +55 -6
  51. synapse/models/inet.py +21 -6
  52. synapse/models/orgs.py +48 -2
  53. synapse/models/risk.py +126 -2
  54. synapse/models/syn.py +6 -0
  55. synapse/tests/files/stormpkg/badapidef.yaml +13 -0
  56. synapse/tests/files/stormpkg/storm/modules/apimod +10 -0
  57. synapse/tests/files/stormpkg/testpkg.yaml +23 -0
  58. synapse/tests/test_axon.py +7 -2
  59. synapse/tests/test_cortex.py +231 -35
  60. synapse/tests/test_lib_ast.py +138 -43
  61. synapse/tests/test_lib_autodoc.py +1 -1
  62. synapse/tests/test_lib_modelrev.py +9 -0
  63. synapse/tests/test_lib_node.py +55 -0
  64. synapse/tests/test_lib_storm.py +14 -1
  65. synapse/tests/test_lib_stormhttp.py +65 -6
  66. synapse/tests/test_lib_stormlib_auth.py +12 -3
  67. synapse/tests/test_lib_stormlib_cortex.py +1327 -0
  68. synapse/tests/test_lib_stormlib_iters.py +116 -0
  69. synapse/tests/test_lib_stormlib_stats.py +187 -0
  70. synapse/tests/test_lib_stormlib_storm.py +8 -0
  71. synapse/tests/test_lib_stormsvc.py +24 -1
  72. synapse/tests/test_lib_stormtypes.py +124 -69
  73. synapse/tests/test_lib_trigger.py +315 -0
  74. synapse/tests/test_lib_view.py +1 -2
  75. synapse/tests/test_model_base.py +26 -0
  76. synapse/tests/test_model_inet.py +22 -0
  77. synapse/tests/test_model_orgs.py +28 -0
  78. synapse/tests/test_model_risk.py +73 -0
  79. synapse/tests/test_tools_autodoc.py +25 -0
  80. synapse/tests/test_tools_genpkg.py +9 -3
  81. synapse/tests/utils.py +39 -0
  82. synapse/tools/autodoc.py +42 -2
  83. {synapse-2.152.0.dist-info → synapse-2.154.0.dist-info}/METADATA +2 -2
  84. {synapse-2.152.0.dist-info → synapse-2.154.0.dist-info}/RECORD +87 -79
  85. {synapse-2.152.0.dist-info → synapse-2.154.0.dist-info}/WHEEL +1 -1
  86. {synapse-2.152.0.dist-info → synapse-2.154.0.dist-info}/LICENSE +0 -0
  87. {synapse-2.152.0.dist-info → synapse-2.154.0.dist-info}/top_level.txt +0 -0
synapse/models/risk.py CHANGED
@@ -99,9 +99,22 @@ class RiskModule(s_module.CoreModule):
99
99
  }),
100
100
 
101
101
  ('risk:threat:type:taxonomy', ('taxonomy', {}), {
102
- 'doc': 'A taxonomy of threat types.',
103
102
  'interfaces': ('taxonomy',),
104
- }),
103
+ 'doc': 'A taxonomy of threat types.'}),
104
+
105
+ ('risk:leak', ('guid', {}), {
106
+ 'doc': 'An event where information was disclosed without permission.'}),
107
+
108
+ ('risk:leak:type:taxonomy', ('taxonomy', {}), {
109
+ 'interfaces': ('taxonomy',),
110
+ 'doc': 'A taxonomy of leak event types.'}),
111
+
112
+ ('risk:extortion', ('guid', {}), {
113
+ 'doc': 'An event where an attacker attempted to extort a victim.'}),
114
+
115
+ ('risk:extortion:type:taxonomy', ('taxonomy', {}), {
116
+ 'interfaces': ('taxonomy',),
117
+ 'doc': 'A taxonomy of extortion event types.'}),
105
118
  ),
106
119
  'edges': (
107
120
  # some explicit examples...
@@ -141,6 +154,12 @@ class RiskModule(s_module.CoreModule):
141
154
  'doc': 'The target node was stolen or copied as a result of the compromise.'}),
142
155
  (('risk:mitigation', 'addresses', 'ou:technique'), {
143
156
  'doc': 'The mitigation addresses the technique.'}),
157
+
158
+ (('risk:leak', 'leaked', None), {
159
+ 'doc': 'The leak included the disclosure of the target node.'}),
160
+
161
+ (('risk:extortion', 'leveraged', None), {
162
+ 'doc': 'The extortion event was based on attacker access to the target node.'}),
144
163
  ),
145
164
  'forms': (
146
165
 
@@ -596,6 +615,9 @@ class RiskModule(s_module.CoreModule):
596
615
 
597
616
  ('ext:id', ('str', {}), {
598
617
  'doc': 'An external identifier for the alert.'}),
618
+
619
+ ('host', ('it:host', {}), {
620
+ 'doc': 'The host which generated the alert.'}),
599
621
  )),
600
622
  ('risk:compromisetype', {}, ()),
601
623
  ('risk:compromise', {}, (
@@ -612,6 +634,12 @@ class RiskModule(s_module.CoreModule):
612
634
  ('reporter:name', ('ou:name', {}), {
613
635
  'doc': 'The name of the organization reporting on the compromise.'}),
614
636
 
637
+ ('ext:id', ('str', {}), {
638
+ 'doc': 'An external unique ID for the compromise.'}),
639
+
640
+ ('url', ('inet:url', {}), {
641
+ 'doc': 'A URL which documents the compromise.'}),
642
+
615
643
  ('type', ('risk:compromisetype', {}), {
616
644
  'ex': 'cno.breach',
617
645
  'doc': 'A type for the compromise, as a taxonomy entry.'}),
@@ -815,6 +843,102 @@ class RiskModule(s_module.CoreModule):
815
843
  'doc': 'An external unique ID for the attack.'}),
816
844
 
817
845
  )),
846
+
847
+ ('risk:leak:type:taxonomy', {}, ()),
848
+ ('risk:leak', {}, (
849
+
850
+ ('name', ('str', {'lower': True, 'onespace': True}), {
851
+ 'doc': 'A simple name for the leak event.'}),
852
+
853
+ ('desc', ('str', {}), {
854
+ 'disp': {'hint': 'text'},
855
+ 'doc': 'A description of the leak event.'}),
856
+
857
+ ('reporter', ('ou:org', {}), {
858
+ 'doc': 'The organization reporting on the leak event.'}),
859
+
860
+ ('reporter:name', ('ou:name', {}), {
861
+ 'doc': 'The name of the organization reporting on the leak event.'}),
862
+
863
+ ('disclosed', ('time', {}), {
864
+ 'doc': 'The time the leaked information was disclosed.'}),
865
+
866
+ ('owner', ('ps:contact', {}), {
867
+ 'doc': 'The owner of the leaked information.'}),
868
+
869
+ ('leaker', ('ps:contact', {}), {
870
+ 'doc': 'The identity which leaked the information.'}),
871
+
872
+ ('type', ('risk:leak:type:taxonomy', {}), {
873
+ 'doc': 'A type taxonomy for the leak.'}),
874
+
875
+ ('goal', ('ou:goal', {}), {
876
+ 'doc': 'The goal of the leaker in disclosing the information.'}),
877
+
878
+ ('compromise', ('risk:compromise', {}), {
879
+ 'doc': 'The compromise which allowed the leaker access to the information.'}),
880
+
881
+ ('public', ('bool', {}), {
882
+ 'doc': 'Set to true if the leaked information was made publicly available.'}),
883
+
884
+ ('public:url', ('inet:url', {}), {
885
+ 'doc': 'The URL where the leaked information was made publicly available.'}),
886
+
887
+ )),
888
+
889
+ ('risk:extortion:type:taxonomy', {}, ()),
890
+ ('risk:extortion', {}, (
891
+
892
+ ('name', ('str', {'lower': True, 'onespace': True}), {
893
+ 'doc': 'A name for the extortion event.'}),
894
+
895
+ ('desc', ('str', {}), {
896
+ 'disp': {'hint': 'text'},
897
+ 'doc': 'A description of the extortion event.'}),
898
+
899
+ ('reporter', ('ou:org', {}), {
900
+ 'doc': 'The organization reporting on the extortion event.'}),
901
+
902
+ ('reporter:name', ('ou:name', {}), {
903
+ 'doc': 'The name of the organization reporting on the extortion event.'}),
904
+
905
+ ('demanded', ('time', {}), {
906
+ 'doc': 'The time that the attacker made their demands.'}),
907
+
908
+ ('goal', ('ou:goal', {}), {
909
+ 'doc': 'The goal of the attacker in extorting the victim.'}),
910
+
911
+ ('type', ('risk:extortion:type:taxonomy', {}), {
912
+ 'doc': 'A type taxonomy for the extortion event.'}),
913
+
914
+ ('attacker', ('ps:contact', {}), {
915
+ 'doc': 'The extortion attacker identity.'}),
916
+
917
+ ('target', ('ps:contact', {}), {
918
+ 'doc': 'The extortion target identity.'}),
919
+
920
+ ('success', ('bool', {}), {
921
+ 'doc': 'Set to true if the victim met the attackers demands.'}),
922
+
923
+ ('enacted', ('bool', {}), {
924
+ 'doc': 'Set to true if attacker carried out the threat.'}),
925
+
926
+ ('public', ('bool', {}), {
927
+ 'doc': 'Set to true if the attacker publicly announced the extortion.'}),
928
+
929
+ ('public:url', ('inet:url', {}), {
930
+ 'doc': 'The URL where the attacker publicly announced the extortion.'}),
931
+
932
+ ('compromise', ('risk:compromise', {}), {
933
+ 'doc': 'The compromise which allowed the attacker to extort the target.'}),
934
+
935
+ ('demanded:payment:price', ('econ:price', {}), {
936
+ 'doc': 'The payment price which was demanded.'}),
937
+
938
+ ('demanded:payment:currency', ('econ:currency', {}), {
939
+ 'doc': 'The currency in which payment was demanded.'}),
940
+
941
+ )),
818
942
  ),
819
943
  }
820
944
  name = 'risk'
synapse/models/syn.py CHANGED
@@ -236,6 +236,12 @@ class SynModule(s_module.CoreModule):
236
236
  ('form', ('str', {'lower': True, 'strip': True}), {
237
237
  'doc': 'Form the trigger is watching for.'
238
238
  }),
239
+ ('verb', ('str', {'lower': True, 'strip': True}), {
240
+ 'doc': 'Edge verb the trigger is watching for.'
241
+ }),
242
+ ('n2form', ('str', {'lower': True, 'strip': True}), {
243
+ 'doc': 'N2 form the trigger is watching for.'
244
+ }),
239
245
  ('prop', ('str', {'lower': True, 'strip': True}), {
240
246
  'doc': 'Property the trigger is watching for.'
241
247
  }),
@@ -0,0 +1,13 @@
1
+ name: testpkg
2
+ version: 0.0.1
3
+
4
+ modules:
5
+ - name: apimod
6
+ apidefs:
7
+ - name: status
8
+ desc: Get the status of the foo.
9
+ type:
10
+ type: function
11
+ returns:
12
+ type: 42
13
+ desc: A status dictionary.
@@ -0,0 +1,10 @@
1
+
2
+ function search(text, mintime="-30days") {
3
+ for $t in $text {
4
+ [ it:devstr=$t ]
5
+ }
6
+ }
7
+
8
+ function status() {
9
+ return(({"ok": true}))
10
+ }
@@ -7,6 +7,29 @@ logo:
7
7
 
8
8
  modules:
9
9
  - name: testmod
10
+ - name: apimod
11
+ apidefs:
12
+ - name: search
13
+ desc: |
14
+ Execute a search
15
+
16
+ This API will foo the bar.
17
+ type:
18
+ type: function
19
+ args:
20
+ - { name: text, type: str, desc: "The text." }
21
+ - { name: mintime, type: [str, int], desc: "The mintime.", default: "-30days" }
22
+ returns:
23
+ name: yields
24
+ type: node
25
+ desc: Yields it:dev:str nodes.
26
+ - name: status
27
+ desc: Get the status of the foo.
28
+ type:
29
+ type: function
30
+ returns:
31
+ type: dict
32
+ desc: A status dictionary.
10
33
 
11
34
  external_modules:
12
35
  - name: testext
@@ -278,8 +278,14 @@ class AxonTest(s_t_utils.SynTest):
278
278
  (lsize, l256) = await axon.put(linesbuf)
279
279
  (jsize, j256) = await axon.put(jsonsbuf)
280
280
  (bsize, b256) = await axon.put(b'\n'.join((jsonsbuf, linesbuf)))
281
+ (binsize, bin256) = await axon.put(bin_buf)
282
+
281
283
  lines = [item async for item in axon.readlines(s_common.ehex(l256))]
282
284
  self.eq(('asdf', '', 'qwer'), lines)
285
+ lines = [item async for item in axon.readlines(s_common.ehex(bin256))] # Default is errors=ignore
286
+ self.eq(lines, ['/$A\x00_v4\x1b'])
287
+ lines = [item async for item in axon.readlines(s_common.ehex(bin256), errors='replace')]
288
+ self.eq(lines, ['�/$�A�\x00_�v4��\x1b'])
283
289
  jsons = [item async for item in axon.jsonlines(s_common.ehex(j256))]
284
290
  self.eq(({'foo': 'bar'}, {'baz': 'faz'}), jsons)
285
291
  jsons = []
@@ -288,9 +294,8 @@ class AxonTest(s_t_utils.SynTest):
288
294
  jsons.append(item)
289
295
  self.eq(({'foo': 'bar'}, {'baz': 'faz'}), jsons)
290
296
 
291
- binsize, bin256 = await axon.put(bin_buf)
292
297
  with self.raises(s_exc.BadDataValu):
293
- lines = [item async for item in axon.readlines(s_common.ehex(bin256))]
298
+ lines = [item async for item in axon.readlines(s_common.ehex(bin256), errors=None)]
294
299
 
295
300
  with self.raises(s_exc.NoSuchFile):
296
301
  lines = [item async for item in axon.readlines(s_common.ehex(newphash))]
@@ -2046,8 +2046,10 @@ class CortexTest(s_t_utils.SynTest):
2046
2046
  await core.nodes('test:str +test:str@=2018')
2047
2047
  with self.raises(s_exc.NoSuchCmpr):
2048
2048
  await core.nodes('test:str +test:str:tick*near=newp')
2049
- with self.raises(s_exc.BadTypeValu):
2049
+ with self.raises(s_exc.NoSuchCmpr):
2050
2050
  await core.nodes('test:str +#test*near=newp')
2051
+ with self.raises(s_exc.BadTypeValu):
2052
+ await core.nodes('test:str +#test*in=newp')
2051
2053
  with self.raises(s_exc.BadSyntax):
2052
2054
  await core.nodes('test:str -> # } limit 10')
2053
2055
  with self.raises(s_exc.BadSyntax):
@@ -6115,55 +6117,71 @@ class CortexBasicTest(s_t_utils.SynTest):
6115
6117
  self.true(layr.lockmemory)
6116
6118
 
6117
6119
  async def test_cortex_storm_lib_dmon(self):
6118
- async with self.getTestCoreAndProxy() as (core, prox):
6119
- nodes = await core.nodes('''
6120
6120
 
6121
- $lib.print(hi)
6121
+ with self.getTestDir() as dirn:
6122
6122
 
6123
- $tx = $lib.queue.add(tx)
6124
- $rx = $lib.queue.add(rx)
6123
+ async with self.getTestCoreAndProxy(dirn=dirn) as (core, prox):
6124
+ nodes = await core.nodes('''
6125
6125
 
6126
- $ddef = $lib.dmon.add(${
6126
+ $lib.print(hi)
6127
6127
 
6128
- $rx = $lib.queue.get(tx)
6129
- $tx = $lib.queue.get(rx)
6128
+ $tx = $lib.queue.add(tx)
6129
+ $rx = $lib.queue.add(rx)
6130
6130
 
6131
- $ipv4 = nope
6132
- for ($offs, $ipv4) in $rx.gets(wait=1) {
6133
- [ inet:ipv4=$ipv4 ]
6134
- $rx.cull($offs)
6135
- $tx.put($ipv4)
6136
- }
6137
- })
6131
+ $ddef = $lib.dmon.add(${
6138
6132
 
6139
- $tx.put(1.2.3.4)
6133
+ $rx = $lib.queue.get(tx)
6134
+ $tx = $lib.queue.get(rx)
6140
6135
 
6141
- for ($xoff, $xpv4) in $rx.gets(size=1, wait=1) { }
6136
+ $ipv4 = nope
6137
+ for ($offs, $ipv4) in $rx.gets(wait=1) {
6138
+ [ inet:ipv4=$ipv4 ]
6139
+ $rx.cull($offs)
6140
+ $tx.put($ipv4)
6141
+ }
6142
+ })
6142
6143
 
6143
- $lib.print(xed)
6144
+ $tx.put(1.2.3.4)
6144
6145
 
6145
- inet:ipv4=$xpv4
6146
+ for ($xoff, $xpv4) in $rx.gets(size=1, wait=1) { }
6146
6147
 
6147
- $lib.dmon.del($ddef.iden)
6148
+ $lib.print(xed)
6148
6149
 
6149
- $lib.queue.del(tx)
6150
- $lib.queue.del(rx)
6151
- ''')
6152
- self.len(1, nodes)
6153
- self.len(0, await prox.getStormDmons())
6150
+ inet:ipv4=$xpv4
6154
6151
 
6155
- with self.raises(s_exc.NoSuchIden):
6156
- await core.nodes('$lib.dmon.del(newp)')
6152
+ $lib.dmon.del($ddef.iden)
6157
6153
 
6158
- await core.stormlist('auth.user.add user')
6159
- user = await core.auth.getUserByName('user')
6160
- asuser = {'user': user.iden}
6154
+ $lib.queue.del(tx)
6155
+ $lib.queue.del(rx)
6156
+ ''')
6157
+ self.len(1, nodes)
6158
+ self.len(0, await prox.getStormDmons())
6161
6159
 
6162
- ddef = await core.callStorm('return($lib.dmon.add(${$lib.print(foo)}))')
6163
- iden = ddef.get('iden')
6160
+ with self.raises(s_exc.NoSuchIden):
6161
+ await core.nodes('$lib.dmon.del(newp)')
6164
6162
 
6165
- with self.raises(s_exc.AuthDeny):
6166
- await core.callStorm(f'$lib.dmon.del({iden})', opts=asuser)
6163
+ await core.stormlist('auth.user.add user')
6164
+ user = await core.auth.getUserByName('user')
6165
+ asuser = {'user': user.iden}
6166
+
6167
+ ddef = await core.callStorm('return($lib.dmon.add(${$lib.print(foo)}))')
6168
+ iden = ddef.get('iden')
6169
+ asuser['vars'] = {'iden': iden}
6170
+
6171
+ with self.raises(s_exc.AuthDeny):
6172
+ await core.callStorm(f'$lib.dmon.del($iden)', opts=asuser)
6173
+
6174
+ # remove the dmon without a nexus entry to verify recover works
6175
+ await core._delStormDmon(iden)
6176
+ self.none(await core.callStorm('return($lib.dmon.get($iden))', opts=asuser))
6177
+ self.eq('storm:dmon:add', (await core.nexsroot.nexslog.last())[1][1])
6178
+
6179
+ async with self.getTestCoreAndProxy(dirn=dirn) as (core, prox):
6180
+
6181
+ # nexus recover() previously failed on adding to the hive
6182
+ # although the dmon would get successfully started
6183
+ self.nn(await core.callStorm('return($lib.dmon.get($iden))', opts=asuser))
6184
+ self.nn(core.stormdmonhive.get(iden))
6167
6185
 
6168
6186
  async def test_cortex_storm_dmon_view(self):
6169
6187
 
@@ -7164,6 +7182,66 @@ class CortexBasicTest(s_t_utils.SynTest):
7164
7182
  # Avoid races in cleanup
7165
7183
  del genr
7166
7184
 
7185
+ async def test_cortex_syncnodeedits(self):
7186
+
7187
+ async with self.getTestCore() as core:
7188
+
7189
+ layr00 = core.getLayer().iden
7190
+ layr01 = (await core.addLayer())['iden']
7191
+ view01 = (await core.addView({'layers': (layr01,)}))['iden']
7192
+
7193
+ async def layrgenr(layr, startoff, endoff=None, newlayer=False):
7194
+ wait = endoff is None
7195
+ async for ioff, item, meta in layr.syncNodeEdits2(startoff, wait=wait):
7196
+ if endoff is not None and ioff >= endoff:
7197
+ break
7198
+ yield ioff, item, meta
7199
+
7200
+ indx = await core.getNexsIndx()
7201
+
7202
+ offsdict = {
7203
+ layr00: indx,
7204
+ layr01: indx,
7205
+ }
7206
+
7207
+ genr = None
7208
+
7209
+ try:
7210
+
7211
+ # test that a slow consumer can continue to stream edits
7212
+ # even if a layer exceeds the window maxsize
7213
+
7214
+ oldv = s_layer.WINDOW_MAXSIZE
7215
+ s_layer.WINDOW_MAXSIZE = 2
7216
+
7217
+ genr = core._syncNodeEdits(offsdict, layrgenr, wait=True)
7218
+
7219
+ nodes = await core.nodes('[ test:str=foo ]')
7220
+ item = await asyncio.wait_for(genr.__anext__(), timeout=2)
7221
+ self.eq(s_common.uhex(nodes[0].iden()), item[1][0][0])
7222
+
7223
+ # we should now be in live sync
7224
+ # and the empty layer will be pulling from the window
7225
+
7226
+ nodes = await core.nodes('[ test:str=bar ]')
7227
+ item = await asyncio.wait_for(genr.__anext__(), timeout=2)
7228
+ self.eq(s_common.uhex(nodes[0].iden()), item[1][0][0])
7229
+
7230
+ # add more nodes than the window size without consuming from the genr
7231
+
7232
+ opts = {'view': view01}
7233
+ nodes = await core.nodes('for $s in (baz, bam, cat, dog) { [ test:str=$s ] }', opts=opts)
7234
+ items = [await asyncio.wait_for(genr.__anext__(), timeout=2) for _ in range(4)]
7235
+ self.sorteq(
7236
+ [s_common.uhex(n.iden()) for n in nodes],
7237
+ [item[1][0][0] for item in items],
7238
+ )
7239
+
7240
+ finally:
7241
+ s_layer.WINDOW_MAXSIZE = oldv
7242
+ if genr is not None:
7243
+ del genr
7244
+
7167
7245
  async def test_cortex_all_layr_read(self):
7168
7246
  async with self.getTestCore() as core:
7169
7247
  layr = core.getView().layers[0].iden
@@ -7613,3 +7691,121 @@ class CortexBasicTest(s_t_utils.SynTest):
7613
7691
  self.isin('Set admin=True for lowuser', mesg.get('message'))
7614
7692
  self.eq('admin', mesg.get('username'))
7615
7693
  self.eq('lowuser', mesg.get('target_username'))
7694
+
7695
+ async def test_cortex_ext_httpapi(self):
7696
+ # Cortex API tests for Extended HttpAPI
7697
+ async with self.getTestCore() as core: # type: s_cortex.Cortex
7698
+
7699
+ newp = s_common.guid()
7700
+ with self.raises(s_exc.SynErr):
7701
+ await core.setHttpApiIndx(newp, 0)
7702
+
7703
+ unfo = await core.getUserDefByName('root')
7704
+ view = core.getView()
7705
+ info = await core.addHttpExtApi({
7706
+ 'path': 'test/path/(hehe|haha)/(.*)',
7707
+ 'owner': unfo.get('iden'),
7708
+ 'view': view.iden,
7709
+ })
7710
+
7711
+ info2 = await core.addHttpExtApi({
7712
+ 'path': 'something/else',
7713
+ 'owner': unfo.get('iden'),
7714
+ 'view': view.iden,
7715
+ })
7716
+
7717
+ info3 = await core.addHttpExtApi({
7718
+ 'path': 'something/else/goes/here',
7719
+ 'owner': unfo.get('iden'),
7720
+ 'view': view.iden,
7721
+ })
7722
+
7723
+ info4 = await core.addHttpExtApi({
7724
+ 'path': 'another/item',
7725
+ 'owner': unfo.get('iden'),
7726
+ 'view': view.iden,
7727
+ })
7728
+
7729
+ iden = info.get('iden')
7730
+
7731
+ adef = await core.getHttpExtApi(iden)
7732
+ self.eq(adef, info)
7733
+
7734
+ adef, args = await core.getHttpExtApiByPath('test/path/hehe/wow')
7735
+ self.eq(adef, info)
7736
+ self.eq(args, ('hehe', 'wow'))
7737
+
7738
+ adef, args = await core.getHttpExtApiByPath('test/path/hehe/wow/more/')
7739
+ self.eq(adef, info)
7740
+ self.eq(args, ('hehe', 'wow/more/'))
7741
+
7742
+ adef, args = await core.getHttpExtApiByPath('test/path/HeHe/wow')
7743
+ self.none(adef)
7744
+ self.eq(args, ())
7745
+
7746
+ async with core.getLocalProxy() as prox:
7747
+ adef, args = await prox.getHttpExtApiByPath('test/path/haha/words')
7748
+ self.eq(adef, info)
7749
+ self.eq(args, ('haha', 'words'))
7750
+
7751
+ self.len(4, core._exthttpapicache)
7752
+
7753
+ # Reordering / safety
7754
+ self.eq(1, await core.setHttpApiIndx(info4.get('iden'), 1))
7755
+
7756
+ # Cache is cleared when reloading
7757
+ self.len(0, core._exthttpapicache)
7758
+ adef, args = await core.getHttpExtApiByPath('test/path/hehe/wow')
7759
+ self.eq(adef, info)
7760
+ self.len(1, core._exthttpapicache)
7761
+
7762
+ self.eq([adef.get('iden') for adef in await core.getHttpExtApis()],
7763
+ [info.get('iden'), info4.get('iden'), info2.get('iden'), info3.get('iden')])
7764
+
7765
+ items = await core.getHttpExtApis()
7766
+ self.eq(items, (info, info4, info2, info3))
7767
+
7768
+ # Tiny sleep to ensure that updated ticks forward when modified
7769
+ created = adef.get('created')
7770
+ updated = adef.get('updated')
7771
+ await asyncio.sleep(0.005)
7772
+ adef = await core.modHttpExtApi(iden, 'name', 'wow')
7773
+ self.eq(adef.get('created'), created)
7774
+ self.gt(adef.get('updated'), updated)
7775
+
7776
+ # Sad path
7777
+
7778
+ with self.raises(s_exc.SynErr):
7779
+ await core.setHttpApiIndx(newp, 0)
7780
+
7781
+ with self.raises(s_exc.BadArg):
7782
+ await core.setHttpApiIndx(newp, -1)
7783
+
7784
+ with self.raises(s_exc.NoSuchUser):
7785
+ await core.modHttpExtApi(iden, 'owner', newp)
7786
+
7787
+ with self.raises(s_exc.NoSuchView):
7788
+ await core.modHttpExtApi(iden, 'view', newp)
7789
+
7790
+ with self.raises(s_exc.BadArg):
7791
+ await core.modHttpExtApi(iden, 'created', 1234)
7792
+
7793
+ with self.raises(s_exc.BadArg):
7794
+ await core.modHttpExtApi(iden, 'updated', 1234)
7795
+
7796
+ with self.raises(s_exc.BadArg):
7797
+ await core.modHttpExtApi(iden, 'creator', s_common.guid())
7798
+
7799
+ with self.raises(s_exc.BadArg):
7800
+ await core.modHttpExtApi(iden, 'newp', newp)
7801
+
7802
+ with self.raises(s_exc.NoSuchIden):
7803
+ await core.modHttpExtApi(newp, 'path', 'a/new/path/')
7804
+
7805
+ with self.raises(s_exc.NoSuchIden):
7806
+ await core.getHttpExtApi(newp)
7807
+
7808
+ self.none(await core.delHttpExtApi(newp))
7809
+
7810
+ with self.raises(s_exc.BadArg):
7811
+ await core.delHttpExtApi('notAGuid')