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.
- synapse/cmds/cortex.py +1 -6
- synapse/common.py +6 -0
- synapse/cortex.py +73 -56
- synapse/datamodel.py +32 -0
- synapse/lib/agenda.py +81 -51
- synapse/lib/ast.py +21 -23
- synapse/lib/base.py +0 -6
- synapse/lib/cell.py +13 -22
- synapse/lib/httpapi.py +1 -0
- synapse/lib/nexus.py +3 -2
- synapse/lib/schemas.py +2 -0
- synapse/lib/snap.py +50 -0
- synapse/lib/storm.py +19 -17
- synapse/lib/stormlib/aha.py +4 -1
- synapse/lib/stormlib/auth.py +11 -4
- synapse/lib/stormlib/cache.py +202 -0
- synapse/lib/stormlib/cortex.py +69 -7
- synapse/lib/stormlib/spooled.py +109 -0
- synapse/lib/stormtypes.py +43 -15
- synapse/lib/trigger.py +10 -12
- synapse/lib/types.py +1 -1
- synapse/lib/version.py +2 -2
- synapse/lib/view.py +12 -0
- synapse/models/inet.py +74 -2
- synapse/models/orgs.py +52 -8
- synapse/models/person.py +30 -11
- synapse/models/risk.py +44 -3
- synapse/telepath.py +114 -32
- synapse/tests/test_cortex.py +40 -6
- synapse/tests/test_datamodel.py +22 -0
- synapse/tests/test_lib_agenda.py +8 -1
- synapse/tests/test_lib_aha.py +18 -4
- synapse/tests/test_lib_storm.py +95 -4
- synapse/tests/test_lib_stormlib_cache.py +272 -0
- synapse/tests/test_lib_stormlib_cortex.py +71 -0
- synapse/tests/test_lib_stormlib_spooled.py +190 -0
- synapse/tests/test_lib_stormtypes.py +27 -4
- synapse/tests/test_model_inet.py +67 -0
- synapse/tests/test_model_risk.py +6 -0
- synapse/tests/test_telepath.py +30 -7
- synapse/tests/test_tools_modrole.py +81 -0
- synapse/tests/test_tools_moduser.py +105 -0
- synapse/tools/modrole.py +59 -7
- synapse/tools/moduser.py +78 -10
- {synapse-2.165.0.dist-info → synapse-2.166.0.dist-info}/METADATA +2 -2
- {synapse-2.165.0.dist-info → synapse-2.166.0.dist-info}/RECORD +49 -47
- synapse/lib/provenance.py +0 -111
- synapse/tests/test_lib_provenance.py +0 -37
- {synapse-2.165.0.dist-info → synapse-2.166.0.dist-info}/LICENSE +0 -0
- {synapse-2.165.0.dist-info → synapse-2.166.0.dist-info}/WHEEL +0 -0
- {synapse-2.165.0.dist-info → synapse-2.166.0.dist-info}/top_level.txt +0 -0
|
@@ -2,6 +2,32 @@ import synapse.lib.output as s_output
|
|
|
2
2
|
import synapse.tests.utils as s_test
|
|
3
3
|
import synapse.tools.moduser as s_t_moduser
|
|
4
4
|
|
|
5
|
+
userlist = '''
|
|
6
|
+
Users:
|
|
7
|
+
root
|
|
8
|
+
visi
|
|
9
|
+
'''.strip()
|
|
10
|
+
|
|
11
|
+
userinfo = s_test.deguidify('''
|
|
12
|
+
User: visi (04dddd4ff39e4ce00b36c7d526b9eac7)
|
|
13
|
+
|
|
14
|
+
Locked: False
|
|
15
|
+
Admin: False
|
|
16
|
+
Email: visi@test.com
|
|
17
|
+
Rules:
|
|
18
|
+
[0 ] - !foo.bar.baz
|
|
19
|
+
[1 ] - foo.bar
|
|
20
|
+
|
|
21
|
+
Roles:
|
|
22
|
+
576a948f9944c58d3953f0d36bc2da81 - all
|
|
23
|
+
|
|
24
|
+
Gates:
|
|
25
|
+
c7b276154c0c799430668cb3c4cd259d
|
|
26
|
+
Admin: False
|
|
27
|
+
[0 ] - !bar.baz.faz
|
|
28
|
+
[1 ] - bar.baz
|
|
29
|
+
'''.strip())
|
|
30
|
+
|
|
5
31
|
class ModUserTest(s_test.SynTest):
|
|
6
32
|
|
|
7
33
|
async def test_tools_moduser(self):
|
|
@@ -124,6 +150,78 @@ class ModUserTest(s_test.SynTest):
|
|
|
124
150
|
self.true(bool(visi.allowed('foo.bar.gaz'.split('.'))))
|
|
125
151
|
self.false(bool(visi.allowed('foo.bar.baz'.split('.'))))
|
|
126
152
|
|
|
153
|
+
gateiden = core.getLayer().iden
|
|
154
|
+
argv = (
|
|
155
|
+
'--svcurl', svcurl,
|
|
156
|
+
'visi',
|
|
157
|
+
'--admin', 'true',
|
|
158
|
+
'--gate', gateiden,
|
|
159
|
+
)
|
|
160
|
+
outp = s_output.OutPutStr()
|
|
161
|
+
self.eq(0, await s_t_moduser.main(argv, outp=outp))
|
|
162
|
+
self.isin(f'...setting admin: true on gate {gateiden}', str(outp))
|
|
163
|
+
|
|
164
|
+
gate = await core.getAuthGate(gateiden)
|
|
165
|
+
for user in gate['users']:
|
|
166
|
+
if user['iden'] == visi.iden:
|
|
167
|
+
self.true(user['admin'])
|
|
168
|
+
|
|
169
|
+
gateiden = core.getLayer().iden
|
|
170
|
+
argv = (
|
|
171
|
+
'--svcurl', svcurl,
|
|
172
|
+
'visi',
|
|
173
|
+
'--admin', 'false',
|
|
174
|
+
'--allow', 'bar.baz',
|
|
175
|
+
'--deny', 'bar.baz.faz',
|
|
176
|
+
'--gate', gateiden,
|
|
177
|
+
)
|
|
178
|
+
outp = s_output.OutPutStr()
|
|
179
|
+
self.eq(0, await s_t_moduser.main(argv, outp=outp))
|
|
180
|
+
self.isin(f'...setting admin: false on gate {gateiden}', str(outp))
|
|
181
|
+
self.isin(f'...adding allow rule: bar.baz on gate {gateiden}', str(outp))
|
|
182
|
+
self.isin(f'...adding deny rule: bar.baz.faz on gate {gateiden}', str(outp))
|
|
183
|
+
|
|
184
|
+
gate = await core.getAuthGate(gateiden)
|
|
185
|
+
for user in gate['users']:
|
|
186
|
+
if user['iden'] == visi.iden:
|
|
187
|
+
self.isin((True, ('bar', 'baz')), user['rules'])
|
|
188
|
+
self.isin((False, ('bar', 'baz', 'faz')), user['rules'])
|
|
189
|
+
|
|
190
|
+
argv = (
|
|
191
|
+
'--svcurl', svcurl,
|
|
192
|
+
'--list',
|
|
193
|
+
)
|
|
194
|
+
outp = s_output.OutPutStr()
|
|
195
|
+
self.eq(0, await s_t_moduser.main(argv, outp=outp))
|
|
196
|
+
self.isin(userlist, str(outp))
|
|
197
|
+
|
|
198
|
+
argv = (
|
|
199
|
+
'--svcurl', svcurl,
|
|
200
|
+
'--list',
|
|
201
|
+
'visi',
|
|
202
|
+
)
|
|
203
|
+
outp = s_output.OutPutStr()
|
|
204
|
+
self.eq(0, await s_t_moduser.main(argv, outp=outp))
|
|
205
|
+
self.isin(userinfo, s_test.deguidify(str(outp)))
|
|
206
|
+
|
|
207
|
+
argv = (
|
|
208
|
+
'--svcurl', svcurl,
|
|
209
|
+
'--list',
|
|
210
|
+
'newpuser',
|
|
211
|
+
)
|
|
212
|
+
outp = s_output.OutPutStr()
|
|
213
|
+
self.eq(1, await s_t_moduser.main(argv, outp=outp))
|
|
214
|
+
self.isin('ERROR: User not found: newpuser', str(outp))
|
|
215
|
+
|
|
216
|
+
argv = (
|
|
217
|
+
'--svcurl', svcurl,
|
|
218
|
+
'visi',
|
|
219
|
+
'--gate', 'newp',
|
|
220
|
+
)
|
|
221
|
+
outp = s_output.OutPutStr()
|
|
222
|
+
self.eq(1, await s_t_moduser.main(argv, outp=outp))
|
|
223
|
+
self.isin('ERROR: No auth gate found with iden: newp', str(outp))
|
|
224
|
+
|
|
127
225
|
argv = (
|
|
128
226
|
'--svcurl', svcurl,
|
|
129
227
|
'visi',
|
|
@@ -151,3 +249,10 @@ class ModUserTest(s_test.SynTest):
|
|
|
151
249
|
outp = s_output.OutPutStr()
|
|
152
250
|
self.eq(1, await s_t_moduser.main(argv, outp=outp))
|
|
153
251
|
self.isin('ERROR: User not found (need --add?): visi', str(outp))
|
|
252
|
+
|
|
253
|
+
argv = (
|
|
254
|
+
'--svcurl', svcurl,
|
|
255
|
+
)
|
|
256
|
+
outp = s_output.OutPutStr()
|
|
257
|
+
self.eq(1, await s_t_moduser.main(argv, outp=outp))
|
|
258
|
+
self.isin('ERROR: A username argument is required when --list is not specified.', str(outp))
|
synapse/tools/modrole.py
CHANGED
|
@@ -11,15 +11,34 @@ descr = '''
|
|
|
11
11
|
Add or modify a role in a Synapse service.
|
|
12
12
|
'''
|
|
13
13
|
|
|
14
|
+
def printrole(role, outp):
|
|
15
|
+
|
|
16
|
+
outp.printf(f'Role: {role.get("name")} ({role.get("iden")})')
|
|
17
|
+
outp.printf('')
|
|
18
|
+
outp.printf(' Rules:')
|
|
19
|
+
for indx, rule in enumerate(role.get('rules')):
|
|
20
|
+
outp.printf(f' [{str(indx).ljust(3)}] - {s_common.reprauthrule(rule)}')
|
|
21
|
+
|
|
22
|
+
outp.printf('')
|
|
23
|
+
outp.printf(' Gates:')
|
|
24
|
+
for gateiden, gateinfo in role.get('authgates', {}).items():
|
|
25
|
+
outp.printf(f' {gateiden}')
|
|
26
|
+
outp.printf(f' Admin: {gateinfo.get("admin") == True}')
|
|
27
|
+
for indx, rule in enumerate(gateinfo.get('rules', ())):
|
|
28
|
+
outp.printf(f' [{str(indx).ljust(3)}] - {s_common.reprauthrule(rule)}')
|
|
29
|
+
|
|
14
30
|
async def main(argv, outp=s_output.stdout):
|
|
15
31
|
|
|
16
32
|
pars = argparse.ArgumentParser(prog='modrole', description=descr)
|
|
17
33
|
pars.add_argument('--svcurl', default='cell:///vertex/storage', help='The telepath URL of the Synapse service.')
|
|
18
34
|
pars.add_argument('--add', default=False, action='store_true', help='Add the role if they do not already exist.')
|
|
19
35
|
pars.add_argument('--del', dest='delete', default=False, action='store_true', help='Delete the role if it exists.')
|
|
36
|
+
pars.add_argument('--list', default=False, action='store_true',
|
|
37
|
+
help='List existing roles, or rules of a specific role.')
|
|
20
38
|
pars.add_argument('--allow', default=[], action='append', help='A permission string to allow for the role.')
|
|
21
39
|
pars.add_argument('--deny', default=[], action='append', help='A permission string to deny for the role.')
|
|
22
|
-
pars.add_argument('
|
|
40
|
+
pars.add_argument('--gate', default=None, help='The iden of an auth gate to add/del rules on.')
|
|
41
|
+
pars.add_argument('rolename', nargs='?', help='The rolename to add/edit.')
|
|
23
42
|
|
|
24
43
|
opts = pars.parse_args(argv)
|
|
25
44
|
|
|
@@ -31,6 +50,31 @@ async def main(argv, outp=s_output.stdout):
|
|
|
31
50
|
|
|
32
51
|
async with await s_telepath.openurl(opts.svcurl) as cell:
|
|
33
52
|
|
|
53
|
+
if opts.list:
|
|
54
|
+
if opts.rolename:
|
|
55
|
+
role = await cell.getRoleDefByName(opts.rolename)
|
|
56
|
+
if role is None:
|
|
57
|
+
outp.printf(f'ERROR: Role not found: {opts.rolename}')
|
|
58
|
+
return 1
|
|
59
|
+
|
|
60
|
+
printrole(role, outp)
|
|
61
|
+
|
|
62
|
+
else:
|
|
63
|
+
outp.printf('Roles:')
|
|
64
|
+
for role in await cell.getRoleDefs():
|
|
65
|
+
outp.printf(f' {role.get("iden")} - {role.get("name")}')
|
|
66
|
+
|
|
67
|
+
return 0
|
|
68
|
+
elif opts.rolename is None:
|
|
69
|
+
outp.printf(f'ERROR: A rolename argument is required when --list is not specified.')
|
|
70
|
+
return 1
|
|
71
|
+
|
|
72
|
+
if opts.gate:
|
|
73
|
+
gate = await cell.getAuthGate(opts.gate)
|
|
74
|
+
if gate is None:
|
|
75
|
+
outp.printf(f'ERROR: No auth gate found with iden: {opts.gate}')
|
|
76
|
+
return 1
|
|
77
|
+
|
|
34
78
|
role = await cell.getRoleDefByName(opts.rolename)
|
|
35
79
|
if role is not None:
|
|
36
80
|
outp.printf(f'Modifying role: {opts.rolename}')
|
|
@@ -52,15 +96,23 @@ async def main(argv, outp=s_output.stdout):
|
|
|
52
96
|
|
|
53
97
|
for allow in opts.allow:
|
|
54
98
|
perm = allow.lower().split('.')
|
|
55
|
-
|
|
56
|
-
if
|
|
57
|
-
|
|
99
|
+
mesg = f'...adding allow rule: {allow}'
|
|
100
|
+
if opts.gate:
|
|
101
|
+
mesg += f' on gate {opts.gate}'
|
|
102
|
+
|
|
103
|
+
outp.printf(mesg)
|
|
104
|
+
if not await cell.isRoleAllowed(roleiden, perm, gateiden=opts.gate):
|
|
105
|
+
await cell.addRoleRule(roleiden, (True, perm), indx=0, gateiden=opts.gate)
|
|
58
106
|
|
|
59
107
|
for deny in opts.deny:
|
|
60
108
|
perm = deny.lower().split('.')
|
|
61
|
-
|
|
62
|
-
if
|
|
63
|
-
|
|
109
|
+
mesg = f'...adding deny rule: {deny}'
|
|
110
|
+
if opts.gate:
|
|
111
|
+
mesg += f' on gate {opts.gate}'
|
|
112
|
+
|
|
113
|
+
outp.printf(mesg)
|
|
114
|
+
if await cell.isRoleAllowed(roleiden, perm, gateiden=opts.gate):
|
|
115
|
+
await cell.addRoleRule(roleiden, (False, perm), indx=0, gateiden=opts.gate)
|
|
64
116
|
return 0
|
|
65
117
|
|
|
66
118
|
if __name__ == '__main__': # pragma: no cover
|
synapse/tools/moduser.py
CHANGED
|
@@ -9,15 +9,44 @@ import synapse.telepath as s_telepath
|
|
|
9
9
|
import synapse.lib.output as s_output
|
|
10
10
|
|
|
11
11
|
descr = '''
|
|
12
|
-
Add or
|
|
12
|
+
Add, modify, or list users of a Synapse service.
|
|
13
13
|
'''
|
|
14
14
|
|
|
15
|
+
def printuser(user, outp):
|
|
16
|
+
|
|
17
|
+
admin = user.get('admin')
|
|
18
|
+
authtype = user.get('type')
|
|
19
|
+
|
|
20
|
+
outp.printf(f'User: {user.get("name")} ({user.get("iden")})')
|
|
21
|
+
outp.printf('')
|
|
22
|
+
outp.printf(f' Locked: {user.get("locked")}')
|
|
23
|
+
outp.printf(f' Admin: {user.get("admin")}')
|
|
24
|
+
outp.printf(f' Email: {user.get("email")}')
|
|
25
|
+
outp.printf(' Rules:')
|
|
26
|
+
for indx, rule in enumerate(user.get('rules')):
|
|
27
|
+
outp.printf(f' [{str(indx).ljust(3)}] - {s_common.reprauthrule(rule)}')
|
|
28
|
+
|
|
29
|
+
outp.printf('')
|
|
30
|
+
outp.printf(' Roles:')
|
|
31
|
+
for role in user.get('roles'):
|
|
32
|
+
outp.printf(f' {role.get("iden")} - {role.get("name")}')
|
|
33
|
+
|
|
34
|
+
outp.printf('')
|
|
35
|
+
outp.printf(' Gates:')
|
|
36
|
+
for gateiden, gateinfo in user.get('authgates', {}).items():
|
|
37
|
+
outp.printf(f' {gateiden}')
|
|
38
|
+
outp.printf(f' Admin: {gateinfo.get("admin") == True}')
|
|
39
|
+
for indx, rule in enumerate(gateinfo.get('rules', ())):
|
|
40
|
+
outp.printf(f' [{str(indx).ljust(3)}] - {s_common.reprauthrule(rule)}')
|
|
41
|
+
|
|
15
42
|
async def main(argv, outp=s_output.stdout):
|
|
16
43
|
|
|
17
44
|
pars = argparse.ArgumentParser(prog='moduser', description=descr)
|
|
18
45
|
pars.add_argument('--svcurl', default='cell:///vertex/storage', help='The telepath URL of the Synapse service.')
|
|
19
46
|
pars.add_argument('--add', default=False, action='store_true', help='Add the user if they do not already exist.')
|
|
20
47
|
pars.add_argument('--del', dest='delete', default=False, action='store_true', help='Delete the user if they exist.')
|
|
48
|
+
pars.add_argument('--list', default=False, action='store_true',
|
|
49
|
+
help='List existing users of the service, or details of a specific user.')
|
|
21
50
|
pars.add_argument('--admin', choices=('true', 'false'), default=None, help='Set the user admin status.')
|
|
22
51
|
pars.add_argument('--passwd', action='store', type=str, help='A password to set for the user.')
|
|
23
52
|
pars.add_argument('--email', action='store', type=str, help='An email to set for the user.')
|
|
@@ -26,7 +55,8 @@ async def main(argv, outp=s_output.stdout):
|
|
|
26
55
|
pars.add_argument('--revoke', default=[], action='append', help='A role to revoke from the user.')
|
|
27
56
|
pars.add_argument('--allow', default=[], action='append', help='A permission string to allow for the user.')
|
|
28
57
|
pars.add_argument('--deny', default=[], action='append', help='A permission string to deny for the user.')
|
|
29
|
-
pars.add_argument('
|
|
58
|
+
pars.add_argument('--gate', default=None, help='The iden of an auth gate to add/del rules or set admin status on.')
|
|
59
|
+
pars.add_argument('username', nargs='?', help='The username to add/edit or show details.')
|
|
30
60
|
|
|
31
61
|
opts = pars.parse_args(argv)
|
|
32
62
|
|
|
@@ -38,6 +68,32 @@ async def main(argv, outp=s_output.stdout):
|
|
|
38
68
|
|
|
39
69
|
async with await s_telepath.openurl(opts.svcurl) as cell:
|
|
40
70
|
|
|
71
|
+
if opts.list:
|
|
72
|
+
if opts.username:
|
|
73
|
+
user = await cell.getUserDefByName(opts.username)
|
|
74
|
+
if user is None:
|
|
75
|
+
outp.printf(f'ERROR: User not found: {opts.username}')
|
|
76
|
+
return 1
|
|
77
|
+
|
|
78
|
+
printuser(user, outp)
|
|
79
|
+
|
|
80
|
+
else:
|
|
81
|
+
outp.printf('Users:')
|
|
82
|
+
for user in await cell.getUserDefs():
|
|
83
|
+
outp.printf(f' {user.get("name")}')
|
|
84
|
+
|
|
85
|
+
return 0
|
|
86
|
+
|
|
87
|
+
elif opts.username is None:
|
|
88
|
+
outp.printf(f'ERROR: A username argument is required when --list is not specified.')
|
|
89
|
+
return 1
|
|
90
|
+
|
|
91
|
+
if opts.gate:
|
|
92
|
+
gate = await cell.getAuthGate(opts.gate)
|
|
93
|
+
if gate is None:
|
|
94
|
+
outp.printf(f'ERROR: No auth gate found with iden: {opts.gate}')
|
|
95
|
+
return 1
|
|
96
|
+
|
|
41
97
|
grants = []
|
|
42
98
|
revokes = []
|
|
43
99
|
|
|
@@ -80,8 +136,12 @@ async def main(argv, outp=s_output.stdout):
|
|
|
80
136
|
|
|
81
137
|
if opts.admin is not None:
|
|
82
138
|
admin = s_common.yamlloads(opts.admin)
|
|
83
|
-
|
|
84
|
-
|
|
139
|
+
mesg = f'...setting admin: {opts.admin}'
|
|
140
|
+
if opts.gate:
|
|
141
|
+
mesg += f' on gate {opts.gate}'
|
|
142
|
+
|
|
143
|
+
outp.printf(mesg)
|
|
144
|
+
await cell.setUserAdmin(useriden, admin, gateiden=opts.gate)
|
|
85
145
|
|
|
86
146
|
if opts.locked is not None:
|
|
87
147
|
locked = s_common.yamlloads(opts.locked)
|
|
@@ -108,15 +168,23 @@ async def main(argv, outp=s_output.stdout):
|
|
|
108
168
|
|
|
109
169
|
for allow in opts.allow:
|
|
110
170
|
perm = allow.lower().split('.')
|
|
111
|
-
|
|
112
|
-
if
|
|
113
|
-
|
|
171
|
+
mesg = f'...adding allow rule: {allow}'
|
|
172
|
+
if opts.gate:
|
|
173
|
+
mesg += f' on gate {opts.gate}'
|
|
174
|
+
|
|
175
|
+
outp.printf(mesg)
|
|
176
|
+
if not await cell.isUserAllowed(useriden, perm, gateiden=opts.gate):
|
|
177
|
+
await cell.addUserRule(useriden, (True, perm), indx=0, gateiden=opts.gate)
|
|
114
178
|
|
|
115
179
|
for deny in opts.deny:
|
|
116
180
|
perm = deny.lower().split('.')
|
|
117
|
-
|
|
118
|
-
if
|
|
119
|
-
|
|
181
|
+
mesg = f'...adding deny rule: {deny}'
|
|
182
|
+
if opts.gate:
|
|
183
|
+
mesg += f' on gate {opts.gate}'
|
|
184
|
+
|
|
185
|
+
outp.printf(mesg)
|
|
186
|
+
if await cell.isUserAllowed(useriden, perm, gateiden=opts.gate):
|
|
187
|
+
await cell.addUserRule(useriden, (False, perm), indx=0, gateiden=opts.gate)
|
|
120
188
|
return 0
|
|
121
189
|
|
|
122
190
|
if __name__ == '__main__': # pragma: no cover
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: synapse
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.166.0
|
|
4
4
|
Summary: Synapse Intelligence Analysis Framework
|
|
5
5
|
Author-email: The Vertex Project LLC <root@vertex.link>
|
|
6
6
|
License: Apache License 2.0
|
|
@@ -36,7 +36,7 @@ Requires-Dist: lark ==1.1.9
|
|
|
36
36
|
Requires-Dist: Pygments <2.18.0,>=2.7.4
|
|
37
37
|
Requires-Dist: packaging <24.0,>=20.0
|
|
38
38
|
Requires-Dist: fastjsonschema <2.20.0,>=2.18.0
|
|
39
|
-
Requires-Dist: stix2-validator <
|
|
39
|
+
Requires-Dist: stix2-validator <3.2.0,>=3.0.0
|
|
40
40
|
Requires-Dist: vcrpy <5.2.0,>=4.3.1
|
|
41
41
|
Requires-Dist: base58 <2.2.0,>=2.1.0
|
|
42
42
|
Requires-Dist: python-bitcoinlib <0.13.0,>=0.11.0
|