synapse 2.165.0__py311-none-any.whl → 2.166.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 (51) hide show
  1. synapse/cmds/cortex.py +1 -6
  2. synapse/common.py +6 -0
  3. synapse/cortex.py +73 -56
  4. synapse/datamodel.py +32 -0
  5. synapse/lib/agenda.py +81 -51
  6. synapse/lib/ast.py +21 -23
  7. synapse/lib/base.py +0 -6
  8. synapse/lib/cell.py +13 -22
  9. synapse/lib/httpapi.py +1 -0
  10. synapse/lib/nexus.py +3 -2
  11. synapse/lib/schemas.py +2 -0
  12. synapse/lib/snap.py +50 -0
  13. synapse/lib/storm.py +19 -17
  14. synapse/lib/stormlib/aha.py +4 -1
  15. synapse/lib/stormlib/auth.py +11 -4
  16. synapse/lib/stormlib/cache.py +202 -0
  17. synapse/lib/stormlib/cortex.py +69 -7
  18. synapse/lib/stormlib/spooled.py +109 -0
  19. synapse/lib/stormtypes.py +43 -15
  20. synapse/lib/trigger.py +10 -12
  21. synapse/lib/types.py +1 -1
  22. synapse/lib/version.py +2 -2
  23. synapse/lib/view.py +12 -0
  24. synapse/models/inet.py +74 -2
  25. synapse/models/orgs.py +52 -8
  26. synapse/models/person.py +30 -11
  27. synapse/models/risk.py +44 -3
  28. synapse/telepath.py +114 -32
  29. synapse/tests/test_cortex.py +40 -6
  30. synapse/tests/test_datamodel.py +22 -0
  31. synapse/tests/test_lib_agenda.py +8 -1
  32. synapse/tests/test_lib_aha.py +18 -4
  33. synapse/tests/test_lib_storm.py +95 -4
  34. synapse/tests/test_lib_stormlib_cache.py +272 -0
  35. synapse/tests/test_lib_stormlib_cortex.py +71 -0
  36. synapse/tests/test_lib_stormlib_spooled.py +190 -0
  37. synapse/tests/test_lib_stormtypes.py +27 -4
  38. synapse/tests/test_model_inet.py +67 -0
  39. synapse/tests/test_model_risk.py +6 -0
  40. synapse/tests/test_telepath.py +30 -7
  41. synapse/tests/test_tools_modrole.py +81 -0
  42. synapse/tests/test_tools_moduser.py +105 -0
  43. synapse/tools/modrole.py +59 -7
  44. synapse/tools/moduser.py +78 -10
  45. {synapse-2.165.0.dist-info → synapse-2.166.0.dist-info}/METADATA +2 -2
  46. {synapse-2.165.0.dist-info → synapse-2.166.0.dist-info}/RECORD +49 -47
  47. synapse/lib/provenance.py +0 -111
  48. synapse/tests/test_lib_provenance.py +0 -37
  49. {synapse-2.165.0.dist-info → synapse-2.166.0.dist-info}/LICENSE +0 -0
  50. {synapse-2.165.0.dist-info → synapse-2.166.0.dist-info}/WHEEL +0 -0
  51. {synapse-2.165.0.dist-info → synapse-2.166.0.dist-info}/top_level.txt +0 -0
synapse/models/orgs.py CHANGED
@@ -24,14 +24,27 @@ class OuModule(s_module.CoreModule):
24
24
  }),
25
25
  ('ou:isic', ('str', {'regex': r'^[A-Z]([0-9]{2}[0-9]{0,2})?$'}), {
26
26
  'doc': 'An International Standard Industrial Classification of All Economic Activities (ISIC) code.',
27
- 'ex': 'C1393',
28
- }),
27
+ 'ex': 'C1393'}),
28
+
29
29
  ('ou:org', ('guid', {}), {
30
30
  'doc': 'A GUID for a human organization such as a company or military unit.',
31
- }),
31
+ 'display': {
32
+ 'columns': (
33
+ {'type': 'prop', 'opts': {'name': 'name'}},
34
+ {'type': 'prop', 'opts': {'name': 'names'}},
35
+ {'type': 'prop', 'opts': {'name': 'country:code'}},
36
+ ),
37
+ }}),
38
+
32
39
  ('ou:team', ('guid', {}), {
33
40
  'doc': 'A GUID for a team within an organization.',
34
- }),
41
+ 'display': {
42
+ 'columns': (
43
+ {'type': 'prop', 'opts': {'name': 'name'}},
44
+ {'type': 'prop', 'opts': {'name': 'org::name'}},
45
+ ),
46
+ }}),
47
+
35
48
  ('ou:orgtype', ('taxonomy', {}), {
36
49
  'doc': 'An org type taxonomy.',
37
50
  'interfaces': ('meta:taxonomy',),
@@ -49,6 +62,11 @@ class OuModule(s_module.CoreModule):
49
62
  }),
50
63
  ('ou:industry', ('guid', {}), {
51
64
  'doc': 'An industry classification type.',
65
+ 'display': {
66
+ 'columns': (
67
+ {'type': 'prop', 'opts': {'name': 'name'}},
68
+ ),
69
+ },
52
70
  }),
53
71
  ('ou:industry:type:taxonomy', ('taxonomy', {}), {
54
72
  'interfaces': ('meta:taxonomy',),
@@ -113,6 +131,12 @@ class OuModule(s_module.CoreModule):
113
131
  }),
114
132
  ('ou:conference', ('guid', {}), {
115
133
  'doc': 'A conference with a name and sponsoring org.',
134
+ 'display': {
135
+ 'columns': (
136
+ {'type': 'prop', 'opts': {'name': 'name'}},
137
+ {'type': 'prop', 'opts': {'name': 'start'}},
138
+ ),
139
+ },
116
140
  }),
117
141
  ('ou:conference:attendee', ('comp', {'fields': (('conference', 'ou:conference'), ('person', 'ps:person'))}), {
118
142
  'deprecated': True,
@@ -133,6 +157,11 @@ class OuModule(s_module.CoreModule):
133
157
  }),
134
158
  ('ou:goal', ('guid', {}), {
135
159
  'doc': 'An assessed or stated goal which may be abstract or org specific.',
160
+ 'display': {
161
+ 'columns': (
162
+ {'type': 'prop', 'opts': {'name': 'name'}},
163
+ ),
164
+ },
136
165
  }),
137
166
  ('ou:goalname', ('str', {'lower': True, 'onespace': True}), {
138
167
  'doc': 'A goal name.',
@@ -154,16 +183,31 @@ class OuModule(s_module.CoreModule):
154
183
 
155
184
  ('ou:campaign', ('guid', {}), {
156
185
  'doc': "Represents an org's activity in pursuit of a goal.",
157
- }),
186
+ 'display': {
187
+ 'columns': (
188
+ {'type': 'prop', 'opts': {'name': 'name'}},
189
+ {'type': 'prop', 'opts': {'name': 'names'}},
190
+ {'type': 'prop', 'opts': {'name': 'reporter:name'}},
191
+ {'type': 'prop', 'opts': {'name': 'tag'}},
192
+ ),
193
+ }}),
194
+
158
195
  ('ou:conflict', ('guid', {}), {
159
196
  'doc': 'Represents a conflict where two or more campaigns have mutually exclusive goals.',
160
197
  }),
161
198
  ('ou:contribution', ('guid', {}), {
162
- 'doc': 'Represents a specific instance of contributing material support to a campaign.',
163
- }),
199
+ 'doc': 'Represents a specific instance of contributing material support to a campaign.'}),
200
+
164
201
  ('ou:technique', ('guid', {}), {
165
202
  'doc': 'A specific technique used to achieve a goal.',
166
- }),
203
+ 'display': {
204
+ 'columns': (
205
+ {'type': 'prop', 'opts': {'name': 'name'}},
206
+ {'type': 'prop', 'opts': {'name': 'reporter:name'}},
207
+ {'type': 'prop', 'opts': {'name': 'tag'}},
208
+ ),
209
+ }}),
210
+
167
211
  ('ou:technique:taxonomy', ('taxonomy', {}), {
168
212
  'interfaces': ('meta:taxonomy',),
169
213
  'doc': 'An analyst defined taxonomy to classify techniques in different disciplines.',
synapse/models/person.py CHANGED
@@ -39,11 +39,19 @@ class PsModule(s_module.CoreModule):
39
39
  ('ps:persona:has', ('comp', {'fields': (('persona', 'ps:persona'), ('node', 'ndef'))}), {
40
40
  'deprecated': True,
41
41
  'doc': 'A persona owns, controls, or has exclusive use of an object or'
42
- ' resource, potentially during a specific period of time.'
43
- }),
42
+ ' resource, potentially during a specific period of time.'}),
43
+
44
44
  ('ps:contact', ('guid', {}), {
45
45
  'doc': 'A GUID for a contact info record.',
46
- }),
46
+ 'display': {
47
+ 'columns': (
48
+ {'type': 'prop', 'opts': {'name': 'name'}},
49
+ {'type': 'prop', 'opts': {'name': 'type'}},
50
+ {'type': 'prop', 'opts': {'name': 'orgname'}},
51
+ {'type': 'prop', 'opts': {'name': 'email'}},
52
+ ),
53
+ }}),
54
+
47
55
  ('ps:contact:type:taxonomy', ('taxonomy', {}), {
48
56
  'interfaces': ('meta:taxonomy',),
49
57
  'doc': 'A taxonomy of contact types.',
@@ -55,18 +63,29 @@ class PsModule(s_module.CoreModule):
55
63
  'doc': "A GUID representing entry in a contact's work history.",
56
64
  }),
57
65
  ('ps:vitals', ('guid', {}), {
58
- 'doc': 'Statistics and demographic data about a person or contact.',
59
- }),
66
+ 'doc': 'Statistics and demographic data about a person or contact.'}),
67
+
60
68
  ('ps:skill', ('guid', {}), {
61
- 'doc': 'A specific skill which a person or organization may have.'
62
- }),
69
+ 'doc': 'A specific skill which a person or organization may have.',
70
+ 'display': {
71
+ 'columns': (
72
+ {'type': 'prop', 'opts': {'name': 'name'}},
73
+ {'type': 'prop', 'opts': {'name': 'type'}},
74
+ ),
75
+ }}),
76
+
63
77
  ('ps:skill:type:taxonomy', ('taxonomy', {}), {
64
78
  'interfaces': ('meta:taxonomy',),
65
- 'doc': 'A taxonomy of skill types.',
66
- }),
79
+ 'doc': 'A taxonomy of skill types.'}),
80
+
67
81
  ('ps:proficiency', ('guid', {}), {
68
- 'doc': 'The assessment that a given contact possesses a specific skill.'
69
- }),
82
+ 'doc': 'The assessment that a given contact possesses a specific skill.',
83
+ 'display': {
84
+ 'columns': (
85
+ {'type': 'prop', 'opts': {'name': 'contact::name'}},
86
+ {'type': 'prop', 'opts': {'name': 'skill::name'}},
87
+ ),
88
+ }}),
70
89
  ),
71
90
  'edges': (
72
91
  (('ps:contact', 'has', None), {
synapse/models/risk.py CHANGED
@@ -68,6 +68,14 @@ class RiskModule(s_module.CoreModule):
68
68
 
69
69
  ('risk:threat', ('guid', {}), {
70
70
  'doc': 'A threat cluster or subgraph of threat activity, as reported by a specific organization.',
71
+ 'display': {
72
+ 'columns': (
73
+ {'type': 'prop', 'opts': {'name': 'org:name'}},
74
+ {'type': 'prop', 'opts': {'name': 'org:names'}},
75
+ {'type': 'prop', 'opts': {'name': 'reporter:name'}},
76
+ {'type': 'prop', 'opts': {'name': 'tag'}},
77
+ ),
78
+ },
71
79
  }),
72
80
  ('risk:attack', ('guid', {}), {
73
81
  'doc': 'An instance of an actor attacking a target.',
@@ -81,9 +89,22 @@ class RiskModule(s_module.CoreModule):
81
89
  }),
82
90
  ('risk:compromise', ('guid', {}), {
83
91
  'doc': 'An instance of a compromise and its aggregate impact.',
92
+ 'display': {
93
+ 'columns': (
94
+ {'type': 'prop', 'opts': {'name': 'name'}},
95
+ {'type': 'prop', 'opts': {'name': 'reporter:name'}},
96
+ ),
97
+ },
84
98
  }),
85
99
  ('risk:mitigation', ('guid', {}), {
86
100
  'doc': 'A mitigation for a specific risk:vuln.',
101
+ 'display': {
102
+ 'columns': (
103
+ {'type': 'prop', 'opts': {'name': 'name'}},
104
+ {'type': 'prop', 'opts': {'name': 'reporter:name'}},
105
+ {'type': 'prop', 'opts': {'name': 'tag'}},
106
+ ),
107
+ },
87
108
  }),
88
109
  ('risk:attacktype', ('taxonomy', {}), {
89
110
  'doc': 'A taxonomy of attack types.',
@@ -104,6 +125,14 @@ class RiskModule(s_module.CoreModule):
104
125
  }),
105
126
  ('risk:tool:software', ('guid', {}), {
106
127
  'doc': 'A software tool used in threat activity, as reported by a specific organization.',
128
+ 'display': {
129
+ 'columns': (
130
+ {'type': 'prop', 'opts': {'name': 'soft:name'}},
131
+ {'type': 'prop', 'opts': {'name': 'soft:names'}},
132
+ {'type': 'prop', 'opts': {'name': 'reporter:name'}},
133
+ {'type': 'prop', 'opts': {'name': 'tag'}},
134
+ ),
135
+ },
107
136
  }),
108
137
 
109
138
  ('risk:alert:verdict:taxonomy', ('taxonomy', {}), {
@@ -128,19 +157,22 @@ class RiskModule(s_module.CoreModule):
128
157
  ('risk:extortion:type:taxonomy', ('taxonomy', {}), {
129
158
  'interfaces': ('meta:taxonomy',),
130
159
  'doc': 'A taxonomy of extortion event types.'}),
160
+
131
161
  ('risk:technique:masquerade', ('guid', {}), {
132
162
  'doc': 'Represents the assessment that a node is designed to resemble another in order to mislead.'}),
133
163
  ),
134
164
  'edges': (
135
165
  # some explicit examples...
136
166
  (('risk:attack', 'uses', 'ou:technique'), {
137
- 'doc': 'The attackers used the technique in the attack.'}),
167
+ 'doc': 'The attacker used the technique in the attack.'}),
138
168
  (('risk:threat', 'uses', 'ou:technique'), {
139
169
  'doc': 'The threat cluster uses the technique.'}),
140
170
  (('risk:tool:software', 'uses', 'ou:technique'), {
141
171
  'doc': 'The tool uses the technique.'}),
142
172
  (('risk:compromise', 'uses', 'ou:technique'), {
143
- 'doc': 'The attackers used the technique in the compromise.'}),
173
+ 'doc': 'The attacker used the technique in the compromise.'}),
174
+ (('risk:extortion', 'uses', 'ou:technique'), {
175
+ 'doc': 'The attacker used the technique to extort the victim.'}),
144
176
 
145
177
  (('risk:attack', 'uses', 'risk:vuln'), {
146
178
  'doc': 'The attack used the vulnerability.'}),
@@ -936,12 +968,18 @@ class RiskModule(s_module.CoreModule):
936
968
  ('compromise', ('risk:compromise', {}), {
937
969
  'doc': 'The compromise which allowed the leaker access to the information.'}),
938
970
 
971
+ ('extortion', ('risk:extortion', {}), {
972
+ 'doc': 'The extortion event which used the threat of the leak as leverage.'}),
973
+
939
974
  ('public', ('bool', {}), {
940
975
  'doc': 'Set to true if the leaked information was made publicly available.'}),
941
976
 
942
977
  ('public:url', ('inet:url', {}), {
943
978
  'doc': 'The URL where the leaked information was made publicly available.'}),
944
979
 
980
+ ('size:bytes', ('int', {'min': 0}), {
981
+ 'doc': 'The approximate uncompressed size of the total data leaked.'}),
982
+
945
983
  )),
946
984
 
947
985
  ('risk:extortion:type:taxonomy', {}, ()),
@@ -963,6 +1001,9 @@ class RiskModule(s_module.CoreModule):
963
1001
  ('demanded', ('time', {}), {
964
1002
  'doc': 'The time that the attacker made their demands.'}),
965
1003
 
1004
+ ('deadline', ('time', {}), {
1005
+ 'doc': 'The time that the demand must be met.'}),
1006
+
966
1007
  ('goal', ('ou:goal', {}), {
967
1008
  'doc': 'The goal of the attacker in extorting the victim.'}),
968
1009
 
@@ -976,7 +1017,7 @@ class RiskModule(s_module.CoreModule):
976
1017
  'doc': 'The extortion target identity.'}),
977
1018
 
978
1019
  ('success', ('bool', {}), {
979
- 'doc': 'Set to true if the victim met the attackers demands.'}),
1020
+ 'doc': "Set to true if the victim met the attacker's demands."}),
980
1021
 
981
1022
  ('enacted', ('bool', {}), {
982
1023
  'doc': 'Set to true if attacker carried out the threat.'}),
synapse/telepath.py CHANGED
@@ -135,25 +135,24 @@ def mergeAhaInfo(info0, info1):
135
135
 
136
136
  return info0
137
137
 
138
- async def open(url, timeout=None):
138
+ async def open(url, onlink=None):
139
139
  '''
140
- Open a new telepath Client (or AHA Service Pool) based on the given URL.
141
- '''
142
- # backward compatible support for a list of URLs or urlinfo dicts...
143
- if isinstance(url, (tuple, list)): # pragma: no cover
144
- return await Client.anit(url)
145
-
146
- urlinfo = chopurl(url)
140
+ Open a new telepath ClientV2 object based on the given URL.
147
141
 
148
- if urlinfo.get('scheme') == 'aha':
149
-
150
- ahaclient, ahasvc = await _getAhaSvc(urlinfo, timeout=timeout)
142
+ Args:
143
+ url (str): The URL to connect to.
144
+ onlink: An optional async callback function to run when connections are made.
151
145
 
152
- # check if we should return a Pool rather than a Client :)
153
- if ahasvc.get('services'):
154
- return await Pool.anit(ahaclient, ahasvc, urlinfo)
146
+ Notes:
147
+ The onlink callback function has the call signature ``(proxy, urlinfo)``.
148
+ The proxy is the Telepath Proxy object.
149
+ The urlinfo is the parsed URL information used to create the proxy object.
150
+ The urlinfo structure may change between versions of Synapse.
155
151
 
156
- return await Client.anit(urlinfo)
152
+ Returns:
153
+ ClientV2: A ClientV2 object.
154
+ '''
155
+ return await ClientV2.anit(url, onlink=onlink)
157
156
 
158
157
  async def _getAhaSvc(urlinfo, timeout=None):
159
158
 
@@ -832,7 +831,7 @@ class Proxy(s_base.Base):
832
831
 
833
832
  mesg = await link.rx()
834
833
  if mesg is None:
835
- raise s_exc.LinkShutDown(mesg=mesg)
834
+ raise s_exc.LinkShutDown(mesg='Remote peer disconnected')
836
835
 
837
836
  if mesg[0] != 't2:yield': # pragma: no cover
838
837
  info = 'Telepath protocol violation: unexpected message received'
@@ -869,6 +868,8 @@ class Proxy(s_base.Base):
869
868
  if self.sess is not None:
870
869
  return await self.taskv2(todo, name=name)
871
870
 
871
+ s_common.deprecated('Telepath task with no session', curv='2.166.0')
872
+
872
873
  task = Task()
873
874
 
874
875
  mesg = ('task:init', {
@@ -980,21 +981,39 @@ class Proxy(s_base.Base):
980
981
  setattr(self, name, meth)
981
982
  return meth
982
983
 
983
- class Pool(s_base.Base):
984
+ class ClientV2(s_base.Base):
984
985
  '''
985
986
  A telepath client which:
986
987
  * connects to multiple services
987
988
  * distributes API calls across them
988
989
  * receives topology updates from AHA
990
+
991
+ NOTE: This must co-exist with Client until we eliminate uses that
992
+ attempt to call telepath APIs directly from the Client rather
993
+ than awaiting a proxy()
989
994
  '''
990
- async def __anit__(self, aha, ahasvc, urlinfo):
995
+ async def __anit__(self, urlinfo, onlink=None):
996
+
991
997
  await s_base.Base.__anit__(self)
998
+
999
+ # some ugly stuff in order to be backward compatible...
1000
+ if not isinstance(urlinfo, (list, tuple)):
1001
+ urlinfo = (urlinfo,)
1002
+
1003
+ urlinfo = [chopurl(u) for u in urlinfo]
1004
+
1005
+ self.aha = None
1006
+
992
1007
  self.clients = {}
993
1008
  self.proxies = set()
994
1009
 
995
- self.aha = aha
996
- self.ahasvc = ahasvc
997
- self.urlinfo = urlinfo
1010
+ self.poolname = None
1011
+
1012
+ self.onlink = onlink
1013
+
1014
+ self.booturls = urlinfo
1015
+ self.bootdeque = collections.deque()
1016
+ self.bootdeque.extend(self.booturls)
998
1017
 
999
1018
  self.ready = asyncio.Event()
1000
1019
  self.deque = collections.deque()
@@ -1003,7 +1022,6 @@ class Pool(s_base.Base):
1003
1022
  'svc:add': self._onPoolSvcAdd,
1004
1023
  'svc:del': self._onPoolSvcDel,
1005
1024
  }
1006
- self.schedCoro(self._toposync())
1007
1025
 
1008
1026
  async def fini():
1009
1027
  await self._shutDownPool()
@@ -1012,8 +1030,63 @@ class Pool(s_base.Base):
1012
1030
 
1013
1031
  self.onfini(fini)
1014
1032
 
1033
+ self.schedCoro(self._initBootProxy())
1034
+
1035
+ def getNextBootUrl(self):
1036
+ if not self.bootdeque:
1037
+ self.bootdeque.extend(self.booturls)
1038
+ return self.bootdeque.popleft()
1039
+
1040
+ async def _initBootProxy(self):
1041
+
1042
+ lastlog = 0.0
1043
+ while not self.isfini:
1044
+
1045
+ urlinfo = self.getNextBootUrl()
1046
+
1047
+ try:
1048
+
1049
+ if urlinfo.get('scheme') == 'aha':
1050
+
1051
+ self.aha, svcinfo = await _getAhaSvc(urlinfo)
1052
+
1053
+ # if the service is a pool, enter pool mode and fire
1054
+ # the topography sync task to manage pool members.
1055
+ services = svcinfo.get('services')
1056
+ if services is not None:
1057
+ # we are an AHA pool!
1058
+ if self.poolname is None:
1059
+ self.poolname = svcinfo.get('name')
1060
+ self.schedCoro(self._toposync())
1061
+ return
1062
+
1063
+ # regular telepath client behavior
1064
+ proxy = await openinfo(urlinfo)
1065
+ await self._onPoolLink(proxy, urlinfo)
1066
+
1067
+ async def reconnect():
1068
+ if not self.isfini:
1069
+ self.schedCoro(self._initBootProxy())
1070
+
1071
+ proxy.onfini(reconnect)
1072
+ return
1073
+
1074
+ except Exception as e:
1075
+
1076
+ now = time.monotonic()
1077
+ if now > lastlog + 60.0: # don't logspam the disconnect message more than 1/min
1078
+ url = s_urlhelp.sanitizeUrl(zipurl(urlinfo))
1079
+ logger.exception(f'telepath clientv2 ({url}) encountered an error: {e}')
1080
+ lastlog = now
1081
+
1082
+ retrysleep = float(urlinfo.get('retrysleep', 0.2))
1083
+ await self.waitfini(timeout=retrysleep)
1084
+
1085
+ async def waitready(self, timeout=None):
1086
+ await s_common.wait_for(self.ready.wait(), timeout=timeout)
1087
+
1015
1088
  def size(self):
1016
- return len(self.clients)
1089
+ return len(self.proxies)
1017
1090
 
1018
1091
  async def _onPoolSvcAdd(self, mesg):
1019
1092
  svcname = mesg[1].get('name')
@@ -1022,7 +1095,7 @@ class Pool(s_base.Base):
1022
1095
  await oldc.fini()
1023
1096
 
1024
1097
  urlinfo = {'scheme': 'aha', 'host': svcname, 'path': ''}
1025
- self.clients[svcname] = await Client.anit(urlinfo, onlink=self._onPoolLink)
1098
+ self.clients[svcname] = await ClientV2.anit(urlinfo, onlink=self._onPoolLink)
1026
1099
  await self.fire('svc:add', **mesg[1])
1027
1100
 
1028
1101
  async def _onPoolSvcDel(self, mesg):
@@ -1033,10 +1106,11 @@ class Pool(s_base.Base):
1033
1106
  self.deque.clear()
1034
1107
  await self.fire('svc:del', **mesg[1])
1035
1108
 
1036
- async def _onPoolLink(self, proxy):
1109
+ async def _onPoolLink(self, proxy, urlinfo):
1037
1110
 
1038
1111
  async def onfini():
1039
- self.proxies.remove(proxy)
1112
+ if proxy in self.proxies:
1113
+ self.proxies.remove(proxy)
1040
1114
  if proxy in self.deque:
1041
1115
  self.deque.remove(proxy)
1042
1116
  if not len(self.proxies):
@@ -1046,13 +1120,23 @@ class Pool(s_base.Base):
1046
1120
  self.proxies.add(proxy)
1047
1121
  self.ready.set()
1048
1122
 
1123
+ if self.onlink is not None:
1124
+ try:
1125
+ await self.onlink(proxy, urlinfo)
1126
+ except Exception as e:
1127
+ logger.exception(f'onlink: {self.onlink}')
1128
+
1049
1129
  async def _shutDownPool(self):
1050
1130
  # when we reconnect to our AHA service, we need to dump the current
1051
1131
  # topology state and gather it again.
1052
- for client in self.clients.values():
1132
+ for client in list(self.clients.values()):
1053
1133
  await client.fini()
1054
1134
 
1135
+ for proxy in list(self.proxies):
1136
+ await proxy.fini()
1137
+
1055
1138
  self.deque.clear()
1139
+ self.ready.clear()
1056
1140
  self.clients.clear()
1057
1141
  self.proxies.clear()
1058
1142
 
@@ -1065,16 +1149,14 @@ class Pool(s_base.Base):
1065
1149
 
1066
1150
  while not self.isfini:
1067
1151
 
1068
- poolname = self.ahasvc.get('name')
1069
-
1070
1152
  try:
1071
1153
  ahaproxy = await self.aha.proxy()
1072
1154
 
1073
1155
  await reset()
1074
1156
 
1075
- async for mesg in ahaproxy.iterPoolTopo(poolname):
1157
+ async for mesg in ahaproxy.iterPoolTopo(self.poolname):
1076
1158
  hand = self.mesghands.get(mesg[0])
1077
- if hand is None: # pragma: no cover
1159
+ if hand is None: # pragma: no cover
1078
1160
  logger.warning(f'Unknown AHA pool topography message: {mesg}')
1079
1161
  continue
1080
1162
 
@@ -1092,7 +1174,7 @@ class Pool(s_base.Base):
1092
1174
 
1093
1175
  await self.ready.wait()
1094
1176
 
1095
- if self.isfini: # pragma: no cover
1177
+ if self.isfini: # pragma: no cover
1096
1178
  raise s_exc.IsFini()
1097
1179
 
1098
1180
  if not self.deque:
@@ -4564,10 +4564,9 @@ class CortexBasicTest(s_t_utils.SynTest):
4564
4564
  await core1.addFeedData('syn.nodes', data)
4565
4565
  self.len(1, await core1.nodes('test:int=8 -#test.12345'))
4566
4566
 
4567
- # This tag does match regex
4568
4567
  data = [(('test:int', 8), {'tags': {'test.1234': (None, None)}})]
4569
4568
  await core1.addFeedData('syn.nodes', data)
4570
- self.len(0, await core1.nodes('test:int=8 -#test.1234'))
4569
+ self.len(0, await core1.nodes('test:int=8 -#newtag.1234'))
4571
4570
 
4572
4571
  core1.view.layers[0].readonly = True
4573
4572
  await self.asyncraises(s_exc.IsReadOnly, core1.addFeedData('syn.nodes', data))
@@ -7809,6 +7808,9 @@ class CortexBasicTest(s_t_utils.SynTest):
7809
7808
  self.stormHasNoWarnErr(msgs)
7810
7809
  self.stormIsInPrint('AHA service (01.core...) added to service pool (pool00.loop.vertex.link)', msgs)
7811
7810
 
7811
+ msgs = await core00.stormlist('cortex.storm.pool.set newp')
7812
+ self.stormIsInErr(':// not found in [newp]', msgs)
7813
+
7812
7814
  msgs = await core00.stormlist('cortex.storm.pool.set --connection-timeout 1 --sync-timeout 1 aha://pool00...')
7813
7815
  self.stormHasNoWarnErr(msgs)
7814
7816
  self.stormIsInPrint('Storm pool configuration set.', msgs)
@@ -7817,6 +7819,8 @@ class CortexBasicTest(s_t_utils.SynTest):
7817
7819
 
7818
7820
  core00 = await base.enter_context(self.getTestCore(dirn=dirn00))
7819
7821
 
7822
+ await core00.stormpool.waitready(timeout=12)
7823
+
7820
7824
  with self.getLoggerStream('synapse') as stream:
7821
7825
  msgs = await alist(core00.storm('inet:asn=0'))
7822
7826
  self.len(1, [m for m in msgs if m[0] == 'node'])
@@ -7906,28 +7910,28 @@ class CortexBasicTest(s_t_utils.SynTest):
7906
7910
 
7907
7911
  stream.seek(0)
7908
7912
  data = stream.read()
7909
- self.isin('Unable to get proxy', data)
7913
+ self.isin('Storm query mirror pool is empty, running query locally.', data)
7910
7914
 
7911
7915
  with self.getLoggerStream('synapse') as stream:
7912
7916
  self.true(await core00.callStorm('inet:asn=0 return($lib.true)'))
7913
7917
 
7914
7918
  stream.seek(0)
7915
7919
  data = stream.read()
7916
- self.isin('Unable to get proxy', data)
7920
+ self.isin('Storm query mirror pool is empty, running query locally.', data)
7917
7921
 
7918
7922
  with self.getLoggerStream('synapse') as stream:
7919
7923
  self.len(1, await alist(core00.exportStorm('inet:asn=0')))
7920
7924
 
7921
7925
  stream.seek(0)
7922
7926
  data = stream.read()
7923
- self.isin('Unable to get proxy', data)
7927
+ self.isin('Storm query mirror pool is empty, running query locally.', data)
7924
7928
 
7925
7929
  with self.getLoggerStream('synapse') as stream:
7926
7930
  self.eq(1, await core00.count('inet:asn=0'))
7927
7931
 
7928
7932
  stream.seek(0)
7929
7933
  data = stream.read()
7930
- self.isin('Unable to get proxy', data)
7934
+ self.isin('Storm query mirror pool is empty, running query locally.', data)
7931
7935
 
7932
7936
  core01 = await base.enter_context(self.getTestCore(dirn=dirn01))
7933
7937
  await core01.promote(graceful=True)
@@ -7984,3 +7988,33 @@ class CortexBasicTest(s_t_utils.SynTest):
7984
7988
 
7985
7989
  msgs = await alist(core01.storm('inet:asn=0', opts={'mirror': False}))
7986
7990
  self.len(1, [m for m in msgs if m[0] == 'node'])
7991
+
7992
+ async def test_cortex_authgate(self):
7993
+ # TODO - Remove this in 3.0.0
7994
+ with self.getTestDir() as dirn:
7995
+
7996
+ async with self.getTestCore(dirn=dirn) as core: # type: s_cortex.Cortex
7997
+
7998
+ unfo = await core.addUser('lowuser')
7999
+ lowuser = unfo.get('iden')
8000
+
8001
+ msgs = await core.stormlist('auth.user.addrule lowuser --gate cortex node')
8002
+ self.stormIsInWarn('Adding rule on the "cortex" authgate. This authgate is not used', msgs)
8003
+ msgs = await core.stormlist('auth.role.addrule all --gate cortex hehe')
8004
+ self.stormIsInWarn('Adding rule on the "cortex" authgate. This authgate is not used', msgs)
8005
+
8006
+ aslow = {'user': lowuser}
8007
+
8008
+ # The cortex authgate does nothing
8009
+ with self.raises(s_exc.AuthDeny) as cm:
8010
+ await core.nodes('[test:str=hello]', opts=aslow)
8011
+
8012
+ with self.getAsyncLoggerStream('synapse.cortex') as stream:
8013
+ async with self.getTestCore(dirn=dirn) as core: # type: s_cortex.Cortex
8014
+ # The cortex authgate still does nothing
8015
+ with self.raises(s_exc.AuthDeny) as cm:
8016
+ await core.nodes('[test:str=hello]', opts=aslow)
8017
+ stream.seek(0)
8018
+ buf = stream.read()
8019
+ self.isin('(lowuser) has a rule on the "cortex" authgate', buf)
8020
+ self.isin('(all) has a rule on the "cortex" authgate', buf)
@@ -41,6 +41,28 @@ class DeprecatedModel(s_module.CoreModule):
41
41
 
42
42
  class DataModelTest(s_t_utils.SynTest):
43
43
 
44
+ async def test_datamodel_basics(self):
45
+ async with self.getTestCore() as core:
46
+ core.model.addType('woot:one', 'guid', {}, {
47
+ 'display': {
48
+ 'columns': (
49
+ {'type': 'newp', 'opts': {}},
50
+ ),
51
+ },
52
+ })
53
+ with self.raises(s_exc.BadFormDef):
54
+ core.model.addForm('woot:one', {}, ())
55
+
56
+ core.model.addType('woot:two', 'guid', {}, {
57
+ 'display': {
58
+ 'columns': (
59
+ {'type': 'prop', 'opts': {'name': 'hehe'}},
60
+ ),
61
+ },
62
+ })
63
+ with self.raises(s_exc.BadFormDef):
64
+ core.model.addForm('woot:two', {}, ())
65
+
44
66
  async def test_datamodel_formname(self):
45
67
  modl = s_datamodel.Model()
46
68
  mods = (