synapse 2.169.0__py311-none-any.whl → 2.171.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 (41) hide show
  1. synapse/cortex.py +99 -3
  2. synapse/datamodel.py +5 -0
  3. synapse/lib/ast.py +70 -12
  4. synapse/lib/cell.py +76 -7
  5. synapse/lib/layer.py +75 -6
  6. synapse/lib/lmdbslab.py +17 -0
  7. synapse/lib/node.py +7 -0
  8. synapse/lib/snap.py +22 -4
  9. synapse/lib/storm.py +1 -1
  10. synapse/lib/stormlib/cortex.py +1 -1
  11. synapse/lib/stormlib/model.py +339 -40
  12. synapse/lib/stormtypes.py +58 -1
  13. synapse/lib/types.py +36 -1
  14. synapse/lib/version.py +2 -2
  15. synapse/lib/view.py +94 -15
  16. synapse/models/files.py +40 -0
  17. synapse/models/inet.py +8 -4
  18. synapse/models/infotech.py +355 -17
  19. synapse/tests/files/cpedata.json +525034 -0
  20. synapse/tests/test_cortex.py +108 -0
  21. synapse/tests/test_lib_ast.py +66 -0
  22. synapse/tests/test_lib_cell.py +112 -0
  23. synapse/tests/test_lib_layer.py +52 -1
  24. synapse/tests/test_lib_lmdbslab.py +36 -0
  25. synapse/tests/test_lib_scrape.py +72 -71
  26. synapse/tests/test_lib_snap.py +16 -1
  27. synapse/tests/test_lib_storm.py +118 -0
  28. synapse/tests/test_lib_stormlib_cortex.py +15 -0
  29. synapse/tests/test_lib_stormlib_model.py +427 -0
  30. synapse/tests/test_lib_stormtypes.py +147 -15
  31. synapse/tests/test_lib_types.py +21 -0
  32. synapse/tests/test_lib_view.py +77 -0
  33. synapse/tests/test_model_files.py +52 -0
  34. synapse/tests/test_model_inet.py +63 -1
  35. synapse/tests/test_model_infotech.py +187 -26
  36. synapse/tests/utils.py +42 -9
  37. {synapse-2.169.0.dist-info → synapse-2.171.0.dist-info}/METADATA +1 -1
  38. {synapse-2.169.0.dist-info → synapse-2.171.0.dist-info}/RECORD +41 -40
  39. {synapse-2.169.0.dist-info → synapse-2.171.0.dist-info}/LICENSE +0 -0
  40. {synapse-2.169.0.dist-info → synapse-2.171.0.dist-info}/WHEEL +0 -0
  41. {synapse-2.169.0.dist-info → synapse-2.171.0.dist-info}/top_level.txt +0 -0
synapse/lib/snap.py CHANGED
@@ -183,6 +183,15 @@ class ProtoNode:
183
183
 
184
184
  return s_common.novalu
185
185
 
186
+ async def hasData(self, name):
187
+ if name in self.nodedata:
188
+ return True
189
+
190
+ if self.node is not None:
191
+ return await self.node.hasData(name)
192
+
193
+ return False
194
+
186
195
  async def setData(self, name, valu):
187
196
 
188
197
  if await self.getData(name) == valu:
@@ -287,6 +296,15 @@ class ProtoNode:
287
296
  if self.node is not None:
288
297
  return self.node.getTagProp(tag, name)
289
298
 
299
+ def hasTagProp(self, tag, name):
300
+ if (tag, name) in self.tagprops:
301
+ return True
302
+
303
+ if self.node is not None:
304
+ return self.node.hasTagProp(tag, name)
305
+
306
+ return False
307
+
290
308
  async def setTagProp(self, tag, name, valu):
291
309
 
292
310
  tagnode = await self.addTag(tag)
@@ -322,7 +340,7 @@ class ProtoNode:
322
340
  if self.node is not None:
323
341
  return self.node.get(name)
324
342
 
325
- async def _set(self, prop, valu, norminfo=None):
343
+ async def _set(self, prop, valu, norminfo=None, ignore_ro=False):
326
344
 
327
345
  if prop.locked:
328
346
  mesg = f'Prop {prop.full} is locked due to deprecation.'
@@ -360,7 +378,7 @@ class ProtoNode:
360
378
  if curv == valu:
361
379
  return False
362
380
 
363
- if prop.info.get('ro') and curv is not None:
381
+ if not ignore_ro and prop.info.get('ro') and curv is not None:
364
382
  mesg = f'Property is read only: {prop.full}.'
365
383
  await self.ctx.snap._raiseOnStrict(s_exc.ReadOnlyProp, mesg)
366
384
  return False
@@ -372,12 +390,12 @@ class ProtoNode:
372
390
 
373
391
  return valu, norminfo
374
392
 
375
- async def set(self, name, valu, norminfo=None):
393
+ async def set(self, name, valu, norminfo=None, ignore_ro=False):
376
394
  prop = self.form.props.get(name)
377
395
  if prop is None:
378
396
  return False
379
397
 
380
- retn = await self._set(prop, valu, norminfo=norminfo)
398
+ retn = await self._set(prop, valu, norminfo=norminfo, ignore_ro=ignore_ro)
381
399
  if retn is False:
382
400
  return False
383
401
 
synapse/lib/storm.py CHANGED
@@ -2351,7 +2351,7 @@ class Runtime(s_base.Base):
2351
2351
  if view is None:
2352
2352
  raise s_exc.NoSuchView(mesg=f'No such view iden={viewiden}', iden=viewiden)
2353
2353
 
2354
- self.user.confirm(('view', 'read'), gateiden=viewiden)
2354
+ self.confirm(('view', 'read'), gateiden=viewiden)
2355
2355
  snap = await view.snap(self.user)
2356
2356
 
2357
2357
  return snap
@@ -980,7 +980,7 @@ class HttpReq(s_stormtypes.StormType):
980
980
  try:
981
981
  return json.loads(self.rnfo.get('body'))
982
982
  except (UnicodeDecodeError, json.JSONDecodeError) as e:
983
- raise s_exc.StormRuntimeError(mesg='Failed to decode request body as JSON: {e}') from None
983
+ raise s_exc.StormRuntimeError(mesg=f'Failed to decode request body as JSON: {e}') from None
984
984
 
985
985
  @s_stormtypes.stormfunc(readonly=True)
986
986
  async def _methSendCode(self, code):
@@ -5,6 +5,19 @@ import synapse.lib.node as s_node
5
5
  import synapse.lib.cache as s_cache
6
6
  import synapse.lib.stormtypes as s_stormtypes
7
7
 
8
+ import synapse.models.infotech as s_infotech
9
+
10
+ RISK_HASVULN_VULNPROPS = (
11
+ 'hardware',
12
+ 'host',
13
+ 'item',
14
+ 'org',
15
+ 'person',
16
+ 'place',
17
+ 'software',
18
+ 'spec',
19
+ )
20
+
8
21
  stormcmds = [
9
22
  {
10
23
  'name': 'model.edge.set',
@@ -311,7 +324,7 @@ class LibModel(s_stormtypes.Lib):
311
324
  {'name': 'name', 'type': 'str', 'desc': 'The name of the tag prop to retrieve.', },
312
325
  ),
313
326
  'returns': {'type': ['model:tagprop', 'null'],
314
- 'desc': 'The ``model:tagprop`` instance if the tag prop if present or null.',
327
+ 'desc': 'The ``model:tagprop`` instance of the tag prop if present or null.',
315
328
  }}},
316
329
  )
317
330
 
@@ -694,8 +707,58 @@ class LibModelDeprecated(s_stormtypes.Lib):
694
707
  gatekeys = ((self.runt.user.iden, ('model', 'deprecated', 'lock'), None),)
695
708
  await self.runt.dyncall('cortex', todo, gatekeys=gatekeys)
696
709
 
710
+ class MigrationEditorMixin:
711
+ '''
712
+ Mixin helpers for migrating data within an editor context.
713
+ '''
714
+
715
+ async def copyData(self, src, proto, overwrite=False):
716
+
717
+ async for name in src.iterDataKeys():
718
+ if overwrite or not await proto.hasData(name):
719
+ self.runt.layerConfirm(('node', 'data', 'set', name))
720
+ valu = await src.getData(name)
721
+ await proto.setData(name, valu)
722
+
723
+ async def copyEdges(self, editor, src, proto):
724
+
725
+ verbs = set()
726
+
727
+ async for (verb, n2iden) in src.iterEdgesN1():
728
+
729
+ if verb not in verbs:
730
+ self.runt.layerConfirm(('node', 'edge', 'add', verb))
731
+ verbs.add(verb)
732
+
733
+ if await self.runt.snap.getNodeByBuid(s_common.uhex(n2iden)) is not None:
734
+ await proto.addEdge(verb, n2iden)
735
+
736
+ dstiden = proto.iden()
737
+
738
+ async for (verb, n1iden) in src.iterEdgesN2():
739
+
740
+ if verb not in verbs:
741
+ self.runt.layerConfirm(('node', 'edge', 'add', verb))
742
+ verbs.add(verb)
743
+
744
+ n1proto = await editor.getNodeByBuid(s_common.uhex(n1iden))
745
+ if n1proto is not None:
746
+ await n1proto.addEdge(verb, dstiden)
747
+
748
+ async def copyTags(self, src, proto, overwrite=False):
749
+
750
+ for name, valu in src.tags.items():
751
+ self.runt.layerConfirm(('node', 'tag', 'add', *name.split('.')))
752
+ await proto.addTag(name, valu=valu)
753
+
754
+ for tagname, tagprops in src.tagprops.items():
755
+ for propname, valu in tagprops.items():
756
+ if overwrite or not proto.hasTagProp(tagname, propname):
757
+ await proto.setTagProp(tagname, propname, valu) # use tag perms
758
+
759
+
697
760
  @s_stormtypes.registry.registerLib
698
- class LibModelMigration(s_stormtypes.Lib):
761
+ class LibModelMigration(s_stormtypes.Lib, MigrationEditorMixin):
699
762
  '''
700
763
  A Storm library containing migration tools.
701
764
  '''
@@ -745,14 +808,8 @@ class LibModelMigration(s_stormtypes.Lib):
745
808
  overwrite = await s_stormtypes.tobool(overwrite)
746
809
 
747
810
  async with self.runt.snap.getEditor() as editor:
748
-
749
811
  proto = editor.loadNode(dst)
750
-
751
- async for name in src.iterDataKeys():
752
- if overwrite or not await dst.hasData(name):
753
- self.runt.layerConfirm(('node', 'data', 'set', name))
754
- valu = await src.getData(name)
755
- await proto.setData(name, valu)
812
+ await self.copyData(src, proto, overwrite=overwrite)
756
813
 
757
814
  async def _methCopyEdges(self, src, dst):
758
815
 
@@ -764,30 +821,8 @@ class LibModelMigration(s_stormtypes.Lib):
764
821
  snap = self.runt.snap
765
822
 
766
823
  async with snap.getEditor() as editor:
767
-
768
824
  proto = editor.loadNode(dst)
769
- verbs = set()
770
-
771
- async for (verb, n2iden) in src.iterEdgesN1():
772
-
773
- if verb not in verbs:
774
- self.runt.layerConfirm(('node', 'edge', 'add', verb))
775
- verbs.add(verb)
776
-
777
- if await snap.getNodeByBuid(s_common.uhex(n2iden)) is not None:
778
- await proto.addEdge(verb, n2iden)
779
-
780
- dstiden = s_common.ehex(dst.buid)
781
-
782
- async for (verb, n1iden) in src.iterEdgesN2():
783
-
784
- if verb not in verbs:
785
- self.runt.layerConfirm(('node', 'edge', 'add', verb))
786
- verbs.add(verb)
787
-
788
- n1proto = await editor.getNodeByBuid(s_common.uhex(n1iden))
789
- if n1proto is not None:
790
- await n1proto.addEdge(verb, dstiden)
825
+ await self.copyEdges(editor, src, proto)
791
826
 
792
827
  async def _methCopyTags(self, src, dst, overwrite=False):
793
828
 
@@ -801,14 +836,278 @@ class LibModelMigration(s_stormtypes.Lib):
801
836
  snap = self.runt.snap
802
837
 
803
838
  async with snap.getEditor() as editor:
804
-
805
839
  proto = editor.loadNode(dst)
840
+ await self.copyTags(src, proto, overwrite=overwrite)
841
+
842
+ @s_stormtypes.registry.registerLib
843
+ class LibModelMigrations(s_stormtypes.Lib, MigrationEditorMixin):
844
+ '''
845
+ A Storm library for selectively migrating nodes in the current view.
846
+ '''
847
+ _storm_locals = (
848
+ {'name': 'itSecCpe_2_170_0',
849
+ 'desc': '''
850
+ Versions of Synapse prior to v2.169.0 did not correctly parse and
851
+ convert CPE strings from 2.2 -> 2.3 or 2.3 -> 2.2. This migration
852
+ attempts to re-normalize `it:sec:cpe` nodes that may be fixable.
853
+
854
+ NOTE: It is highly recommended to test the `it:sec:cpe` migrations
855
+ in a fork first and confirm the migration was successful without any
856
+ issues. Then run the migration in view deporder to migrate the
857
+ entire cortex. E.g.::
858
+
859
+ for $view in $lib.view.list(deporder=$lib.true) {
860
+ view.exec $view.iden {
861
+ for $n in $lib.layer.get().liftByProp(it:sec:cpe) {
862
+ $lib.model.migration.s.itSecCpe_2_170_0($n)
863
+ }
864
+ }
865
+ }
866
+
867
+ Upon completion of the migration, nodedata will contain a
868
+ `migration.s.itSecCpe_2_170_0` dict with information about the
869
+ migration status. This dict may contain the following:
870
+
871
+ - `status`: (required str) "success" or "failed"
872
+ - `reason`: (optional str) if "status" is "failed", this key will
873
+ explain why the migration failed.
874
+ - `valu`: (optional str) if this key is present, it will contain
875
+ an updated CPE2.3 string since the primary property cannot be
876
+ changed.
877
+ - `updated`: (optional list[str]) A list of properties that were
878
+ updated by the migration.
879
+
880
+ Failed or incorrect migrations may be helped by updating the :v2_2
881
+ property to be a valid CPE2.2 string and then re-running the
882
+ migration with `force=$lib.true`. If the primary property (CPE2.3)
883
+ is valid but incorrect, users may update the :v2_2 property and then
884
+ run the migration with `prefer_v22=$lib.true` to make the migration
885
+ use the `:v2_2` string instead of the primary property for the
886
+ migration process.
887
+ ''',
888
+ 'type': {'type': 'function', '_funcname': '_itSecCpe_2_170_0',
889
+ 'args': (
890
+ {'name': 'n', 'type': 'node', 'desc': 'The it:sec:cpe node to migrate.'},
891
+ {'name': 'prefer_v22', 'type': 'bool', 'default': False,
892
+ 'desc': '''
893
+ Try to renormalize using the :v2_2 prop instead of the
894
+ primary property. This can be especially useful when the
895
+ primary property is a valid but incorrect CPE string.
896
+ '''},
897
+ {'name': 'force', 'type': 'bool', 'default': False,
898
+ 'desc': 'Perform fixups even if the primary property and :v2_2 are valid.'},
899
+ ),
900
+ 'returns': {'type': 'boolean', 'desc': 'Boolean indicating if the migration was successful.'}}},
901
+ {'name': 'riskHasVulnToVulnerable', 'desc': '''
902
+ Create a risk:vulnerable node from the provided risk:hasvuln node.
903
+
904
+ Edits will be made to the risk:vulnerable node in the current write layer.
905
+
906
+ If multiple vulnerable properties are set on the risk:hasvuln node
907
+ multiple risk:vulnerable nodes will be created (each with a unique guid).
908
+ Otherwise, a single risk:vulnerable node will be created with the same guid
909
+ as the provided risk:hasvuln node. Extended properties will not be migrated.
910
+
911
+ Tags, tag properties, edges, and node data will be copied
912
+ to the risk:vulnerable node. However, existing tag properties and
913
+ node data will not be overwritten.
914
+ ''',
915
+ 'type': {'type': 'function', '_funcname': '_riskHasVulnToVulnerable',
916
+ 'args': (
917
+ {'name': 'n', 'type': 'node', 'desc': 'The risk:hasvuln node to migrate.'},
918
+ {'name': 'nodata', 'type': 'bool', 'default': False,
919
+ 'desc': 'Do not copy nodedata to the risk:vulnerable node.'},
920
+ ),
921
+ 'returns': {'type': 'list', 'desc': 'A list of idens for the risk:vulnerable nodes.'}}},
922
+ )
923
+ _storm_lib_path = ('model', 'migration', 's')
924
+
925
+ def getObjLocals(self):
926
+ return {
927
+ 'itSecCpe_2_170_0': self._itSecCpe_2_170_0,
928
+ 'riskHasVulnToVulnerable': self._riskHasVulnToVulnerable,
929
+ }
930
+
931
+ async def _itSecCpe_2_170_0(self, n, prefer_v22=False, force=False):
932
+
933
+ if not isinstance(n, s_node.Node):
934
+ raise s_exc.BadArg(mesg='$lib.model.migration.s.itSecCpe_2_170_0() argument must be a node.')
935
+
936
+ if n.form.name != 'it:sec:cpe':
937
+ raise s_exc.BadArg(f'itSecCpeFix only accepts it:sec:cpe nodes, not {n.form.name}')
938
+
939
+ prefer_v22 = await s_stormtypes.tobool(prefer_v22)
940
+ force = await s_stormtypes.tobool(force)
941
+
942
+ layr = self.runt.snap.wlyr
943
+ # We only need to check :v2_2 since that's the only property that's
944
+ # writable. Everthing else is readonly. And we can do it here once
945
+ # instead of in the loop below which will cause a perf hit.
946
+ self.runt.confirmPropSet(n.form.prop('v2_2'), layriden=layr.iden)
947
+
948
+ curv = n.repr()
949
+ reprvalu = f'it:sec:cpe={curv}'
950
+
951
+ nodedata = await n.getData('migration.s.itSecCpe_2_170_0', {})
952
+ if nodedata.get('status') == 'success' and not force:
953
+ if self.runt.debug:
954
+ mesg = f'DEBUG: itSecCpe_2_170_0({reprvalu}): Node already migrated.'
955
+ await self.runt.printf(mesg)
956
+ return True
957
+
958
+ modl = self.runt.model.type('it:sec:cpe')
959
+
960
+ valu23 = None
961
+ valu22 = None
962
+ invalid = ''
963
+
964
+ # Check the primary property for validity.
965
+ cpe23 = s_infotech.cpe23_regex.match(curv)
966
+ if cpe23 is not None and cpe23.group() == curv:
967
+ valu23 = curv
968
+
969
+ # Check the v2_2 property for validity.
970
+ v2_2 = n.props.get('v2_2')
971
+ if v2_2 is not None:
972
+ rgx = s_infotech.cpe22_regex.match(v2_2)
973
+ if rgx is not None and rgx.group() == v2_2:
974
+ valu22 = v2_2
975
+
976
+ async with self.runt.snap.getNodeEditor(n) as proto:
977
+
978
+ # If both values are populated, this node is valid
979
+ if valu23 is not None and valu22 is not None and not force:
980
+ if self.runt.debug:
981
+ mesg = f'DEBUG: itSecCpe_2_170_0({reprvalu}): Node is valid, no migration necessary.'
982
+ await self.runt.printf(mesg)
983
+
984
+ await proto.setData('migration.s.itSecCpe_2_170_0', {
985
+ 'status': 'success',
986
+ })
987
+
988
+ return True
989
+
990
+ if valu23 is None and valu22 is None:
991
+ reason = 'Unable to migrate due to invalid data. Primary property and :v2_2 are both invalid.'
992
+ # Invalid 2.3 string and no/invalid v2_2 prop. Nothing
993
+ # we can do here so log, mark, and go around.
994
+ mesg = f'itSecCpe_2_170_0({reprvalu}): {reason}'
995
+ await self.runt.warn(mesg)
996
+
997
+ await proto.setData('migration.s.itSecCpe_2_170_0', {
998
+ 'status': 'failed',
999
+ 'reason': reason,
1000
+ })
1001
+
1002
+ return False
1003
+
1004
+ if prefer_v22:
1005
+ valu = valu22 or valu23
1006
+ else:
1007
+ valu = valu23 or valu22
1008
+
1009
+ # Re-normalize the data from the 2.3 or 2.2 string, whichever was valid.
1010
+ norm, info = modl.norm(valu)
1011
+ subs = info.get('subs')
1012
+
1013
+ edits = []
1014
+ nodedata = {'status': 'success'}
1015
+
1016
+ if norm != curv:
1017
+ # The re-normed value is not the same as the current value.
1018
+ # Since we can't change the primary property, store the
1019
+ # updated value in nodedata.
1020
+ if self.runt.debug:
1021
+ mesg = f'DEBUG: itSecCpe_2_170_0({reprvalu}): Stored updated primary property value to nodedata: {curv} -> {norm}.'
1022
+ await self.runt.printf(mesg)
1023
+
1024
+ nodedata['valu'] = norm
1025
+
1026
+ # Iterate over the existing properties
1027
+ for propname, propcurv in n.props.items():
1028
+ subscurv = subs.get(propname)
1029
+ if subscurv is None:
1030
+ continue
1031
+
1032
+ if propname == 'v2_2' and isinstance(subscurv, list):
1033
+ subscurv = s_infotech.zipCpe22(subscurv)
1034
+
1035
+ # Values are the same, go around
1036
+ if propcurv == subscurv:
1037
+ continue
1038
+
1039
+ nodedata.setdefault('updated', [])
1040
+ nodedata['updated'].append(propname)
1041
+
1042
+ # Update the existing property with the re-normalized property value.
1043
+ await proto.set(propname, subscurv, ignore_ro=True)
1044
+
1045
+ await proto.setData('migration.s.itSecCpe_2_170_0', nodedata)
1046
+
1047
+ if self.runt.debug:
1048
+ if nodedata.get('updated'):
1049
+ mesg = f'DEBUG: itSecCpe_2_170_0({reprvalu}): Updated properties: {", ".join(nodedata["updated"])}.'
1050
+ await self.runt.printf(mesg)
1051
+ else:
1052
+ mesg = f'DEBUG: itSecCpe_2_170_0({reprvalu}): No property updates required.'
1053
+ await self.runt.printf(mesg)
1054
+
1055
+ return True
1056
+
1057
+ async def _riskHasVulnToVulnerable(self, n, nodata=False):
1058
+
1059
+ nodata = await s_stormtypes.tobool(nodata)
1060
+
1061
+ if not isinstance(n, s_node.Node):
1062
+ raise s_exc.BadArg(mesg='$lib.model.migration.s.riskHasVulnToVulnerable() argument must be a node.')
1063
+
1064
+ if n.form.name != 'risk:hasvuln':
1065
+ mesg = f'$lib.model.migration.s.riskHasVulnToVulnerable() only accepts risk:hasvuln nodes, not {n.form.name}'
1066
+ raise s_exc.BadArg(mesg=mesg)
1067
+
1068
+ retidens = []
1069
+
1070
+ if not (vuln := n.get('vuln')):
1071
+ return retidens
1072
+
1073
+ props = {
1074
+ 'vuln': vuln,
1075
+ }
1076
+
1077
+ links = {prop: valu for prop in RISK_HASVULN_VULNPROPS if (valu := n.get(prop)) is not None}
1078
+
1079
+ match len(links):
1080
+ case 0:
1081
+ return retidens
1082
+ case 1:
1083
+ guid = n.ndef[1]
1084
+ case _:
1085
+ guid = None
1086
+
1087
+ riskvuln = self.runt.model.form('risk:vulnerable')
1088
+
1089
+ self.runt.layerConfirm(riskvuln.addperm)
1090
+ self.runt.confirmPropSet(riskvuln.props['vuln'])
1091
+ self.runt.confirmPropSet(riskvuln.props['node'])
1092
+
1093
+ if (seen := n.get('.seen')):
1094
+ self.runt.confirmPropSet(riskvuln.props['.seen'])
1095
+ props['.seen'] = seen
1096
+
1097
+ async with self.runt.snap.getEditor() as editor:
1098
+
1099
+ for prop, valu in links.items():
1100
+
1101
+ pguid = guid if guid is not None else s_common.guid((guid, prop))
1102
+ pprops = props | {'node': (n.form.props[prop].type.name, valu)}
1103
+
1104
+ proto = await editor.addNode('risk:vulnerable', pguid, props=pprops)
1105
+ retidens.append(proto.iden())
1106
+
1107
+ await self.copyTags(n, proto, overwrite=False)
1108
+ await self.copyEdges(editor, n, proto)
806
1109
 
807
- for name, valu in src.tags.items():
808
- self.runt.layerConfirm(('node', 'tag', 'add', *name.split('.')))
809
- await proto.addTag(name, valu=valu)
1110
+ if not nodata:
1111
+ await self.copyData(n, proto, overwrite=False)
810
1112
 
811
- for tagname, tagprops in src.tagprops.items():
812
- for propname, valu in tagprops.items():
813
- if overwrite or not dst.hasTagProp(tagname, propname):
814
- await proto.setTagProp(tagname, propname, valu) # use tag perms
1113
+ return retidens
synapse/lib/stormtypes.py CHANGED
@@ -6721,6 +6721,20 @@ class Layer(Prim):
6721
6721
  ''',
6722
6722
  'type': {'type': 'function', '_funcname': 'getStorNodes',
6723
6723
  'returns': {'name': 'Yields', 'type': 'list', 'desc': 'Tuple of buid, sode values.', }}},
6724
+ {'name': 'getStorNodesByForm', 'desc': '''
6725
+ Get buid, sode tuples representing the data stored in the layer for a given form.
6726
+
6727
+ Notes:
6728
+ The storage nodes represent **only** the data stored in the layer
6729
+ and may not represent whole nodes. If the only data stored in the layer for
6730
+ a given buid is an N2 edge reference, a storage node will not be returned.
6731
+ ''',
6732
+ 'type': {'type': 'function', '_funcname': 'getStorNodesByForm',
6733
+ 'args': (
6734
+ {'name': 'form', 'type': 'str',
6735
+ 'desc': 'The name of the form to get storage nodes for.'},
6736
+ ),
6737
+ 'returns': {'name': 'Yields', 'type': 'list', 'desc': 'Tuple of buid, sode values.', }}},
6724
6738
  {'name': 'getMirrorStatus', 'desc': '''
6725
6739
  Return a dictionary of the mirror synchronization status for the layer.
6726
6740
  ''',
@@ -6902,6 +6916,7 @@ class Layer(Prim):
6902
6916
  'getFormCounts': self._methGetFormcount,
6903
6917
  'getStorNode': self.getStorNode,
6904
6918
  'getStorNodes': self.getStorNodes,
6919
+ 'getStorNodesByForm': self.getStorNodesByForm,
6905
6920
  'getEdgesByN1': self.getEdgesByN1,
6906
6921
  'getEdgesByN2': self.getEdgesByN2,
6907
6922
  'getMirrorStatus': self.getMirrorStatus,
@@ -7198,6 +7213,19 @@ class Layer(Prim):
7198
7213
  async for item in layr.getStorNodes():
7199
7214
  yield item
7200
7215
 
7216
+ @stormfunc(readonly=True)
7217
+ async def getStorNodesByForm(self, form):
7218
+ form = await tostr(form)
7219
+ if self.runt.snap.core.model.form(form) is None:
7220
+ raise s_exc.NoSuchForm.init(form)
7221
+
7222
+ layriden = self.valu.get('iden')
7223
+ await self.runt.reqUserCanReadLayer(layriden)
7224
+ layr = self.runt.snap.core.getLayer(layriden)
7225
+
7226
+ async for item in layr.getStorNodesByForm(form):
7227
+ yield item
7228
+
7201
7229
  @stormfunc(readonly=True)
7202
7230
  async def getEdges(self):
7203
7231
  layriden = self.valu.get('iden')
@@ -7452,6 +7480,12 @@ class View(Prim):
7452
7480
  {'name': 'name', 'type': 'str', 'desc': 'The name of the new view.', 'default': None, },
7453
7481
  ),
7454
7482
  'returns': {'type': 'view', 'desc': 'The ``view`` object for the new View.', }}},
7483
+ {'name': 'insertParentFork', 'desc': 'Insert a new View between a forked View and its parent.',
7484
+ 'type': {'type': 'function', '_funcname': '_methViewInsertParentFork',
7485
+ 'args': (
7486
+ {'name': 'name', 'type': 'str', 'desc': 'The name of the new View.', 'default': None},
7487
+ ),
7488
+ 'returns': {'type': 'view', 'desc': 'The ``view`` object for the new View.', }}},
7455
7489
  {'name': 'pack', 'desc': 'Get the View definition.',
7456
7490
  'type': {'type': 'function', '_funcname': '_methViewPack',
7457
7491
  'returns': {'type': 'dict', 'desc': 'Dictionary containing the View definition.', }}},
@@ -7653,7 +7687,6 @@ class View(Prim):
7653
7687
  return {
7654
7688
  'set': self._methViewSet,
7655
7689
  'get': self._methViewGet,
7656
- 'fork': self._methViewFork,
7657
7690
  'pack': self._methViewPack,
7658
7691
  'repr': self._methViewRepr,
7659
7692
  'merge': self._methViewMerge,
@@ -7668,6 +7701,9 @@ class View(Prim):
7668
7701
  'getTagPropCount': self._methGetTagPropCount,
7669
7702
  'getPropArrayCount': self._methGetPropArrayCount,
7670
7703
 
7704
+ 'fork': self._methViewFork,
7705
+ 'insertParentFork': self._methViewInsertParentFork,
7706
+
7671
7707
  'getMerges': self.getMerges,
7672
7708
  'delMergeVote': self.delMergeVote,
7673
7709
  'setMergeVote': self.setMergeVote,
@@ -7919,6 +7955,27 @@ class View(Prim):
7919
7955
 
7920
7956
  return View(self.runt, newv, path=self.path)
7921
7957
 
7958
+ async def _methViewInsertParentFork(self, name=None):
7959
+ useriden = self.runt.user.iden
7960
+ viewiden = self.valu.get('iden')
7961
+
7962
+ name = await tostr(name, noneok=True)
7963
+
7964
+ self.runt.reqAdmin(gateiden=viewiden)
7965
+
7966
+ view = self.runt.snap.core.reqView(viewiden)
7967
+ if not view.isafork():
7968
+ mesg = f'View ({viewiden}) is not a fork, cannot insert a new fork between it and parent.'
7969
+ raise s_exc.BadState(mesg=mesg)
7970
+
7971
+ self.runt.confirm(('view', 'add'))
7972
+ self.runt.confirm(('view', 'read'), gateiden=view.parent.iden)
7973
+ self.runt.confirm(('view', 'fork'), gateiden=view.parent.iden)
7974
+
7975
+ newv = await view.insertParentFork(useriden, name=name)
7976
+
7977
+ return View(self.runt, newv, path=self.path)
7978
+
7922
7979
  async def _methViewMerge(self, force=False):
7923
7980
  '''
7924
7981
  Merge a forked view back into its parent.
synapse/lib/types.py CHANGED
@@ -1408,6 +1408,31 @@ class Ndef(Type):
1408
1408
  self.setNormFunc(list, self._normPyTuple)
1409
1409
  self.setNormFunc(tuple, self._normPyTuple)
1410
1410
 
1411
+ self.formfilter = None
1412
+
1413
+ self.forms = self.opts.get('forms')
1414
+ self.ifaces = self.opts.get('interfaces')
1415
+
1416
+ if self.forms or self.ifaces:
1417
+ if self.forms is not None:
1418
+ forms = set(self.forms)
1419
+
1420
+ if self.ifaces is not None:
1421
+ ifaces = set(self.ifaces)
1422
+
1423
+ def filtfunc(form):
1424
+ if self.forms is not None and form.name in forms:
1425
+ return False
1426
+
1427
+ if self.ifaces is not None:
1428
+ for iface in form.ifaces.keys():
1429
+ if iface in ifaces:
1430
+ return False
1431
+
1432
+ return True
1433
+
1434
+ self.formfilter = filtfunc
1435
+
1411
1436
  def _normStormNode(self, valu):
1412
1437
  return self._normPyTuple(valu.ndef)
1413
1438
 
@@ -1421,6 +1446,16 @@ class Ndef(Type):
1421
1446
  if form is None:
1422
1447
  raise s_exc.NoSuchForm.init(formname)
1423
1448
 
1449
+ if self.formfilter is not None and self.formfilter(form):
1450
+ mesg = f'Ndef of form {formname} is not allowed as a value for {self.name} with form filter'
1451
+ if self.forms is not None:
1452
+ mesg += f' forms={self.forms}'
1453
+
1454
+ if self.ifaces is not None:
1455
+ mesg += f' interfaces={self.ifaces}'
1456
+
1457
+ raise s_exc.BadTypeValu(valu=formname, name=self.name, mesg=mesg, forms=self.forms, interfaces=self.ifaces)
1458
+
1424
1459
  formnorm, forminfo = form.type.norm(formvalu)
1425
1460
  norm = (form.name, formnorm)
1426
1461
 
@@ -1727,7 +1762,7 @@ class Str(Type):
1727
1762
 
1728
1763
  def _normPyFloat(self, valu):
1729
1764
  deci = s_common.hugectx.create_decimal(str(valu))
1730
- return format(deci, 'f'), {}
1765
+ return self._normPyStr(format(deci, 'f'))
1731
1766
 
1732
1767
  def _normPyStr(self, valu):
1733
1768
 
synapse/lib/version.py CHANGED
@@ -223,6 +223,6 @@ def reqVersion(valu, reqver,
223
223
  ##############################################################################
224
224
  # The following are touched during the release process by bumpversion.
225
225
  # Do not modify these directly.
226
- version = (2, 169, 0)
226
+ version = (2, 171, 0)
227
227
  verstring = '.'.join([str(x) for x in version])
228
- commit = '41421efcf40bb7655af3f90cb7d6d335247fc726'
228
+ commit = 'c89839928860a36f70377a8cefd09aaa4b12595a'