diff --git a/synapse/cortex.py b/synapse/cortex.py index 75087c502ef..7e94b41acc8 100644 --- a/synapse/cortex.py +++ b/synapse/cortex.py @@ -6440,6 +6440,83 @@ async def delVault(self, iden): self.slab.delete(name.encode(), db=self.vaultsbynamedb) self.slab.delete(bidn, db=self.vaultsdb) + def _propAllowedReason(self, user, perms, gateiden=None, default=None): + ''' + Similar to allowed, but always prefer the default value specified by the caller. + Default values are still pulled from permdefs if there is a match there; but still prefer caller default. + This results in a ternary response that can be used to know if a rule had a positive/negative or no match. + The matching reason metadata is also returned. + ''' + if default is None: + permdef = self.getPermDef(perms) + if permdef: + default = permdef.get('default', default) + + return user.getAllowedReason(perms, gateiden=gateiden, default=default) + + def confirmPropSet(self, user, prop, layriden): + meta0 = self._propAllowedReason(user, prop.setperms[0], gateiden=layriden) + + if meta0.isadmin: + return + + allowed0 = meta0.value + + meta1 = self._propAllowedReason(user, prop.setperms[1], gateiden=layriden) + allowed1 = meta1.value + + if allowed0: + if allowed1: + return + elif allowed1 is False: + # This is a allow-with-precedence case. + # Inspect meta to determine if the rule a0 is more specific than rule a1 + if len(meta0.rule) >= len(meta1.rule): + return + user.raisePermDeny(prop.setperms[0], gateiden=layriden) + return + + if allowed1: + if allowed0 is None: + return + # allowed0 here is False. This is a deny-with-precedence case. + # Inspect meta to determine if the rule a1 is more specific than rule a0 + if len(meta1.rule) > len(meta0.rule): + return + + user.raisePermDeny(prop.setperms[0], gateiden=layriden) + + def confirmPropDel(self, user, prop, layriden): + meta0 = self._propAllowedReason(user, prop.delperms[0], gateiden=layriden) + + if meta0.isadmin: + return + + allowed0 = meta0.value + meta1 = self._propAllowedReason(user, prop.delperms[1], gateiden=layriden) + allowed1 = meta1.value + + if allowed0: + if allowed1: + return + elif allowed1 is False: + # This is a allow-with-precedence case. + # Inspect meta to determine if the rule a0 is more specific than rule a1 + if len(meta0.rule) >= len(meta1.rule): + return + user.raisePermDeny(prop.delperms[0], gateiden=layriden) + return + + if allowed1: + if allowed0 is None: + return + # allowed0 here is False. This is a deny-with-precedence case. + # Inspect meta to determine if the rule a1 is more specific than rule a0 + if len(meta1.rule) > len(meta0.rule): + return + + user.raisePermDeny(prop.delperms[0], gateiden=layriden) + @contextlib.asynccontextmanager async def getTempCortex(mods=None): ''' diff --git a/synapse/lib/coro.py b/synapse/lib/coro.py index 6097093c017..fc4706aaca4 100644 --- a/synapse/lib/coro.py +++ b/synapse/lib/coro.py @@ -39,7 +39,7 @@ async def agen(item): for x in item: yield x -async def pause(genr, iterations=1000): +async def pause(genr, iterations=10): idx = 0 async for out in agen(genr): diff --git a/synapse/lib/layer.py b/synapse/lib/layer.py index 66d8fc131d0..b8aec85d82e 100644 --- a/synapse/lib/layer.py +++ b/synapse/lib/layer.py @@ -4577,91 +4577,120 @@ async def iterTombstones(self): for _, nid in self.layrslab.scanByPref(lkey, db=self.indxdb): yield (nid, tombtype, tombinfo) - async def iterLayerAddPerms(self): + async def confirmLayerEditPerms(self, user, gateiden, delete=False): + if user.allowed(('node',), gateiden=gateiden): + return + + if delete: + perm_forms = ('node', 'del') + perm_props = ('node', 'prop', 'del') + perm_tags = ('node', 'tag', 'del') + perm_ndata = ('node', 'data', 'pop') + perm_edges = ('node', 'edge', 'del') + else: + perm_forms = ('node', 'add') + perm_props = ('node', 'prop', 'set') + perm_tags = ('node', 'tag', 'add') + perm_ndata = ('node', 'data', 'set') + perm_edges = ('node', 'edge', 'add') + + allow_forms = user.allowed(perm_forms, gateiden=gateiden) + allow_props = user.allowed(perm_props, gateiden=gateiden) + allow_tags = user.allowed(perm_tags, gateiden=gateiden) + allow_ndata = user.allowed(perm_ndata, gateiden=gateiden) + allow_edges = user.allowed(perm_edges, gateiden=gateiden) + + if all((allow_forms, allow_props, allow_tags, allow_ndata, allow_edges)): + return # nodes & props - async for byts, abrv in s_coro.pause(self.propabrv.slab.scanByFull(db=self.propabrv.name2abrv)): - form, prop = s_msgpack.un(byts) - if form is None: - continue + if not allow_forms or not allow_props: + async for byts, abrv in s_coro.pause(self.propabrv.slab.scanByFull(db=self.propabrv.name2abrv)): + form, prop = s_msgpack.un(byts) + if form is None: # pragma: no cover + continue - if self.layrslab.prefexists(abrv, db=self.byprop): - if prop: - yield ('node', 'prop', 'set', f'{form}:{prop}') - else: - yield ('node', 'add', form) + if self.layrslab.prefexists(abrv, db=self.byprop): + if prop and not allow_props: + realform = self.core.model.form(form) + if not realform: # pragma: no cover + mesg = f'Invalid form: {form}' + raise s_exc.NoSuchForm(mesg=mesg, form=form) + + realprop = realform.prop(prop) + if not realprop: # pragma: no cover + mesg = f'Invalid prop: {form}:{prop}' + raise s_exc.NoSuchProp(mesg=mesg, form=form, prop=prop) + + if delete: + self.core.confirmPropDel(user, realprop, gateiden) + else: + self.core.confirmPropSet(user, realprop, gateiden) + + elif not prop and not allow_forms: + user.confirm(perm_forms + (form,), gateiden=gateiden) # tagprops - async for byts, abrv in s_coro.pause(self.tagpropabrv.slab.scanByFull(db=self.tagpropabrv.name2abrv)): - info = s_msgpack.un(byts) - if None in info or len(info) != 3: - continue + if not allow_tags: + async for byts, abrv in s_coro.pause(self.tagpropabrv.slab.scanByFull(db=self.tagpropabrv.name2abrv)): + info = s_msgpack.un(byts) + if None in info or len(info) != 3: + continue - if self.layrslab.prefexists(abrv, db=self.bytagprop): - yield ('node', 'tag', 'add', *info[1].split('.')) + if self.layrslab.prefexists(abrv, db=self.bytagprop): + perm = perm_tags + tuple(info[1].split('.')) + user.confirm(perm, gateiden=gateiden) # nodedata - async for abrv in s_coro.pause(self.dataslab.scanKeys(db=self.dataname)): - name, _ = self.getAbrvProp(abrv) - yield ('node', 'data', 'set', name) + if not allow_ndata: + async for abrv in s_coro.pause(self.dataslab.scanKeys(db=self.dataname)): + name, _ = self.getAbrvProp(abrv) + perm = perm_ndata + (name,) + user.confirm(perm, gateiden=gateiden) # edges - async for verb in s_coro.pause(self.layrslab.scanKeys(db=self.byverb)): - yield ('node', 'edge', 'add', verb.decode()) + if not allow_edges: + async for verb in s_coro.pause(self.layrslab.scanKeys(db=self.byverb)): + perm = perm_edges + (verb.decode(),) + user.confirm(perm, gateiden=gateiden) # tags # NB: tag perms should be yielded for every leaf on every node in the layer - async with self.core.getSpooledDict() as tags: - - # Collect all tag abrvs for all nodes in the layer - async for lkey, buid in s_coro.pause(self.layrslab.scanByFull(db=self.bytag)): - abrv = lkey[:8] - abrvs = list(tags.get(buid, [])) - abrvs.append(abrv) - await tags.set(buid, abrvs) - - # Iterate over each node and it's tags - async for buid, abrvs in s_coro.pause(tags.items()): - seen = {} - - if len(abrvs) == 1: - # Easy optimization: If there's only one tag abrv, then it's a - # leaf by default - name = self.tagabrv.abrvToName(abrv) - key = tuple(name.split('.')) - yield ('node', 'tag', 'add', *key) - - else: - for abrv in abrvs: + if not allow_tags: + async with self.core.getSpooledDict() as tags: + + # Collect all tag abrvs for all nodes in the layer + async for lkey, buid in s_coro.pause(self.layrslab.scanByFull(db=self.bytag)): + abrv = lkey[:8] + abrvs = list(tags.get(buid, [])) + abrvs.append(abrv) + await tags.set(buid, abrvs) + + # Iterate over each node and it's tags + async for buid, abrvs in s_coro.pause(tags.items()): + seen = {} + + if len(abrvs) == 1: + # Easy optimization: If there's only one tag abrv, then it's a + # leaf by default name = self.tagabrv.abrvToName(abrv) - parts = tuple(name.split('.')) - for idx in range(1, len(parts) + 1): - key = tuple(parts[:idx]) - seen.setdefault(key, 0) - seen[key] += 1 - - for key, count in seen.items(): - if count == 1: - yield ('node', 'tag', 'add', *key) - - async def iterLayerDelPerms(self): - async for perm in self.iterLayerAddPerms(): - if perm[:2] == ('node', 'add'): - yield ('node', 'del', *perm[2:]) - continue - - match perm[:3]: - case ('node', 'prop', 'set'): - yield ('node', 'prop', 'del', *perm[3:]) - - case ('node', 'tag', 'add'): - yield ('node', 'tag', 'del', *perm[3:]) + key = tuple(name.split('.')) + perm = perm_tags + key + user.confirm(perm, gateiden=gateiden) - case ('node', 'data', 'set'): - yield ('node', 'data', 'pop', *perm[3:]) - - case ('node', 'edge', 'add'): - yield ('node', 'edge', 'del', *perm[3:]) + else: + for abrv in abrvs: + name = self.tagabrv.abrvToName(abrv) + parts = tuple(name.split('.')) + for idx in range(1, len(parts) + 1): + key = tuple(parts[:idx]) + seen.setdefault(key, 0) + seen[key] += 1 + + for key, count in seen.items(): + if count == 1: + perm = perm_tags + key + user.confirm(perm, gateiden=gateiden) async def iterLayerNodeEdits(self): ''' diff --git a/synapse/lib/storm.py b/synapse/lib/storm.py index 45c176ac82e..eae2745cd21 100644 --- a/synapse/lib/storm.py +++ b/synapse/lib/storm.py @@ -2177,92 +2177,28 @@ def allowed(self, perms, gateiden=None, default=None): return self.user.allowed(perms, gateiden=gateiden, default=default) def allowedReason(self, perms, gateiden=None, default=None): - ''' - Similar to allowed, but always prefer the default value specified by the caller. - Default values are still pulled from permdefs if there is a match there; but still prefer caller default. - This results in a ternary response that can be used to know if a rule had a positive/negative or no match. - The matching reason metadata is also returned. - ''' if self.asroot: return self._admin_reason - if default is None: - permdef = self.view.core.getPermDef(perms) - if permdef: - default = permdef.get('default', default) - - return self.user.getAllowedReason(perms, gateiden=gateiden, default=default) + return self.snap.core._propAllowedReason(self.user, perms, gateiden=gateiden, default=default) def confirmPropSet(self, prop, layriden=None): + if self.asroot: + return if layriden is None: layriden = self.view.wlyr.iden - meta0 = self.allowedReason(prop.setperms[0], gateiden=layriden) - - if meta0.isadmin: - return - - allowed0 = meta0.value - - meta1 = self.allowedReason(prop.setperms[1], gateiden=layriden) - allowed1 = meta1.value - - if allowed0: - if allowed1: - return - elif allowed1 is False: - # This is a allow-with-precedence case. - # Inspect meta to determine if the rule a0 is more specific than rule a1 - if len(meta0.rule) >= len(meta1.rule): - return - self.user.raisePermDeny(prop.setperms[0], gateiden=layriden) - return - - if allowed1: - if allowed0 is None: - return - # allowed0 here is False. This is a deny-with-precedence case. - # Inspect meta to determine if the rule a1 is more specific than rule a0 - if len(meta1.rule) > len(meta0.rule): - return - - self.user.raisePermDeny(prop.setperms[0], gateiden=layriden) + return self.snap.core.confirmPropSet(self.user, prop, layriden=layriden) def confirmPropDel(self, prop, layriden=None): + if self.asroot: + return if layriden is None: layriden = self.view.wlyr.iden - meta0 = self.allowedReason(prop.delperms[0], gateiden=layriden) - - if meta0.isadmin: - return - - allowed0 = meta0.value - meta1 = self.allowedReason(prop.delperms[1], gateiden=layriden) - allowed1 = meta1.value - - if allowed0: - if allowed1: - return - elif allowed1 is False: - # This is a allow-with-precedence case. - # Inspect meta to determine if the rule a0 is more specific than rule a1 - if len(meta0.rule) >= len(meta1.rule): - return - self.user.raisePermDeny(prop.delperms[0], gateiden=layriden) - return - - if allowed1: - if allowed0 is None: - return - # allowed0 here is False. This is a deny-with-precedence case. - # Inspect meta to determine if the rule a1 is more specific than rule a0 - if len(meta1.rule) > len(meta0.rule): - return - - self.user.raisePermDeny(prop.delperms[0], gateiden=layriden) + return self.snap.core.confirmPropDel(self.user, prop, layriden=layriden) def confirmEasyPerm(self, item, perm, mesg=None): if not self.asroot: diff --git a/synapse/lib/stormlib/cell.py b/synapse/lib/stormlib/cell.py index e44f3f17a83..e269b306cf8 100644 --- a/synapse/lib/stormlib/cell.py +++ b/synapse/lib/stormlib/cell.py @@ -2,11 +2,16 @@ import logging import synapse.exc as s_exc -import synapse.lib.const as s_const +import synapse.lib.autodoc as s_autodoc import synapse.lib.stormtypes as s_stormtypes logger = logging.getLogger(__name__) +def prepHotfixDesc(txt): + lines = txt.split('\n') + lines = s_autodoc.scrubLines(lines) + lines = s_autodoc.ljuster(lines) + return lines storm_missing_autoadds = ''' $absoluteOrder = $lib.view.list(deporder=$lib.true) @@ -64,6 +69,17 @@ } ''' +storm_migrate_riskhasvuln = ''' +for $view in $lib.view.list(deporder=$lib.true) { + view.exec $view.iden { + $layer = $lib.layer.get() + for ($buid, $sode) in $layer.getStorNodesByForm(risk:hasvuln) { + yield $buid + $lib.model.migration.s.riskHasVulnToVulnerable($node) + } + } +} +''' hotfixes = ( ((1, 0, 0), { @@ -78,6 +94,20 @@ 'desc': 'Populate it:sec:cpe:v2_2 properties from existing CPE where the property is not set.', 'query': storm_missing_cpe22, }), + ((4, 0, 0), { + 'desc': ''' + Create risk:vulnerable nodes from existing risk:hasvuln nodes. + + This hotfix should only be applied after all logic that would create + risk:hasvuln nodes has been updated. The hotfix uses the + $lib.model.migration.s.riskHasVulnToVulnerable() function, + which can be used directly for testing. + + Tags, tag properties, edges, and node data will all be copied + to the risk:vulnerable nodes. + ''', + 'query': storm_migrate_riskhasvuln, + }), ) runtime_fixes_key = 'cortex:runtime:stormfixes' @@ -174,7 +204,9 @@ async def _hotFixesApply(self): assert desc is not None assert vars is not None - await self.runt.printf(f'Applying hotfix {vers} for [{desc}]') + title = prepHotfixDesc(desc)[0] + await self.runt.printf(f'Applying hotfix {vers} for [{title}]') + try: query = await self.runt.getStormQuery(text) async with self.runt.getSubRuntime(query, opts={'vars': vars}) as runt: @@ -206,8 +238,14 @@ async def _hotFixesCheck(self): continue dowork = True - desc = info.get('desc') - await self.runt.printf(f'Would apply fix {vers} for [{desc}]') + + desclines = prepHotfixDesc(info.get('desc')) + await self.runt.printf(f'Would apply fix {vers} for [{desclines[0]}]') + if len(desclines) > 1: + for line in desclines[1:]: + await self.runt.printf(f' {line}' if line else '') + else: + await self.runt.printf('') return dowork diff --git a/synapse/lib/stormlib/model.py b/synapse/lib/stormlib/model.py index d3f7d188901..fb584f2d763 100644 --- a/synapse/lib/stormlib/model.py +++ b/synapse/lib/stormlib/model.py @@ -1091,7 +1091,7 @@ async def _riskHasVulnToVulnerable(self, n, nodata=False): self.runt.confirmPropSet(riskvuln.props['vuln']) self.runt.confirmPropSet(riskvuln.props['node']) - if (seen := n.get('.seen')): + if seen := n.get('.seen'): self.runt.confirmPropSet(riskvuln.props['.seen']) props['.seen'] = seen diff --git a/synapse/lib/view.py b/synapse/lib/view.py index 0956470e941..be5916c1c40 100644 --- a/synapse/lib/view.py +++ b/synapse/lib/view.py @@ -2064,9 +2064,8 @@ async def mergeAllowed(self, user=None, force=False): if user is None or user.isAdmin() or user.isAdmin(gateiden=parentlayr.iden): return - async for perm in self.wlyr.iterLayerAddPerms(): - self.parent._confirm(user, perm) - await asyncio.sleep(0) + fromlayr = self.layers[0] + await fromlayr.confirmLayerEditPerms(user, parentlayr.iden) async def wipeAllowed(self, user=None): ''' @@ -2075,9 +2074,8 @@ async def wipeAllowed(self, user=None): if user is None or user.isAdmin(): return - async for perm in self.wlyr.iterLayerDelPerms(): - self._confirm(user, perm) - await asyncio.sleep(0) + layer = self.layers[0] + await layer.confirmLayerEditPerms(user, layer.iden, delete=True) async def runTagAdd(self, node, tag, valu): diff --git a/synapse/models/inet.py b/synapse/models/inet.py index 44b9b0f6a54..b5d375c954d 100644 --- a/synapse/models/inet.py +++ b/synapse/models/inet.py @@ -1419,7 +1419,7 @@ def getModelDefs(self): 'doc': 'A channel within a web service or instance such as slack or discord.' }), - ('inet:web:hashtag', ('str', {'lower': True, 'regex': r'^#[\w]+$'}), { + ('inet:web:hashtag', ('str', {'lower': True, 'regex': r'^#\w[\w·]*(? it:prod:softver +:name=view0', opts={'view': view0})) + self.len(1, await core.nodes('risk:vulnerable -> it:prod:softver +:name=view1', opts={'view': view1})) + self.len(1, await core.nodes('risk:vulnerable -> it:host', opts={'view': view2})) diff --git a/synapse/tests/test_lib_view.py b/synapse/tests/test_lib_view.py index 1bb4e60ec8c..964533881e4 100644 --- a/synapse/tests/test_lib_view.py +++ b/synapse/tests/test_lib_view.py @@ -739,7 +739,7 @@ async def test_lib_view_merge_perms(self): with self.raises(s_exc.AuthDeny) as cm: await core.nodes('$lib.view.get().merge()', opts=viewopts) - self.eq('node.prop.set.syn:tag:base', cm.exception.errinfo['perm']) + self.eq('node.prop.set.syn:tag.base', cm.exception.errinfo['perm']) await core.addUserRule(useriden, (True, ('node', 'prop', 'set')), gateiden=baselayr) diff --git a/synapse/tests/test_model_inet.py b/synapse/tests/test_model_inet.py index abc573f601e..4d5b0d61e4c 100644 --- a/synapse/tests/test_model_inet.py +++ b/synapse/tests/test_model_inet.py @@ -1,4 +1,3 @@ -import copy import logging import synapse.exc as s_exc @@ -12,10 +11,16 @@ class InetModelTest(s_t_utils.SynTest): async def test_model_inet_basics(self): async with self.getTestCore() as core: self.len(1, await core.nodes('[ inet:web:hashtag="#hehe" ]')) + self.len(1, await core.nodes('[ inet:web:hashtag="#foo·bar"]')) # note the interpunct + self.len(1, await core.nodes('[ inet:web:hashtag="#fo·o·······b·ar"]')) with self.raises(s_exc.BadTypeValu): await core.nodes('[ inet:web:hashtag="foo" ]') with self.raises(s_exc.BadTypeValu): await core.nodes('[ inet:web:hashtag="#foo bar" ]') + with self.raises(s_exc.BadTypeValu): + self.len(1, await core.nodes('[ inet:web:hashtag="#·bar"]')) + with self.raises(s_exc.BadTypeValu): + self.len(1, await core.nodes('[ inet:web:hashtag="#foo·"]')) nodes = await core.nodes(''' [ inet:web:instance=(foo,)