From 0bb39e10bfffedf59d989b36fdf6f6248bc17ad0 Mon Sep 17 00:00:00 2001 From: mikemoritz <57907149+mikemoritz@users.noreply.github.com> Date: Mon, 13 Jan 2025 07:48:52 -0800 Subject: [PATCH 01/19] Add remaining alts to datamodel (SYN-8342) (#4064) --- changes/4a4fe1c2eb09b8a712af72dc6ce41c38.yaml | 8 +++++++ synapse/models/geopol.py | 1 + synapse/models/geospace.py | 1 + synapse/models/infotech.py | 2 ++ synapse/models/orgs.py | 5 +++++ synapse/models/person.py | 8 +++++++ synapse/models/risk.py | 3 +++ synapse/tests/test_model_geopol.py | 4 ++++ synapse/tests/test_model_geospace.py | 6 +++++ synapse/tests/test_model_infotech.py | 8 +++++++ synapse/tests/test_model_orgs.py | 19 ++++++++++++++-- synapse/tests/test_model_person.py | 22 ++++++++++++++++++- synapse/tests/test_model_risk.py | 11 ++++++++++ 13 files changed, 95 insertions(+), 3 deletions(-) create mode 100644 changes/4a4fe1c2eb09b8a712af72dc6ce41c38.yaml diff --git a/changes/4a4fe1c2eb09b8a712af72dc6ce41c38.yaml b/changes/4a4fe1c2eb09b8a712af72dc6ce41c38.yaml new file mode 100644 index 0000000000..2d77bd4367 --- /dev/null +++ b/changes/4a4fe1c2eb09b8a712af72dc6ce41c38.yaml @@ -0,0 +1,8 @@ +--- +desc: 'Added ``alts`` definitions to the following forms: ``geo:place``, ``it:prod:soft``, + ``it:prod:softver``, ``ou:campaign``, ``ou:conference``, ``ou:goal``, ``ou:industry``, + ``pol:country``, ``ps:contact``, ``ps:person``, ``risk:threat``, ``risk:tool:software``, + and ``risk:vuln``.' +prs: [] +type: model +... diff --git a/synapse/models/geopol.py b/synapse/models/geopol.py index 91622dcf66..4bc4df28ee 100644 --- a/synapse/models/geopol.py +++ b/synapse/models/geopol.py @@ -69,6 +69,7 @@ def getModelDefs(self): ('tld', ('inet:fqdn', {}), {}), ('name', ('geo:name', {}), { + 'alts': ('names',), 'doc': 'The name of the country.'}), ('names', ('array', {'type': 'geo:name', 'uniq': True, 'sorted': True}), { diff --git a/synapse/models/geospace.py b/synapse/models/geospace.py index b40f8aebca..92103bb045 100644 --- a/synapse/models/geospace.py +++ b/synapse/models/geospace.py @@ -495,6 +495,7 @@ def getModelDefs(self): ('geo:place', {}, ( ('name', ('geo:name', {}), { + 'alts': ('names',), 'doc': 'The name of the place.'}), ('type', ('geo:place:taxonomy', {}), { diff --git a/synapse/models/infotech.py b/synapse/models/infotech.py index 3772bbfdbf..37661f0510 100644 --- a/synapse/models/infotech.py +++ b/synapse/models/infotech.py @@ -2101,6 +2101,7 @@ def getModelDefs(self): 'doc': 'An ID for the software.'}), ('name', ('it:prod:softname', {}), { + 'alts': ('names',), 'doc': 'Name of the software.', }), ('type', ('it:prod:soft:taxonomy', {}), { @@ -2222,6 +2223,7 @@ def getModelDefs(self): 'doc': 'Deprecated. Please use it:prod:softver:name.', }), ('name', ('it:prod:softname', {}), { + 'alts': ('names',), 'doc': 'Name of the software version.', }), ('names', ('array', {'type': 'it:prod:softname', 'uniq': True, 'sorted': True}), { diff --git a/synapse/models/orgs.py b/synapse/models/orgs.py index a5b9a4bd02..a5f7e0d5f7 100644 --- a/synapse/models/orgs.py +++ b/synapse/models/orgs.py @@ -526,6 +526,7 @@ def getModelDefs(self): ('ou:goal', {}, ( ('name', ('ou:goalname', {}), { + 'alts': ('names',), 'doc': 'A terse name for the goal.'}), ('names', ('array', {'type': 'ou:goalname', 'sorted': True, 'uniq': True}), { @@ -570,6 +571,7 @@ def getModelDefs(self): 'doc': 'The FQDN of the org responsible for the campaign. Used for entity resolution.'}), ('goal', ('ou:goal', {}), { + 'alts': ('goals',), 'doc': 'The assessed primary goal of the campaign.'}), ('slogan', ('lang:phrase', {}), { @@ -585,6 +587,7 @@ def getModelDefs(self): 'doc': 'Records the success/failure status of the campaign if known.'}), ('name', ('ou:campname', {}), { + 'alts': ('names',), 'ex': 'operation overlord', 'doc': 'A terse name of the campaign.'}), @@ -924,6 +927,7 @@ def getModelDefs(self): ('ou:industry', {}, ( ('name', ('ou:industryname', {}), { + 'alts': ('names',), 'doc': 'The name of the industry.'}), ('type', ('ou:industry:type:taxonomy', {}), { @@ -1176,6 +1180,7 @@ def getModelDefs(self): 'doc': 'An array of contacts which sponsored the conference.', }), ('name', ('entity:name', {}), { + 'alts': ('names',), 'doc': 'The full name of the conference.', 'ex': 'defcon 2017'}), diff --git a/synapse/models/person.py b/synapse/models/person.py index 0f45796ec2..1693aa86c1 100644 --- a/synapse/models/person.py +++ b/synapse/models/person.py @@ -254,6 +254,7 @@ def getModelDefs(self): 'doc': 'The most recent known vitals for the person.', }), ('name', ('ps:name', {}), { + 'alts': ('names',), 'doc': 'The localized name for the person.', }), ('name:sur', ('ps:tokn', {}), { @@ -350,6 +351,7 @@ def getModelDefs(self): 'doc': 'The most recent known vitals for the contact.', }), ('name', ('ps:name', {}), { + 'alts': ('names',), 'doc': 'The person name listed for the contact.'}), ('bio', ('str', {}), { @@ -359,6 +361,7 @@ def getModelDefs(self): 'doc': 'A description of this contact.'}), ('title', ('ou:jobtitle', {}), { + 'alts': ('titles',), 'doc': 'The job/org title listed for this contact.'}), ('titles', ('array', {'type': 'ou:jobtitle', 'sorted': True, 'uniq': True}), { @@ -368,12 +371,14 @@ def getModelDefs(self): 'doc': 'The photo listed for this contact.', }), ('orgname', ('ou:name', {}), { + 'alts': ('orgnames',), 'doc': 'The listed org/company name for this contact.', }), ('orgfqdn', ('inet:fqdn', {}), { 'doc': 'The listed org/company FQDN for this contact.', }), ('user', ('inet:user', {}), { + 'alts': ('users',), 'doc': 'The username or handle for this contact.'}), ('service:accounts', ('array', {'type': 'inet:service:account', 'sorted': True, 'uniq': True}), { @@ -415,6 +420,7 @@ def getModelDefs(self): 'doc': 'The home or main site for this contact.', }), ('email', ('inet:email', {}), { + 'alts': ('emails',), 'doc': 'The main email address for this contact.', }), ('email:work', ('inet:email', {}), { @@ -443,6 +449,7 @@ def getModelDefs(self): 'doc': 'The work phone number for this contact.', }), ('id:number', ('ou:id:number', {}), { + 'alts': ('id:numbers',), 'doc': 'An ID number issued by an org and associated with this contact.', }), ('adid', ('it:adid', {}), { @@ -482,6 +489,7 @@ def getModelDefs(self): }), ('lang', ('lang:language', {}), { + 'alts': ('langs',), 'doc': 'The language specified for the contact.'}), ('langs', ('array', {'type': 'lang:language'}), { diff --git a/synapse/models/risk.py b/synapse/models/risk.py index 347b44420a..12614cdf07 100644 --- a/synapse/models/risk.py +++ b/synapse/models/risk.py @@ -310,6 +310,7 @@ def getModelDefs(self): 'doc': "The reporting organization's assessed location of the threat cluster."}), ('org:name', ('ou:name', {}), { + 'alts': ('org:names',), 'ex': 'apt1', 'doc': "The reporting organization's name for the threat cluster."}), @@ -383,6 +384,7 @@ def getModelDefs(self): 'doc': 'The authoritative software family for the tool.'}), ('soft:name', ('it:prod:softname', {}), { + 'alts': ('soft:names',), 'doc': 'The reporting organization\'s name for the tool.'}), ('soft:names', ('array', {'type': 'it:prod:softname', 'uniq': True, 'sorted': True}), { @@ -441,6 +443,7 @@ def getModelDefs(self): ('risk:vuln', {}, ( ('name', ('risk:vulnname', {}), { + 'alts': ('names',), 'doc': 'A user specified name for the vulnerability.'}), ('names', ('array', {'type': 'risk:vulnname', 'sorted': True, 'uniq': True}), { diff --git a/synapse/tests/test_model_geopol.py b/synapse/tests/test_model_geopol.py index 075d1caeb0..14a63c5413 100644 --- a/synapse/tests/test_model_geopol.py +++ b/synapse/tests/test_model_geopol.py @@ -18,6 +18,7 @@ async def test_geopol_country(self): ] ''') self.len(1, nodes) + node = nodes[0] self.eq('visiland', nodes[0].get('name')) self.eq(('visitopia',), nodes[0].get('names')) self.eq(1640995200000, nodes[0].get('founded')) @@ -29,6 +30,9 @@ async def test_geopol_country(self): self.len(2, await core.nodes('pol:country -> geo:name')) self.len(3, await core.nodes('pol:country -> econ:currency')) + self.len(1, nodes := await core.nodes('[ pol:country=({"name": "visitopia"}) ]')) + self.eq(node.ndef, nodes[0].ndef) + nodes = await core.nodes(''' [ pol:vitals=* :country={pol:country:name=visiland} diff --git a/synapse/tests/test_model_geospace.py b/synapse/tests/test_model_geospace.py index 63078853eb..571cae71a0 100644 --- a/synapse/tests/test_model_geospace.py +++ b/synapse/tests/test_model_geospace.py @@ -281,6 +281,12 @@ async def test_types_forms(self): nodes = await core.nodes('[ geo:place=(hehe, haha) :names=("Foo Bar ", baz) ] -> geo:name') self.eq(('baz', 'foo bar'), [n.ndef[1] for n in nodes]) + nodes = await core.nodes('geo:place=(hehe, haha)') + node = nodes[0] + + self.len(1, nodes := await core.nodes('[ geo:place=({"name": "baz"}) ]')) + self.eq(node.ndef, nodes[0].ndef) + async def test_eq(self): async with self.getTestCore() as core: diff --git a/synapse/tests/test_model_infotech.py b/synapse/tests/test_model_infotech.py index 38dc5703ac..1fa4b741e0 100644 --- a/synapse/tests/test_model_infotech.py +++ b/synapse/tests/test_model_infotech.py @@ -787,6 +787,10 @@ async def test_it_forms_prodsoft(self): self.eq(node.get('url'), url0) self.len(1, await core.nodes('it:prod:soft:name="balloon maker" -> it:prod:soft:taxonomy')) self.len(2, await core.nodes('it:prod:softname="balloon maker" -> it:prod:soft -> it:prod:softname')) + + self.len(1, nodes := await core.nodes('[ it:prod:soft=({"name": "clowns inc"}) ]')) + self.eq(node.ndef, nodes[0].ndef) + # it:prod:softver - this does test a bunch of property related callbacks ver0 = s_common.guid() url1 = 'https://vertex.link/products/balloonmaker/release_101-beta.exe' @@ -820,6 +824,10 @@ async def test_it_forms_prodsoft(self): self.eq(node.get('url'), url1) self.eq(node.get('name'), 'balloonmaker') self.eq(node.get('desc'), 'makes balloons') + + self.len(1, nodes := await core.nodes('[ it:prod:softver=({"name": "clowns inc"}) ]')) + self.eq(node.ndef, nodes[0].ndef) + # callback node creation checks self.len(1, await core.nodes('it:dev:str=V1.0.1-beta+exp.sha.5114f85')) self.len(1, await core.nodes('it:dev:str=amd64')) diff --git a/synapse/tests/test_model_orgs.py b/synapse/tests/test_model_orgs.py index 9e453a3ef9..6de8497aa5 100644 --- a/synapse/tests/test_model_orgs.py +++ b/synapse/tests/test_model_orgs.py @@ -60,6 +60,9 @@ async def test_ou_simple(self): self.eq(node.get('desc'), 'MyDesc') self.eq(node.get('prev'), goal) + self.len(1, nodes := await core.nodes('[ ou:goal=({"name": "foo goal"}) ]')) + self.eq(node.ndef, nodes[0].ndef) + nodes = await core.nodes('[(ou:hasgoal=$valu :stated=$lib.true :window="2019,2020")]', opts={'vars': {'valu': (org0, goal)}}) self.len(1, nodes) @@ -69,12 +72,13 @@ async def test_ou_simple(self): self.eq(node.get('stated'), True) self.eq(node.get('window'), (1546300800000, 1577836800000)) + altgoal = s_common.guid() timeline = s_common.guid() props = { 'org': org0, 'goal': goal, - 'goals': (goal,), + 'goals': (goal, altgoal), 'actors': (acto,), 'camptype': 'get.pizza', 'name': 'MyName', @@ -103,7 +107,7 @@ async def test_ou_simple(self): self.eq(node.get('tag'), 'cno.camp.31337') self.eq(node.get('org'), org0) self.eq(node.get('goal'), goal) - self.eq(node.get('goals'), (goal,)) + self.eq(node.get('goals'), sorted((goal, altgoal))) self.eq(node.get('actors'), (acto,)) self.eq(node.get('name'), 'myname') self.eq(node.get('names'), ('bar', 'foo')) @@ -120,6 +124,10 @@ async def test_ou_simple(self): self.eq(node.get('mitre:attack:campaign'), 'C0011') self.eq(node.get('slogan'), 'for the people') + opts = {'vars': {'altgoal': altgoal}} + self.len(1, nodes := await core.nodes('[ ou:campaign=({"name": "foo", "goal": $altgoal}) ]', opts=opts)) + self.eq(node.ndef, nodes[0].ndef) + self.len(1, await core.nodes(f'ou:campaign={camp} :slogan -> lang:phrase')) nodes = await core.nodes(f'ou:campaign={camp} -> it:mitre:attack:campaign') self.len(1, nodes) @@ -405,6 +413,9 @@ async def test_ou_simple(self): self.eq(node.get('place'), place0) self.eq(node.get('url'), 'http://arrowcon.org/2018') + self.len(1, nodes := await core.nodes('[ ou:conference=({"name": "arrcon18"}) ]')) + self.eq(node.ndef, nodes[0].ndef) + props = { 'arrived': '201803010800', 'departed': '201803021500', @@ -870,6 +881,7 @@ async def test_ou_industry(self): ] ''' nodes = await core.nodes(q) self.len(1, nodes) + node = nodes[0] self.nn(nodes[0].get('reporter')) self.eq('foo bar', nodes[0].get('name')) self.eq('vertex', nodes[0].get('reporter:name')) @@ -884,6 +896,9 @@ async def test_ou_industry(self): self.len(3, nodes) self.len(3, await core.nodes('ou:industryname=baz -> ou:industry -> ou:industryname')) + self.len(1, nodes := await core.nodes('[ ou:industry=({"name": "faz"}) ]')) + self.eq(node.ndef, nodes[0].ndef) + async def test_ou_opening(self): async with self.getTestCore() as core: diff --git a/synapse/tests/test_model_person.py b/synapse/tests/test_model_person.py index 8be1a427eb..70e3f50c3f 100644 --- a/synapse/tests/test_model_person.py +++ b/synapse/tests/test_model_person.py @@ -60,6 +60,9 @@ async def test_ps_simple(self): self.eq(node.get('names'), ['billy bob']) self.eq(node.get('photo'), file0) + self.len(1, nodes := await core.nodes('[ ps:person=({"name": "billy bob"}) ]')) + self.eq(node.ndef, nodes[0].ndef) + props = { 'dob': '2000', 'img': file0, @@ -147,6 +150,7 @@ async def test_ps_simple(self): 'id:numbers': (('*', 'asdf'), ('*', 'qwer')), 'users': ('visi', 'invisigoth'), 'crypto:address': 'btc/1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2', + 'langs': (lang00 := s_common.guid(),), } opts = {'vars': {'valu': con0, 'p': props}} q = '''[(ps:contact=$valu @@ -166,7 +170,7 @@ async def test_ps_simple(self): :birth:place:name=$p."birth:place:name" :death:place=$p."death:place" :death:place:loc=$p."death:place:loc" :death:place:name=$p."death:place:name" - :service:accounts=(*, *) + :service:accounts=(*, *) :langs=$p.langs )]''' nodes = await core.nodes(q, opts=opts) self.len(1, nodes) @@ -213,6 +217,22 @@ async def test_ps_simple(self): self.len(1, await core.nodes('ps:contact :death:place -> geo:place')) self.len(2, await core.nodes('ps:contact :service:accounts -> inet:service:account')) + opts = { + 'vars': { + 'ctor': { + 'email': 'v@vtx.lk', + 'id:number': node.get('id:numbers')[0], + 'lang': lang00, + 'name': 'vi', + 'orgname': 'vertex', + 'title': 'haha', + 'user': 'invisigoth', + }, + }, + } + self.len(1, nodes := await core.nodes('[ ps:contact=$ctor ]', opts=opts)) + self.eq(node.ndef, nodes[0].ndef) + nodes = await core.nodes('''[ ps:achievement=* :award=* diff --git a/synapse/tests/test_model_risk.py b/synapse/tests/test_model_risk.py index 3a1d0baf52..29225e0cb6 100644 --- a/synapse/tests/test_model_risk.py +++ b/synapse/tests/test_model_risk.py @@ -253,6 +253,9 @@ async def addNode(text): self.len(1, await core.nodes('risk:attack :target -> ps:contact')) self.len(1, await core.nodes('risk:attack :attacker -> ps:contact')) + self.len(1, nodes := await core.nodes('[ risk:vuln=({"name": "hehe"}) ]')) + self.eq(node.ndef, nodes[0].ndef) + node = await addNode(f'''[ risk:hasvuln={hasv} :vuln={vuln} @@ -399,6 +402,7 @@ async def addNode(text): ] ''') self.len(1, nodes) + node = nodes[0] self.eq('vtx-apt1', nodes[0].get('name')) self.eq('VTX-APT1', nodes[0].get('desc')) self.eq(40, nodes[0].get('activity')) @@ -424,6 +428,9 @@ async def addNode(text): self.len(1, await core.nodes('risk:threat:merged:isnow -> risk:threat')) self.len(1, await core.nodes('risk:threat -> it:mitre:attack:group')) + self.len(1, nodes := await core.nodes('[ risk:threat=({"org:name": "comment crew"}) ]')) + self.eq(node.ndef, nodes[0].ndef) + nodes = await core.nodes('''[ risk:leak=* :name="WikiLeaks ACME Leak" :desc="WikiLeaks leaked ACME stuff." @@ -618,6 +625,7 @@ async def test_model_risk_tool_software(self): ] ''') self.len(1, nodes) + node = nodes[0] self.nn(nodes[0].get('soft')) self.nn(nodes[0].get('reporter')) @@ -640,6 +648,9 @@ async def test_model_risk_tool_software(self): self.len(1, await core.nodes('risk:tool:software -> syn:tag')) self.len(1, await core.nodes('risk:tool:software -> it:mitre:attack:software')) + self.len(1, nodes := await core.nodes('[ risk:tool:software=({"soft:name": "beacon"}) ]')) + self.eq(node.ndef, nodes[0].ndef) + nodes = await core.nodes(''' [ risk:vuln:soft:range=* :vuln={[ risk:vuln=* :name=woot ]} From e43aba560c4d22a6d50c2d7845389a7c7c8ee585 Mon Sep 17 00:00:00 2001 From: invisig0th Date: Mon, 13 Jan 2025 13:39:11 -0500 Subject: [PATCH 02/19] Add guided tour to README (#4065) --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index a430a52aa2..b5358e71a2 100644 --- a/README.rst +++ b/README.rst @@ -9,7 +9,7 @@ Learn More About Synapse ------------------------ * Visit our website_ to learn more about Synapse. -* Watch `Synapse 101`_! +* Watch the `Synapse Guided Tour`_! Installation & Documentation @@ -40,7 +40,7 @@ Connect With Us .. _website: https://v.vtx.lk/vertex -.. _Synapse 101: https://v.vtx.lk/new-syn101 +.. _Synapse Guided Tour: https://v.vtx.lk/synapse-tour .. _pypi: https://v.vtx.lk/synapse-pypi From b284ce43605ed254a46a6552764f3d23beb9a1f3 Mon Sep 17 00:00:00 2001 From: OCBender <181250370+OCBender@users.noreply.github.com> Date: Wed, 15 Jan 2025 10:27:35 -0500 Subject: [PATCH 03/19] resolve scheduler loop error (SYN-7132) (#4058) Co-authored-by: bender Co-authored-by: invisig0th Co-authored-by: vEpiphyte --- changes/ff623ded36292878c995dfcf1874daf4.yaml | 5 + synapse/cortex.py | 1 - synapse/lib/agenda.py | 21 ++- synapse/tests/test_lib_agenda.py | 126 +++++++++++++++++- 4 files changed, 147 insertions(+), 6 deletions(-) create mode 100644 changes/ff623ded36292878c995dfcf1874daf4.yaml diff --git a/changes/ff623ded36292878c995dfcf1874daf4.yaml b/changes/ff623ded36292878c995dfcf1874daf4.yaml new file mode 100644 index 0000000000..8f3ecc2210 --- /dev/null +++ b/changes/ff623ded36292878c995dfcf1874daf4.yaml @@ -0,0 +1,5 @@ +--- +desc: Fixed a Cortex cron scheduler loop error during a mirror promotion. +prs: [] +type: bug +... diff --git a/synapse/cortex.py b/synapse/cortex.py index 603da3dee6..ac7999bd83 100644 --- a/synapse/cortex.py +++ b/synapse/cortex.py @@ -1556,7 +1556,6 @@ async def initServiceRuntime(self): async def initServiceActive(self): await self.stormdmons.start() - await self.agenda.clearRunningStatus() async def _runMigrations(): # Run migrations when this cortex becomes active. This is to prevent diff --git a/synapse/lib/agenda.py b/synapse/lib/agenda.py index f2a10f1e7c..443b2cb5f3 100644 --- a/synapse/lib/agenda.py +++ b/synapse/lib/agenda.py @@ -679,6 +679,11 @@ async def delete(self, iden): mesg = f'No cron job with iden: {iden}' raise s_exc.NoSuchIden(iden=iden, mesg=mesg) + self._delete_appt_from_heap(appt) + del self.appts[iden] + self.apptdefs.delete(iden) + + def _delete_appt_from_heap(self, appt): try: heappos = self.apptheap.index(appt) except ValueError: @@ -692,9 +697,6 @@ async def delete(self, iden): self.apptheap[heappos] = self.apptheap.pop() heapq.heapify(self.apptheap) - del self.appts[iden] - self.apptdefs.delete(iden) - def _getNowTick(self): return time.time() + self.tickoff @@ -707,12 +709,23 @@ async def clearRunningStatus(self): for appt in list(self.appts.values()): if appt.isrunning: logger.debug(f'Clearing the isrunning flag for {appt.iden}') - await self.core.addCronEdits(appt.iden, {'isrunning': False}) + + edits = { + 'isrunning': False, + 'lastfinishtime': self._getNowTick(), + 'lasterrs': ['aborted'] + appt.lasterrs[-4:] + } + await self.core.addCronEdits(appt.iden, edits) + await self.core.feedBeholder('cron:stop', {'iden': appt.iden}) + + if appt.nexttime is None: + self._delete_appt_from_heap(appt) async def runloop(self): ''' Task loop to issue query tasks at the right times. ''' + await self.clearRunningStatus() while not self.isfini: timeout = None diff --git a/synapse/tests/test_lib_agenda.py b/synapse/tests/test_lib_agenda.py index 285b793610..fe8422498d 100644 --- a/synapse/tests/test_lib_agenda.py +++ b/synapse/tests/test_lib_agenda.py @@ -441,7 +441,7 @@ def looptime(): await self.asyncraises(s_exc.DupIden, core.addCronJob(cdef)) await core.delCronJob(viewiden) - self.nn(core.getAuthGate(viewiden)) + self.nn(await core.getAuthGate(viewiden)) async def test_agenda_persistence(self): ''' Test we can make/change/delete appointments and they are persisted to storage ''' @@ -1097,3 +1097,127 @@ async def task(): self.eq(cdef01.get('lastresult'), 'cancelled') self.gt(cdef00['laststarttime'], 0) self.eq(cdef00['laststarttime'], cdef01['laststarttime']) + + async def test_agenda_graceful_promotion_with_running_cron(self): + + async with self.getTestAha() as aha: + + conf00 = { + 'aha:provision': await aha.addAhaSvcProv('00.cortex') + } + + async with self.getTestCore(conf=conf00) as core00: + self.false(core00.conf.get('mirror')) + + q = ''' + while((true)) { + $lib.log.error('I AM A ERROR LOG MESSAGE') + $lib.time.sleep(6) + } + ''' + msgs = await core00.stormlist('cron.at --now $q', opts={'vars': {'q': q}}) + self.stormHasNoWarnErr(msgs) + + crons00 = await core00.callStorm('return($lib.cron.list())') + self.len(1, crons00) + + prov01 = {'mirror': '00.cortex'} + conf01 = { + 'aha:provision': await aha.addAhaSvcProv('01.cortex', provinfo=prov01), + } + + async with self.getTestCore(conf=conf01) as core01: + + with self.getAsyncLoggerStream('synapse.storm.log', 'I AM A ERROR LOG MESSAGE') as stream: + self.true(await stream.wait(timeout=6)) + + cron = await core00.callStorm('return($lib.cron.list())') + self.len(1, cron) + self.true(cron[0].get('isrunning')) + + await core01.promote(graceful=True) + + self.false(core00.isactive) + self.true(core01.isactive) + + await core00.sync() + + cron00 = await core00.callStorm('return($lib.cron.list())') + self.len(1, cron00) + self.false(cron00[0].get('isrunning')) + self.eq(cron00[0].get('lasterrs')[0], 'aborted') + + cron01 = await core01.callStorm('return($lib.cron.list())') + self.len(1, cron01) + self.false(cron01[0].get('isrunning')) + self.eq(cron01[0].get('lasterrs')[0], 'aborted') + + async def test_agenda_force_promotion_with_running_cron(self): + + async with self.getTestAha() as aha: + + conf00 = { + 'aha:provision': await aha.addAhaSvcProv('00.cortex') + } + + async with self.getTestCore(conf=conf00) as core00: + self.false(core00.conf.get('mirror')) + + q = ''' + while((true)) { + $lib.log.error('I AM A ERROR LOG MESSAGE') + $lib.time.sleep(6) + } + ''' + msgs = await core00.stormlist('cron.at --now $q', opts={'vars': {'q': q}}) + self.stormHasNoWarnErr(msgs) + + crons00 = await core00.callStorm('return($lib.cron.list())') + self.len(1, crons00) + + prov01 = {'mirror': '00.cortex'} + conf01 = { + 'aha:provision': await aha.addAhaSvcProv('01.cortex', provinfo=prov01), + } + + async with self.getTestCore(conf=conf01) as core01: + + cron = await core00.callStorm('return($lib.cron.list())') + self.len(1, cron) + self.true(cron[0].get('isrunning')) + + await core01.promote(graceful=False) + + self.true(core00.isactive) + self.true(core01.isactive) + + cron01 = await core01.callStorm('return($lib.cron.list())') + self.len(1, cron01) + self.false(cron01[0].get('isrunning')) + self.eq(cron01[0].get('lasterrs')[0], 'aborted') + + async def test_agenda_clear_running_none_nexttime(self): + + async with self.getTestCore() as core: + + cdef = { + 'creator': core.auth.rootuser.iden, + 'iden': s_common.guid(), + 'storm': '$lib.log.info("test")', + 'reqs': {}, + 'incunit': 'minute', + 'incvals': 1 + } + await core.addCronJob(cdef) + + appt = core.agenda.appts[cdef['iden']] + self.true(appt in core.agenda.apptheap) + + appt.isrunning = True + appt.nexttime = None + + await core.agenda.clearRunningStatus() + self.false(appt in core.agenda.apptheap) + + crons = await core.callStorm('return($lib.cron.list())') + self.len(1, crons) From f3e9978dbf36f9fb4a6ce6c23097169c745aa47e Mon Sep 17 00:00:00 2001 From: vEpiphyte Date: Wed, 15 Jan 2025 12:09:06 -0500 Subject: [PATCH 04/19] SynTest - remove unused withStableUids helpers, test coverage for helpers, fix lmdb test (#4070) Remove withStableUid helpers. No internal or public use of this method is found. Add tests for checkNode helpers. Fix LMDB slab test to use a slab as a context manager. These were QOL changes from #4063 --- synapse/tests/test_lib_lmdbslab.py | 6 ++--- synapse/tests/test_utils.py | 35 +++++++++++++++--------------- synapse/tests/utils.py | 35 ------------------------------ 3 files changed, 20 insertions(+), 56 deletions(-) diff --git a/synapse/tests/test_lib_lmdbslab.py b/synapse/tests/test_lib_lmdbslab.py index 3956c5fd3e..71e459bdcc 100644 --- a/synapse/tests/test_lib_lmdbslab.py +++ b/synapse/tests/test_lib_lmdbslab.py @@ -338,9 +338,9 @@ def progfunc(count): # Ensure that our envar override for memory locking is acknowledged with self.setTstEnvars(SYN_LOCKMEM_DISABLE='1'): - slab = await s_lmdbslab.Slab.anit(path, map_size=1000000, lockmemory=True) - self.false(slab.lockmemory) - self.none(slab.memlocktask) + async with await s_lmdbslab.Slab.anit(path, map_size=1000000, lockmemory=True) as slab: + self.false(slab.lockmemory) + self.none(slab.memlocktask) def simplenow(self): self._nowtime += 1000 diff --git a/synapse/tests/test_utils.py b/synapse/tests/test_utils.py index f452cc5026..af323e58f2 100644 --- a/synapse/tests/test_utils.py +++ b/synapse/tests/test_utils.py @@ -244,24 +244,6 @@ async def test_storm_msgs(self): with self.raises(AssertionError): self.stormHasNoWarnErr(msgs) - async def test_stable_uids(self): - with self.withStableUids(): - guid = s_common.guid() - self.eq('000000', guid[:6]) - guid2 = s_common.guid() - self.ne(guid, guid2) - - guid = s_common.guid(42) - self.ne('000000', guid[:6]) - - buid = s_common.buid() - self.eq(b'\00\00\00\00\00\00', buid[:6]) - buid2 = s_common.buid() - self.ne(buid, buid2) - - buid = s_common.buid(42) - self.ne(b'\00\00\00\00\00\00', buid[:6]) - def test_utils_certdir(self): oldcertdirn = s_certdir.getCertDirn() oldcertdir = s_certdir.getCertDir() @@ -297,3 +279,20 @@ def test_utils_certdir(self): # Patch is removed and singleton behavior is restored self.true(oldcertdir is s_certdir.getCertDir()) self.eq(oldcertdirn, s_certdir.getCertDirn()) + + async def test_checknode(self): + async with self.getTestCore() as core: + nodes = await core.nodes('[test:comp=(1, test)]') + self.len(1, nodes) + self.checkNode(nodes[0], (('test:comp', (1, 'test')), {'hehe': 1, 'haha': 'test'})) + with self.raises(AssertionError): + self.checkNode(nodes[0], (('test:comp', (1, 'newp')), {'hehe': 1, 'haha': 'test'})) + with self.raises(AssertionError): + self.checkNode(nodes[0], (('test:comp', (1, 'test')), {'hehe': 1, 'haha': 'newp'})) + with self.getAsyncLoggerStream('synapse.tests.utils', 'untested properties') as stream: + self.checkNode(nodes[0], (('test:comp', (1, 'test')), {'hehe': 1})) + self.true(await stream.wait(timeout=12)) + + await self.checkNodes(core, [('test:comp', (1, 'test')),]) + with self.raises(AssertionError): + await self.checkNodes(core, [('test:comp', (1, 'newp')),]) diff --git a/synapse/tests/utils.py b/synapse/tests/utils.py index 1bd495f3bf..a6b6145b5c 100644 --- a/synapse/tests/utils.py +++ b/synapse/tests/utils.py @@ -1005,8 +1005,6 @@ class SynTest(unittest.TestCase): ''' def __init__(self, *args, **kwargs): unittest.TestCase.__init__(self, *args, **kwargs) - self._NextBuid = 0 - self._NextGuid = 0 for s in dir(self): attr = getattr(self, s, None) @@ -2377,39 +2375,6 @@ async def getTestTeleHive(self): yield hive - def stablebuid(self, valu=None): - ''' - A stable buid generation for testing purposes - ''' - if valu is None: - retn = self._NextBuid.to_bytes(32, 'big') - self._NextBuid += 1 - return retn - - byts = s_msgpack.en(valu) - return hashlib.sha256(byts).digest() - - def stableguid(self, valu=None): - ''' - A stable guid generation for testing purposes - ''' - if valu is None: - retn = s_common.ehex(self._NextGuid.to_bytes(16, 'big')) - self._NextGuid += 1 - return retn - - byts = s_msgpack.en(valu) - return hashlib.md5(byts, usedforsecurity=False).hexdigest() - - @contextlib.contextmanager - def withStableUids(self): - ''' - A context manager that generates guids and buids in sequence so that successive test runs use the same - data - ''' - with mock.patch('synapse.common.guid', self.stableguid), mock.patch('synapse.common.buid', self.stablebuid): - yield - async def runCoreNodes(self, core, query, opts=None): ''' Run a storm query through a Cortex as a SchedCoro and return the results. From f577ccb6fc5657222fba200ebdc9c87ba7a4dcf0 Mon Sep 17 00:00:00 2001 From: vEpiphyte Date: Wed, 15 Jan 2025 12:10:36 -0500 Subject: [PATCH 05/19] Prevent functions from throwing loop control flow exceptions; convert them into StormRuntimeError. SYN-8397 (#4025) Co-authored-by: blackout Co-authored-by: Cisphyx --- changes/b4642a502f49353787b4f6632a6a6566.yaml | 9 ++ synapse/cortex.py | 9 +- synapse/exc.py | 2 +- synapse/lib/ast.py | 67 +++++---- synapse/lib/storm.py | 4 +- synapse/lib/stormctrl.py | 94 ++++++++++++- synapse/lib/stormtypes.py | 2 +- synapse/lib/view.py | 23 +++- synapse/tests/test_cortex.py | 113 ++++++++++++++- synapse/tests/test_exc.py | 1 + synapse/tests/test_lib_storm.py | 130 +++++++++++++++++- synapse/tests/test_lib_stormctrl.py | 65 +++++++++ synapse/tests/test_lib_stormtypes.py | 6 +- 13 files changed, 475 insertions(+), 50 deletions(-) create mode 100644 changes/b4642a502f49353787b4f6632a6a6566.yaml create mode 100644 synapse/tests/test_lib_stormctrl.py diff --git a/changes/b4642a502f49353787b4f6632a6a6566.yaml b/changes/b4642a502f49353787b4f6632a6a6566.yaml new file mode 100644 index 0000000000..0a2cae37b9 --- /dev/null +++ b/changes/b4642a502f49353787b4f6632a6a6566.yaml @@ -0,0 +1,9 @@ +--- +desc: Fixed an issue with the Storm loop and generator keywords, ``continue``, ``break``, + and ``stop``. Using these keywords outside of a loop or generator function will now + raise a ``StormRuntimeError`` exception. Using these keywords to tear down the Storm + runtime will now emit an ``err`` message with the type ``StormRuntimeError`` and a + message indicating the invalid use of the keywords. +prs: [] +type: bug +... diff --git a/synapse/cortex.py b/synapse/cortex.py index ac7999bd83..759a9d9789 100644 --- a/synapse/cortex.py +++ b/synapse/cortex.py @@ -5909,7 +5909,6 @@ async def storm(self, text, opts=None): opts = self._initStormOpts(opts) if self.stormpool is not None and opts.get('mirror', True): - extra = await self.getLogExtra(text=text) proxy = await self._getMirrorProxy(opts) if proxy is not None: @@ -5929,7 +5928,7 @@ async def storm(self, text, opts=None): except s_exc.TimeOut: mesg = 'Timeout waiting for query mirror, running locally instead.' - logger.warning(mesg) + logger.warning(mesg, extra=extra) if (nexsoffs := opts.get('nexsoffs')) is not None: if not await self.waitNexsOffs(nexsoffs, timeout=opts.get('nexstimeout')): @@ -5944,7 +5943,6 @@ async def callStorm(self, text, opts=None): opts = self._initStormOpts(opts) if self.stormpool is not None and opts.get('mirror', True): - extra = await self.getLogExtra(text=text) proxy = await self._getMirrorProxy(opts) if proxy is not None: @@ -5961,7 +5959,7 @@ async def callStorm(self, text, opts=None): return await proxy.callStorm(text, opts=mirropts) except s_exc.TimeOut: mesg = 'Timeout waiting for query mirror, running locally instead.' - logger.warning(mesg) + logger.warning(mesg, extra=extra) if (nexsoffs := opts.get('nexsoffs')) is not None: if not await self.waitNexsOffs(nexsoffs, timeout=opts.get('nexstimeout')): @@ -5974,7 +5972,6 @@ async def exportStorm(self, text, opts=None): opts = self._initStormOpts(opts) if self.stormpool is not None and opts.get('mirror', True): - extra = await self.getLogExtra(text=text) proxy = await self._getMirrorProxy(opts) if proxy is not None: @@ -5994,7 +5991,7 @@ async def exportStorm(self, text, opts=None): except s_exc.TimeOut: mesg = 'Timeout waiting for query mirror, running locally instead.' - logger.warning(mesg) + logger.warning(mesg, extra=extra) if (nexsoffs := opts.get('nexsoffs')) is not None: if not await self.waitNexsOffs(nexsoffs, timeout=opts.get('nexstimeout')): diff --git a/synapse/exc.py b/synapse/exc.py index 8c5b2ef752..12d6a36129 100644 --- a/synapse/exc.py +++ b/synapse/exc.py @@ -58,7 +58,7 @@ def setdefault(self, name, valu): def update(self, items: dict): '''Update multiple items in the errinfo dict at once.''' - self.errinfo.update(**items) + self.errinfo.update(items) self._setExcMesg() class StormRaise(SynErr): diff --git a/synapse/lib/ast.py b/synapse/lib/ast.py index 816797dfac..c82edb583e 100644 --- a/synapse/lib/ast.py +++ b/synapse/lib/ast.py @@ -1035,13 +1035,13 @@ async def run(self, runt, genr): yield item except s_stormctrl.StormBreak as e: - if e.item is not None: - yield e.item + if (eitem := e.get('item')) is not None: + yield eitem break except s_stormctrl.StormContinue as e: - if e.item is not None: - yield e.item + if (eitem := e.get('item')) is not None: + yield eitem continue finally: @@ -1094,13 +1094,13 @@ async def run(self, runt, genr): yield jtem except s_stormctrl.StormBreak as e: - if e.item is not None: - yield e.item + if (eitem := e.get('item')) is not None: + yield eitem break except s_stormctrl.StormContinue as e: - if e.item is not None: - yield e.item + if (eitem := e.get('item')) is not None: + yield eitem continue finally: @@ -1124,13 +1124,13 @@ async def run(self, runt, genr): await asyncio.sleep(0) except s_stormctrl.StormBreak as e: - if e.item is not None: - yield e.item + if (eitem := e.get('item')) is not None: + yield eitem break except s_stormctrl.StormContinue as e: - if e.item is not None: - yield e.item + if (eitem := e.get('item')) is not None: + yield eitem continue finally: @@ -1148,13 +1148,13 @@ async def run(self, runt, genr): await asyncio.sleep(0) except s_stormctrl.StormBreak as e: - if e.item is not None: - yield e.item + if (eitem := e.get('item')) is not None: + yield eitem break except s_stormctrl.StormContinue as e: - if e.item is not None: - yield e.item + if (eitem := e.get('item')) is not None: + yield eitem continue finally: @@ -4790,9 +4790,9 @@ async def run(self, runt, genr): yield _ async for node, path in genr: - raise s_stormctrl.StormBreak(item=(node, path)) + raise self.addExcInfo(s_stormctrl.StormBreak(item=(node, path))) - raise s_stormctrl.StormBreak() + raise self.addExcInfo(s_stormctrl.StormBreak()) class ContinueOper(AstNode): @@ -4803,9 +4803,9 @@ async def run(self, runt, genr): yield _ async for node, path in genr: - raise s_stormctrl.StormContinue(item=(node, path)) + raise self.addExcInfo(s_stormctrl.StormContinue(item=(node, path))) - raise s_stormctrl.StormContinue() + raise self.addExcInfo(s_stormctrl.StormContinue()) class IfClause(AstNode): pass @@ -4896,20 +4896,26 @@ async def run(self, runt, genr): count = 0 async for node, path in genr: count += 1 - await runt.emit(await self.kids[0].compute(runt, path)) + try: + await runt.emit(await self.kids[0].compute(runt, path)) + except s_exc.StormRuntimeError as e: + raise self.addExcInfo(e) yield node, path # no items in pipeline and runtsafe. execute once. if count == 0 and self.isRuntSafe(runt): - await runt.emit(await self.kids[0].compute(runt, None)) + try: + await runt.emit(await self.kids[0].compute(runt, None)) + except s_exc.StormRuntimeError as e: + raise self.addExcInfo(e) class Stop(Oper): async def run(self, runt, genr): for _ in (): yield _ async for node, path in genr: - raise s_stormctrl.StormStop() - raise s_stormctrl.StormStop() + raise self.addExcInfo(s_stormctrl.StormStop()) + raise self.addExcInfo(s_stormctrl.StormStop()) class FuncArgs(AstNode): ''' @@ -5053,9 +5059,16 @@ async def callfunc(self, runt, argdefs, args, kwargs, funcpath): await asyncio.sleep(0) return None - except s_stormctrl.StormReturn as e: return e.item + except s_stormctrl.StormLoopCtrl as e: + mesg = f'function {self.name} - Loop control statement "{e.statement}" used outside of a loop.' + raise self.addExcInfo(s_exc.StormRuntimeError(mesg=mesg, function=self.name, + statement=e.statement)) from e + except s_stormctrl.StormGenrCtrl as e: + mesg = f'function {self.name} - Generator control statement "{e.statement}" used outside of a generator function.' + raise self.addExcInfo(s_exc.StormRuntimeError(mesg=mesg, function=self.name, + statement=e.statement)) from e async def genr(): async with runt.getSubRuntime(self.kids[2], opts=opts) as subr: @@ -5075,5 +5088,9 @@ async def genr(): yield node, path except s_stormctrl.StormStop: return + except s_stormctrl.StormLoopCtrl as e: + mesg = f'function {self.name} - Loop control statement "{e.statement}" used outside of a loop.' + raise self.addExcInfo(s_exc.StormRuntimeError(mesg=mesg, function=self.name, + statement=e.statement)) from e return genr() diff --git a/synapse/lib/storm.py b/synapse/lib/storm.py index a80263db09..c062e5bd23 100644 --- a/synapse/lib/storm.py +++ b/synapse/lib/storm.py @@ -3552,7 +3552,7 @@ async def _handleBoundMethod(self, func, runt: Runtime, verbose: bool =False): await runt.printf(line) else: # pragma: no cover - raise s_exc.StormRuntimeError(mesgf=f'Unknown bound method {func}') + raise s_exc.StormRuntimeError(mesg=f'Unknown bound method {func}') async def _handleStormLibMethod(self, func, runt: Runtime, verbose: bool =False): # Storm library methods must be derived from a library definition. @@ -3583,7 +3583,7 @@ async def _handleStormLibMethod(self, func, runt: Runtime, verbose: bool =False) await runt.printf(line) else: # pragma: no cover - raise s_exc.StormRuntimeError(mesgf=f'Unknown runtime lib method {func} {cls} {fname}') + raise s_exc.StormRuntimeError(mesg=f'Unknown runtime lib method {func} {cls} {fname}') class DiffCmd(Cmd): ''' diff --git a/synapse/lib/stormctrl.py b/synapse/lib/stormctrl.py index c07c780873..7aeaacebea 100644 --- a/synapse/lib/stormctrl.py +++ b/synapse/lib/stormctrl.py @@ -1,9 +1,91 @@ class StormCtrlFlow(Exception): + ''' + Base class all StormCtrlFlow exceptions derive from. + ''' + def __init__(self): + raise NotImplementedError + +class _SynErrMixin(Exception): + ''' + An exception mixin to give some control flow classes functionality like SynErr. + ''' + def __init__(self, *args, **info): + self.errinfo = info + Exception.__init__(self, self._getExcMsg()) + + def _getExcMsg(self): + props = sorted(self.errinfo.items()) + displ = ' '.join(['%s=%r' % (p, v) for (p, v) in props]) + return '%s: %s' % (self.__class__.__name__, displ) + + def _setExcMesg(self): + '''Should be called when self.errinfo is modified.''' + self.args = (self._getExcMsg(),) + + def __setstate__(self, state): + '''Pickle support.''' + super(StormCtrlFlow, self).__setstate__(state) + self._setExcMesg() + + def items(self): + return {k: v for k, v in self.errinfo.items()} + + def get(self, name, defv=None): + ''' + Return a value from the errinfo dict. + + Example: + + try: + foothing() + except SynErr as e: + blah = e.get('blah') + + ''' + return self.errinfo.get(name, defv) + + def set(self, name, valu): + ''' + Set a value in the errinfo dict. + ''' + self.errinfo[name] = valu + self._setExcMesg() + + def setdefault(self, name, valu): + ''' + Set a value in errinfo dict if it is not already set. + ''' + if name in self.errinfo: + return + self.errinfo[name] = valu + self._setExcMesg() + + def update(self, items: dict): + '''Update multiple items in the errinfo dict at once.''' + self.errinfo.update(items) + self._setExcMesg() + +class StormLoopCtrl(_SynErrMixin): + # Control flow statements for WHILE and FOR loop control + statement = '' + +class StormGenrCtrl(_SynErrMixin): + # Control flow statements for GENERATOR control + statement = '' + +class StormStop(StormGenrCtrl, StormCtrlFlow): + statement = 'stop' + +class StormBreak(StormLoopCtrl, StormCtrlFlow): + statement = 'break' + +class StormContinue(StormLoopCtrl, StormCtrlFlow): + statement = 'continue' + +class StormExit(_SynErrMixin, StormCtrlFlow): pass + +# StormReturn is kept thin since it is commonly used and just +# needs to be the container for moving an item up a frame. +class StormReturn(StormCtrlFlow): def __init__(self, item=None): self.item = item - -class StormExit(StormCtrlFlow): pass -class StormStop(StormCtrlFlow): pass -class StormBreak(StormCtrlFlow): pass -class StormReturn(StormCtrlFlow): pass -class StormContinue(StormCtrlFlow): pass diff --git a/synapse/lib/stormtypes.py b/synapse/lib/stormtypes.py index 068104029d..8147245732 100644 --- a/synapse/lib/stormtypes.py +++ b/synapse/lib/stormtypes.py @@ -1663,7 +1663,7 @@ async def _exit(self, mesg=None, **kwargs): if mesg: mesg = await self._get_mesg(mesg, **kwargs) await self.runt.warn(mesg, log=False) - raise s_stormctrl.StormExit(mesg) + raise s_stormctrl.StormExit(mesg=mesg) raise s_stormctrl.StormExit() @stormfunc(readonly=True) diff --git a/synapse/lib/view.py b/synapse/lib/view.py index e69c60bbd3..89198fbf68 100644 --- a/synapse/lib/view.py +++ b/synapse/lib/view.py @@ -635,7 +635,6 @@ def isForkOf(self, viewiden): async def _calcForkLayers(self): # recompute the proper set of layers for a forked view # (this may only be called from within a nexus handler) - ''' We spent a lot of time thinking/talking about this so some hefty comments are in order: @@ -953,6 +952,15 @@ async def callStorm(self, text, opts=None): extra={'synapse': {'text': text, 'username': user.name, 'user': user.iden}}) raise + except (s_stormctrl.StormLoopCtrl, s_stormctrl.StormGenrCtrl) as e: + if isinstance(e, s_stormctrl.StormLoopCtrl): + mesg = f'Loop control statement "{e.statement}" used outside of a loop.' + else: + mesg = f'Generator control statement "{e.statement}" used outside of a generator function.' + logmesg = f'Error during storm execution for {{ {text} }} - {mesg}' + logger.exception(logmesg, extra={'synapse': {'text': text, 'username': user.name, 'user': user.iden}}) + raise s_exc.StormRuntimeError(mesg=mesg, statement=e.statement, highlight=e.get('highlight')) from e + except Exception: logger.exception(f'Error during callStorm execution for {{ {text} }}', extra={'synapse': {'text': text, 'username': user.name, 'user': user.iden}}) @@ -1055,8 +1063,17 @@ async def runStorm(): raise except Exception as e: - logger.exception(f'Error during storm execution for {{ {text} }}', - extra={'synapse': {'text': text, 'username': user.name, 'user': user.iden}}) + mesg = '' + if isinstance(e, s_stormctrl.StormLoopCtrl): + mesg = f'Loop control statement "{e.statement}" used outside of a loop.' + e = s_exc.StormRuntimeError(mesg=mesg, statement=e.statement, highlight=e.get('highlight')) + elif isinstance(e, s_stormctrl.StormGenrCtrl): + mesg = f'Generator control statement "{e.statement}" used outside of a generator function.' + e = s_exc.StormRuntimeError(mesg=mesg, statement=e.statement, highlight=e.get('highlight')) + logmesg = f'Error during storm execution for {{ {text} }}' + if mesg: + logmesg = f'{logmesg} - {mesg}' + logger.exception(logmesg, extra={'synapse': {'text': text, 'username': user.name, 'user': user.iden}}) enfo = s_common.err(e) enfo[1].pop('esrc', None) enfo[1].pop('ename', None) diff --git a/synapse/tests/test_cortex.py b/synapse/tests/test_cortex.py index 8729ee88a4..ffc88daeb5 100644 --- a/synapse/tests/test_cortex.py +++ b/synapse/tests/test_cortex.py @@ -1033,6 +1033,26 @@ async def test_cortex_callstorm(self): self.eq(cm.exception.get('hehe'), 'haha') self.eq(cm.exception.get('key'), 1) + # We convert StormLoopCtrl and StormGenrCtrl into StormRuntimeError + opts = {'vars': {'i': 2}} + q = 'if ($i = 2) { break }' + with self.raises(s_exc.StormRuntimeError) as cm: + await core.callStorm(q, opts=opts) + self.eq(cm.exception.get('mesg'), + 'Loop control statement "break" used outside of a loop.') + + q = 'if ($i = 2) { continue }' + with self.raises(s_exc.StormRuntimeError) as cm: + await core.callStorm(q, opts=opts) + self.eq(cm.exception.get('mesg'), + 'Loop control statement "continue" used outside of a loop.') + + q = 'if ($i = 2) { stop }' + with self.raises(s_exc.StormRuntimeError) as cm: + await core.callStorm(q, opts=opts) + self.eq(cm.exception.get('mesg'), + 'Generator control statement "stop" used outside of a generator function.') + with self.getAsyncLoggerStream('synapse.lib.view', 'callStorm cancelled') as stream: async with core.getLocalProxy() as proxy: @@ -1081,7 +1101,7 @@ async def test_cortex_callstorm(self): retn = await resp.json() self.eq('err', retn.get('status')) self.eq('StormExit', retn.get('code')) - self.eq('', retn.get('mesg')) + self.eq('StormExit: ', retn.get('mesg')) # No body async with sess.get(f'https://localhost:{port}/api/v1/storm/call') as resp: @@ -3499,6 +3519,97 @@ async def test_storm_contbreak(self): for node in nodes: self.nn(node.getTag('hehe')) + # Break and Continue cannot cross function boundaries and will instead raise a catchable StormRuntimeError + keywords = ('break', 'continue') + base_func_q = ''' + function inner(v) { + if ( $v = 2 ) { + KEYWORD + } + return ( $v ) + } + $N = (5) + + for $valu in $lib.range($N) { + $lib.print(`{$inner($valu)}/{$N}`) + } + ''' + func_catch_q = ''' + function inner(v) { + if ( $v = 2 ) { + KEYWORD + } + return ( $v ) + } + $N = (5) + try { + for $valu in $lib.range($N) { + $lib.print(`{$inner($valu)}/{$N}`) + } + } catch StormRuntimeError as err { + $lib.print(`caught: {$err.mesg}`) + } + ''' + for keyword in keywords: + q = base_func_q.replace('KEYWORD', keyword) + msgs = await core.stormlist(q) + self.stormIsInPrint('1/5', msgs) + self.stormNotInPrint('2/5', msgs) + self.stormIsInErr(f'function inner - Loop control statement "{keyword}" used outside of a loop.', + msgs) + + q = func_catch_q.replace('KEYWORD', keyword) + msgs = await core.stormlist(q) + self.stormIsInPrint('1/5', msgs) + self.stormNotInPrint('2/5', msgs) + self.stormIsInPrint(f'function inner - Loop control statement "{keyword}" used outside of a loop.', + msgs) + + # The toplevel use of the keywords will convert them into StormRuntimeError in the message stream + # but prevent them from being caught. + base_top_q = ''' + $N = (5) + for $j in $lib.range($N) { + if ($j = 2) { break } + $lib.print(`{$j}/{$N}`) + } + if ($j = 2) { + KEYWORD + } + ''' + top_catch_q = ''' + $N = (5) + for $j in $lib.range($N) { + if ($j = 2) { break } + $lib.print(`{$j}/{$N}`) + } + try { + if ($j = 2) { + KEYWORD + } + } catch StormRuntimeError as err { + $lib.print(`caught: {$err.mesg}`) + } + ''' + for keyword in keywords: + q = base_top_q.replace('KEYWORD', keyword) + msgs = await core.stormlist(q) + self.stormIsInPrint('1/5', msgs) + self.stormNotInPrint('2/5', msgs) + self.stormIsInErr(f'Loop control statement "{keyword}" used outside of a loop.', + msgs) + errname = [m[1][0] for m in msgs if m[0] == 'err'][0] + self.eq(errname, 'StormRuntimeError') + + q = top_catch_q.replace('KEYWORD', keyword) + msgs = await core.stormlist(q) + self.stormIsInPrint('1/5', msgs) + self.stormNotInPrint('2/5', msgs) + self.stormIsInErr(f'Loop control statement "{keyword}" used outside of a loop.', + msgs) + errname = [m[1][0] for m in msgs if m[0] == 'err'][0] + self.eq(errname, 'StormRuntimeError') + async def test_storm_varcall(self): async with self.getTestCore() as core: diff --git a/synapse/tests/test_exc.py b/synapse/tests/test_exc.py index 604955ba3d..11340a3bcd 100644 --- a/synapse/tests/test_exc.py +++ b/synapse/tests/test_exc.py @@ -16,6 +16,7 @@ class ExcTest(s_t_utils.SynTest): def test_basic(self): e = s_exc.SynErr(mesg='words', foo='bar') self.eq(e.get('foo'), 'bar') + self.eq(e.items(), {'mesg': 'words', 'foo': 'bar'}) self.eq("SynErr: foo='bar' mesg='words'", str(e)) e.set('hehe', 1234) e.set('foo', 'words') diff --git a/synapse/tests/test_lib_storm.py b/synapse/tests/test_lib_storm.py index 8629226368..fbb05f9dbe 100644 --- a/synapse/tests/test_lib_storm.py +++ b/synapse/tests/test_lib_storm.py @@ -339,9 +339,135 @@ async def test_lib_storm_emit(self): prnt = [m[1]['mesg'] for m in msgs if m[0] == 'print'] self.eq(prnt, ['inner 0', 'outer 0']) - await self.asyncraises(s_exc.StormRuntimeError, core.nodes('emit foo')) + # Emit outside an emitter function raises a runtime error with posinfo + with self.raises(s_exc.StormRuntimeError) as cm: + await core.nodes('emit foo') + self.nn(cm.exception.get('highlight')) + + with self.raises(s_exc.StormRuntimeError) as cm: + await core.nodes('[test:str=emit] emit foo') + self.nn(cm.exception.get('highlight')) + + # stop cannot cross function boundaries + q = ''' + function inner(v) { + if ( $v = 2 ) { + stop + } + return ( $v ) + } + function outer(n) { + for $i in $lib.range($n) { + emit $inner($i) + } + } + $N = (5) + for $valu in $outer($N) { + $lib.print(`{$valu}/{$N}`) + } + ''' + msgs = await core.stormlist(q) + self.stormIsInPrint('1/5', msgs) + self.stormNotInPrint('2/5', msgs) + self.stormIsInErr('function inner - Generator control statement "stop" used outside of a generator ' + 'function.', + msgs) + + # The function exception raised can be caught. + q = ''' + function inner(v) { + if ( $v = 2 ) { + stop + } + return ( $v ) + } + function outer(n) { + for $i in $lib.range($n) { + emit $inner($i) + } + } + $N = (5) + try { + for $valu in $outer($N) { + $lib.print(`{$valu}/{$N}`) + } + } catch StormRuntimeError as err { + $lib.print(`caught: {$err.mesg}`) + } + ''' + msgs = await core.stormlist(q) + self.stormIsInPrint('1/5', msgs) + self.stormNotInPrint('2/5', msgs) + self.stormIsInPrint('caught: function inner - Generator control statement "stop" used outside of a' + ' generator function.', + msgs) - # include a quick test for using stop in a node yielder + # Outside a function, StopStorm is caught and converted into a StormRuntimeError for the message stream. + # Since this is tearing down the runtime, it cannot be caught. + q = ''' + $N = (5) + for $j in $lib.range($N) { + if ($j = 2) { + stop + } + $lib.print(`{$j}/{$N}`) + } + ''' + msgs = await core.stormlist(q) + self.stormIsInPrint('1/5', msgs) + self.stormNotInPrint('2/5', msgs) + self.stormIsInErr('Generator control statement "stop" used outside of a generator function.', + msgs) + errname = [m[1][0] for m in msgs if m[0] == 'err'][0] + self.eq(errname, 'StormRuntimeError') + + q = ''' + $N = (5) + try { + for $j in $lib.range($N) { + if ($j = 2) { + stop + } + $lib.print(`{$j}/{$N}`) + } + } catch StormRuntimeError as err { + $lib.print(`caught: {$err.mesg}`) + } + ''' + msgs = await core.stormlist(q) + self.stormIsInPrint('1/5', msgs) + self.stormNotInPrint('2/5', msgs) + self.stormNotInPrint('caught:', msgs) + self.stormIsInErr('Generator control statement "stop" used outside of a generator function.', + msgs) + + # Mixing a Loop control flow statement in an emitter to stop its processing + # will be converted into a catchable StormRuntimeError + q = ''' + function inner(n) { + emit $n + $n = ( $n + 1 ) + emit $n + $n = ( $n + 1 ) + if ( $n >= 2 ) { + break + } + emit $n + } + $N = (0) + try { + for $valu in $inner($N) { + $lib.print(`got {$valu}`) + } + } catch StormRuntimeError as err { + $lib.print(`caught: {$err.mesg}`) + } + ''' + msgs = await core.stormlist(q) + self.stormIsInPrint('got 1', msgs) + self.stormNotInPrint('got 2', msgs) + self.stormIsInPrint('caught: function inner - Loop control statement "break" used outside of a loop.', + msgs) async def test_lib_storm_intersect(self): async with self.getTestCore() as core: diff --git a/synapse/tests/test_lib_stormctrl.py b/synapse/tests/test_lib_stormctrl.py new file mode 100644 index 0000000000..808768178b --- /dev/null +++ b/synapse/tests/test_lib_stormctrl.py @@ -0,0 +1,65 @@ +import pickle + +import synapse.lib.stormctrl as s_stormctrl + +import synapse.tests.utils as s_t_utils + +class StormctrlTest(s_t_utils.SynTest): + def test_basic(self): + + # Classes inherit as expected + self.isinstance(s_stormctrl.StormReturn(), s_stormctrl.StormCtrlFlow) + self.isinstance(s_stormctrl.StormExit(), s_stormctrl.StormCtrlFlow) + self.isinstance(s_stormctrl.StormBreak(), s_stormctrl.StormCtrlFlow) + self.isinstance(s_stormctrl.StormContinue(), s_stormctrl.StormCtrlFlow) + self.isinstance(s_stormctrl.StormStop(), s_stormctrl.StormCtrlFlow) + + # Subtypes are noted as well + self.isinstance(s_stormctrl.StormBreak(), s_stormctrl.StormLoopCtrl) + self.isinstance(s_stormctrl.StormContinue(), s_stormctrl.StormLoopCtrl) + self.isinstance(s_stormctrl.StormStop(), s_stormctrl.StormGenrCtrl) + + # control flow and exist constructs inherit from the SynErrMixin + # return does not to keep it thin. it is used often. + self.isinstance(s_stormctrl.StormExit(), s_stormctrl._SynErrMixin) + self.isinstance(s_stormctrl.StormBreak(), s_stormctrl._SynErrMixin) + self.isinstance(s_stormctrl.StormContinue(), s_stormctrl._SynErrMixin) + self.isinstance(s_stormctrl.StormStop(), s_stormctrl._SynErrMixin) + self.false(isinstance(s_stormctrl.StormReturn(), s_stormctrl._SynErrMixin)) + + # The base class cannot be used on its own. + with self.raises(NotImplementedError): + s_stormctrl.StormCtrlFlow() + + # The _SynErrMixin classes have several methods that let us treat + # instance of them like SynErr exceptions. + e = s_stormctrl.StormExit(mesg='words', foo='bar') + self.eq(e.get('foo'), 'bar') + self.eq(e.items(), {'mesg': 'words', 'foo': 'bar'}) + self.eq("StormExit: foo='bar' mesg='words'", str(e)) + e.set('hehe', 1234) + e.set('foo', 'words') + self.eq("StormExit: foo='words' hehe=1234 mesg='words'", str(e)) + + e.setdefault('defv', 1) + self.eq("StormExit: defv=1 foo='words' hehe=1234 mesg='words'", str(e)) + + e.setdefault('defv', 2) + self.eq("StormExit: defv=1 foo='words' hehe=1234 mesg='words'", str(e)) + + e.update({'foo': 'newwords', 'bar': 'baz'}) + self.eq("StormExit: bar='baz' defv=1 foo='newwords' hehe=1234 mesg='words'", str(e)) + + # But it does not have an errname property + self.false(hasattr(e, 'errname')) + + # StormReturn is used to move objects around. + e = s_stormctrl.StormReturn('weee') + self.eq(e.item, 'weee') + + async def test_pickled_stormctrlflow(self): + e = s_stormctrl.StormExit(mesg='words', foo='bar') + buf = pickle.dumps(e) + new_e = pickle.loads(buf) + self.eq(new_e.get('foo'), 'bar') + self.eq("StormExit: foo='bar' mesg='words'", str(new_e)) diff --git a/synapse/tests/test_lib_stormtypes.py b/synapse/tests/test_lib_stormtypes.py index e0eb964b36..de87a802bd 100644 --- a/synapse/tests/test_lib_stormtypes.py +++ b/synapse/tests/test_lib_stormtypes.py @@ -5954,7 +5954,7 @@ async def test_exit(self): with self.raises(s_ctrl.StormExit) as cm: q = '[test:str=beep.sys] $lib.exit(foo)' _ = await core.callStorm(q) - self.eq(cm.exception.args, ('foo',)) + self.eq(cm.exception.get('mesg'), 'foo') # Remote tests async with core.getLocalProxy() as prox: @@ -5968,7 +5968,7 @@ async def test_exit(self): q = '[test:str=beep.sys] $lib.exit()' with self.raises(s_exc.SynErr) as cm: _ = await prox.callStorm(q) - self.eq(cm.exception.get('mesg'), '') + self.eq(cm.exception.get('mesg'), 'StormExit: ') self.eq(cm.exception.get('errx'), 'StormExit') # A warn is emitted @@ -5980,7 +5980,7 @@ async def test_exit(self): q = '[test:str=beep.sys] $lib.exit("foo {bar}", bar=baz)' with self.raises(s_exc.SynErr) as cm: _ = await prox.callStorm(q) - self.eq(cm.exception.get('mesg'), 'foo baz') + self.eq(cm.exception.get('mesg'), "StormExit: mesg='foo baz'") self.eq(cm.exception.get('errx'), 'StormExit') async def test_iter(self): From e52efdbf985e90a3e429fe1c0d22e1b7e52c008c Mon Sep 17 00:00:00 2001 From: Cisphyx Date: Wed, 15 Jan 2025 12:39:10 -0500 Subject: [PATCH 06/19] Deprecate $lib.list() (SYN-8473) (#4071) --- changes/1d7ec4bf8dbac3b96ecf801d95a7ab4c.yaml | 6 ++ .../userguides/storm_ref_automation.rstorm | 26 +++---- synapse/lib/storm.py | 2 +- synapse/lib/stormlib/scrape.py | 2 +- synapse/lib/stormlib/stix.py | 16 ++-- synapse/lib/stormtypes.py | 7 +- synapse/tests/test_axon.py | 12 +-- synapse/tests/test_cortex.py | 18 ++--- synapse/tests/test_lib_ast.py | 10 +-- synapse/tests/test_lib_layer.py | 2 +- synapse/tests/test_lib_storm.py | 38 ++++----- synapse/tests/test_lib_stormhttp.py | 10 +-- synapse/tests/test_lib_stormlib_auth.py | 10 +-- synapse/tests/test_lib_stormlib_modelext.py | 6 +- synapse/tests/test_lib_stormlib_scrape.py | 8 +- synapse/tests/test_lib_stormlib_spooled.py | 2 +- synapse/tests/test_lib_stormlib_xml.py | 10 +-- synapse/tests/test_lib_stormtypes.py | 77 ++++++++++--------- synapse/tests/test_lib_view.py | 2 +- synapse/tools/storm.py | 2 +- 20 files changed, 139 insertions(+), 127 deletions(-) create mode 100644 changes/1d7ec4bf8dbac3b96ecf801d95a7ab4c.yaml diff --git a/changes/1d7ec4bf8dbac3b96ecf801d95a7ab4c.yaml b/changes/1d7ec4bf8dbac3b96ecf801d95a7ab4c.yaml new file mode 100644 index 0000000000..c57ab40869 --- /dev/null +++ b/changes/1d7ec4bf8dbac3b96ecf801d95a7ab4c.yaml @@ -0,0 +1,6 @@ +--- +desc: The Storm function ``$lib.list()`` has been deprecated, in favor of using the ``()`` or + ``([])`` style syntax for directly declaring a list in Storm. +prs: [] +type: deprecation +... diff --git a/docs/synapse/userguides/storm_ref_automation.rstorm b/docs/synapse/userguides/storm_ref_automation.rstorm index 54cff8ce80..569ea17285 100644 --- a/docs/synapse/userguides/storm_ref_automation.rstorm +++ b/docs/synapse/userguides/storm_ref_automation.rstorm @@ -2,18 +2,18 @@ .. storm-cortex:: default -.. storm-pre:: $pkg=$lib.dict(name='docs', version='0.0.1', commands=($lib.dict(name=virustotal.file.enrich, storm=${} ),)) $lib.print($pkg) $lib.pkg.add($pkg) -.. storm-pre:: $pkg=$lib.dict(name='docs', version='0.0.1', commands=($lib.dict(name=virustotal.file.behavior, storm=${} ),)) $lib.print($pkg) $lib.pkg.add($pkg) -.. storm-pre:: $pkg=$lib.dict(name='docs', version='0.0.1', commands=($lib.dict(name=virustotal.pdns, storm=${} ),)) $lib.print($pkg) $lib.pkg.add($pkg) -.. storm-pre:: $pkg=$lib.dict(name='docs', version='0.0.1', commands=($lib.dict(name=virustotal.commfiles, storm=${} ),)) $lib.print($pkg) $lib.pkg.add($pkg) -.. storm-pre:: $pkg=$lib.dict(name='docs', version='0.0.1', commands=($lib.dict(name=alienvault.otx.files, storm=${} ),)) $lib.print($pkg) $lib.pkg.add($pkg) -.. storm-pre:: $pkg=$lib.dict(name='docs', version='0.0.1', commands=($lib.dict(name=alienvault.otx.domain, storm=${} ),)) $lib.print($pkg) $lib.pkg.add($pkg) -.. storm-pre:: $pkg=$lib.dict(name='docs', version='0.0.1', commands=($lib.dict(name=alienvault.otx.pdns, storm=${} ),)) $lib.print($pkg) $lib.pkg.add($pkg) -.. storm-pre:: $pkg=$lib.dict(name='docs', version='0.0.1', commands=($lib.dict(name=alienvault.otx.ip, storm=${} ),)) $lib.print($pkg) $lib.pkg.add($pkg) -.. storm-pre:: $pkg=$lib.dict(name='docs', version='0.0.1', commands=($lib.dict(name=nettools.dns, storm=${} ),)) $lib.print($pkg) $lib.pkg.add($pkg) -.. storm-pre:: $pkg=$lib.dict(name='docs', version='0.0.1', commands=($lib.dict(name=nettools.whois, storm=${} ),)) $lib.print($pkg) $lib.pkg.add($pkg) -.. storm-pre:: $pkg=$lib.dict(name='docs', version='0.0.1', commands=($lib.dict(name=maxmind, storm=${} ),)) $lib.print($pkg) $lib.pkg.add($pkg) -.. storm-pre:: $pkg=$lib.dict(name='docs', version='0.0.1', commands=($lib.dict(name=censys.hosts.enrich, storm=${} ),)) $lib.print($pkg) $lib.pkg.add($pkg) +.. storm-pre:: $pkg=({'name': 'docs', 'version': '0.0.1', 'commands': [{'name': 'virustotal.file.enrich', 'storm': ${}}]}) $lib.print($pkg) $lib.pkg.add($pkg) +.. storm-pre:: $pkg=({'name': 'docs', 'version': '0.0.1', 'commands': [{'name': 'virustotal.file.behavior', 'storm': ${}}]}) $lib.print($pkg) $lib.pkg.add($pkg) +.. storm-pre:: $pkg=({'name': 'docs', 'version': '0.0.1', 'commands': [{'name': 'virustotal.pdns', 'storm': ${}}]}) $lib.print($pkg) $lib.pkg.add($pkg) +.. storm-pre:: $pkg=({'name': 'docs', 'version': '0.0.1', 'commands': [{'name': 'virustotal.commfiles', 'storm': ${}}]}) $lib.print($pkg) $lib.pkg.add($pkg) +.. storm-pre:: $pkg=({'name': 'docs', 'version': '0.0.1', 'commands': [{'name': 'alienvault.otx.files', 'storm': ${}}]}) $lib.print($pkg) $lib.pkg.add($pkg) +.. storm-pre:: $pkg=({'name': 'docs', 'version': '0.0.1', 'commands': [{'name': 'alienvault.otx.domain', 'storm': ${}}]}) $lib.print($pkg) $lib.pkg.add($pkg) +.. storm-pre:: $pkg=({'name': 'docs', 'version': '0.0.1', 'commands': [{'name': 'alienvault.otx.pdns', 'storm': ${}}]}) $lib.print($pkg) $lib.pkg.add($pkg) +.. storm-pre:: $pkg=({'name': 'docs', 'version': '0.0.1', 'commands': [{'name': 'alienvault.otx.ip', 'storm': ${}}]}) $lib.print($pkg) $lib.pkg.add($pkg) +.. storm-pre:: $pkg=({'name': 'docs', 'version': '0.0.1', 'commands': [{'name': 'nettools.dns', 'storm': ${}}]}) $lib.print($pkg) $lib.pkg.add($pkg) +.. storm-pre:: $pkg=({'name': 'docs', 'version': '0.0.1', 'commands': [{'name': 'nettools.whois', 'storm': ${}}]}) $lib.print($pkg) $lib.pkg.add($pkg) +.. storm-pre:: $pkg=({'name': 'docs', 'version': '0.0.1', 'commands': [{'name': 'maxmind', 'storm': ${}}]}) $lib.print($pkg) $lib.pkg.add($pkg) +.. storm-pre:: $pkg=({'name': 'docs', 'version': '0.0.1', 'commands': [{'name': 'censys.hosts.enrich', 'storm': ${}}]}) $lib.print($pkg) $lib.pkg.add($pkg) .. _storm-ref-automation: @@ -363,7 +363,7 @@ The output of ``cron.list`` includes the following columns: You want to create a cron job that will run every hour to download the latest MISP data using the ``misp.sync`` command. -.. storm-pre:: $pkg=$lib.dict(name='docs', version='0.0.1', commands=($lib.dict(name=misp.sync, storm=${} ),)) $lib.print($pkg) $lib.pkg.add($pkg) +.. storm-pre:: $pkg=({'name': 'docs', 'version': '0.0.1', 'commands': [{'name': 'misp.sync', 'storm': ${}}]}) $lib.print($pkg) $lib.pkg.add($pkg) .. storm-pre:: cron.add --hour +1 { misp.sync } :: diff --git a/synapse/lib/storm.py b/synapse/lib/storm.py index c062e5bd23..c09d1738e7 100644 --- a/synapse/lib/storm.py +++ b/synapse/lib/storm.py @@ -1603,7 +1603,7 @@ function fetchnodes(url, ssl) { $resp = $lib.inet.http.get($url, ssl_verify=$ssl) if ($resp.code = 200) { - $nodes = $lib.list() + $nodes = () for $valu in $resp.msgpack() { $nodes.append($valu) } diff --git a/synapse/lib/stormlib/scrape.py b/synapse/lib/stormlib/scrape.py index 0e62bed6c0..1435a6d1aa 100644 --- a/synapse/lib/stormlib/scrape.py +++ b/synapse/lib/stormlib/scrape.py @@ -71,7 +71,7 @@ class LibScrape(s_stormtypes.Lib): $form="ps:name" function scrape(text, form) { - $ret = $lib.list() + $ret = () for ($valu, $info) in $lib.scrape.genMatches($text, $re) { $ret.append(($form, $valu, $info)) } diff --git a/synapse/lib/stormlib/stix.py b/synapse/lib/stormlib/stix.py index 78f24d2ba3..d86465b76c 100644 --- a/synapse/lib/stormlib/stix.py +++ b/synapse/lib/stormlib/stix.py @@ -74,7 +74,7 @@ def uuid4(valu=None): 'created': 'return($lib.stix.export.timestamp(.created))', 'modified': 'return($lib.stix.export.timestamp(.created))', 'sectors': ''' - init { $list = $lib.list() } + init { $list = () } -> ou:industry +:name $list.append(:name) fini { if $list { return($list) } } ''', @@ -88,7 +88,7 @@ def uuid4(valu=None): 'first_seen': '+.seen $seen=.seen return($lib.stix.export.timestamp($seen.0))', 'last_seen': '+.seen $seen=.seen return($lib.stix.export.timestamp($seen.1))', 'goals': ''' - init { $goals = $lib.list() } + init { $goals = () } -> ou:campaign:org -> ou:goal | uniq | +:name $goals.append(:name) fini { if $goals { return($goals) } } ''', @@ -183,7 +183,7 @@ def uuid4(valu=None): 'props': { 'value': 'return($node.repr())', 'resolves_to_refs': ''' - init { $refs = $lib.list() } + init { $refs = () } { -> inet:dns:a -> inet:ipv4 $refs.append($bundle.add($node)) } { -> inet:dns:aaaa -> inet:ipv6 $refs.append($bundle.add($node)) } { -> inet:dns:cname:fqdn :cname -> inet:fqdn $refs.append($bundle.add($node)) } @@ -257,7 +257,7 @@ def uuid4(valu=None): ''', 'mime_type': '+:mime return(:mime)', 'contains_refs': ''' - init { $refs = $lib.list() } + init { $refs = () } -(refs)> * $stixid = $bundle.add($node) if $stixid { $refs.append($stixid) } @@ -279,7 +279,7 @@ def uuid4(valu=None): 'is_multipart': 'return($lib.false)', 'from_ref': ':from -> inet:email return($bundle.add($node))', 'to_refs': ''' - init { $refs = $lib.list() } + init { $refs = () } { :to -> inet:email $refs.append($bundle.add($node)) } fini { if $refs { return($refs) } } ''', @@ -311,7 +311,7 @@ def uuid4(valu=None): 'created': 'return($lib.stix.export.timestamp(.created))', 'modified': 'return($lib.stix.export.timestamp(.created))', 'sample_refs': ''' - init { $refs = $lib.list() } + init { $refs = () } -> file:bytes $refs.append($bundle.add($node)) fini { if $refs { return($refs) } } ''', @@ -393,7 +393,7 @@ def uuid4(valu=None): 'description': 'if (:desc) { return (:desc) }', 'created': 'return($lib.stix.export.timestamp(.created))', 'modified': 'return($lib.stix.export.timestamp(.created))', - 'external_references': 'if :cve { $cve=:cve $cve=$cve.upper() $list=$lib.list(({"source_name": "cve", "external_id": $cve})) return($list) }' + 'external_references': 'if :cve { $cve=:cve $cve=$cve.upper() return(([{"source_name": "cve", "external_id": $cve}])) }' }, 'rels': ( @@ -439,7 +439,7 @@ def uuid4(valu=None): 'modified': 'return($lib.stix.export.timestamp(.created))', 'published': 'return($lib.stix.export.timestamp(:published))', 'object_refs': ''' - init { $refs = $lib.list() } + init { $refs = () } -(refs)> * $stixid = $bundle.add($node) if $stixid { $refs.append($stixid) } diff --git a/synapse/lib/stormtypes.py b/synapse/lib/stormtypes.py index 8147245732..8d27565100 100644 --- a/synapse/lib/stormtypes.py +++ b/synapse/lib/stormtypes.py @@ -1247,7 +1247,8 @@ class LibBase(Lib): 'desc': 'Additional keyword arguments containing data to add to the event.', }, ), 'returns': {'type': 'null', }}}, - {'name': 'list', 'desc': 'Get a Storm List object.', + {'name': 'list', 'desc': 'Get a Storm List object. This is deprecated, use ([]) to declare a list instead.', + 'deprecated': {'eolvers': 'v3.0.0'}, 'type': {'type': 'function', '_funcname': '_list', 'args': ( {'name': '*vals', 'type': 'any', 'desc': 'Initial values to place in the list.', }, @@ -1680,6 +1681,8 @@ async def _set(self, *vals): @stormfunc(readonly=True) async def _list(self, *vals): + s_common.deprecated('$lib.list()', curv='2.194.0') + await self.runt.snap.warnonce('$lib.list() is deprecated. Use ([]) instead.') return List(list(vals)) @stormfunc(readonly=True) @@ -5177,7 +5180,7 @@ class List(Prim): Examples: Populate a list by extending it with to other lists:: - $list = $lib.list() + $list = () $foo = (f, o, o) $bar = (b, a, r) diff --git a/synapse/tests/test_axon.py b/synapse/tests/test_axon.py index 0da10dedba..17a8a2ed51 100644 --- a/synapse/tests/test_axon.py +++ b/synapse/tests/test_axon.py @@ -990,12 +990,12 @@ async def test_axon_wput(self): self.isinstance(resp.get('err'), tuple) q = f''' - $fields = $lib.list( - ({{'name':'file', 'sha256':$sha256, 'filename':'file'}}), - ({{'name':'zip_password', 'value':'test'}}), - ({{'name':'dict', 'value':({{'foo':'bar'}}) }}), - ({{'name':'bytes', 'value':$bytes}}) - ) + $fields = ([ + {{'name':'file', 'sha256':$sha256, 'filename':'file'}}, + {{'name':'zip_password', 'value':'test'}}, + {{'name':'dict', 'value':{{'foo':'bar'}} }}, + {{'name':'bytes', 'value':$bytes}} + ]) $resp = $lib.inet.http.post("https://127.0.0.1:{port}/api/v1/pushfile", fields=$fields, ssl_verify=(0)) return($resp) diff --git a/synapse/tests/test_cortex.py b/synapse/tests/test_cortex.py index ffc88daeb5..7d6b8c6108 100644 --- a/synapse/tests/test_cortex.py +++ b/synapse/tests/test_cortex.py @@ -440,7 +440,7 @@ async def test_cortex_stormiface(self): 'interfaces': ['lookup'], 'storm': ''' function lookup(tokens) { - $looks = $lib.list() + $looks = () for $token in $tokens { $looks.append( (inet:fqdn, $token) ) } return($looks) } @@ -679,7 +679,7 @@ async def test_cortex_divert(self): # functions that don't return a generator storm = ''' function x() { - $lst = $lib.list() + $lst = () [ ou:org=* ou:org=* +#cat ] $lst.append($node) fini { return($lst) } @@ -692,7 +692,7 @@ async def test_cortex_divert(self): storm = ''' function x() { - $lst = $lib.list() + $lst = () [ ou:org=* ou:org=* +#dog ] $lst.append($node) fini { return($lst) } @@ -974,7 +974,7 @@ async def test_cortex_edges(self): await core.nodes('media:news -(refs)> $(10)') self.eq(1, await core.callStorm(''' - $list = $lib.list() + $list = () for $edge in $lib.view.get().getEdges() { $list.append($edge) } return($list.size()) ''')) @@ -982,7 +982,7 @@ async def test_cortex_edges(self): # check that auto-deleting a node's edges works await core.nodes('media:news | delnode') self.eq(0, await core.callStorm(''' - $list = $lib.list() + $list = () for $edge in $lib.view.get().getEdges() { $list.append($edge) } return($list.size()) ''')) @@ -1017,7 +1017,7 @@ async def test_cortex_callstorm(self): self.eq(('1', '2'), retn) with self.raises(s_exc.StormRuntimeError): - q = '$foo=$lib.list() $bar=$foo.index(10) return ( $bar )' + q = '$foo=() $bar=$foo.index(10) return ( $bar )' await proxy.callStorm(q) with self.raises(s_exc.SynErr) as cm: @@ -1089,7 +1089,7 @@ async def test_cortex_callstorm(self): self.eq('ok', retn.get('status')) self.eq('asdf', retn['result']) - body = {'query': '$foo=$lib.list() $bar=$foo.index(10) return ( $bar )'} + body = {'query': '$foo=() $bar=$foo.index(10) return ( $bar )'} async with sess.get(f'https://localhost:{port}/api/v1/storm/call', json=body) as resp: retn = await resp.json() self.eq('err', retn.get('status')) @@ -1780,7 +1780,7 @@ async def test_tags(self): self.len(1, nodes) self.eq(set(nodes[0].tags.keys()), {'foo', 'bar', 'bar.baz'}) - nodes = await wcore.nodes('$foo=$lib.list("foo", "bar.baz") [test:int=4 +#$foo]') + nodes = await wcore.nodes('$foo=(["foo", "bar.baz"]) [test:int=4 +#$foo]') self.len(1, nodes) self.eq(set(nodes[0].tags.keys()), {'foo', 'bar', 'bar.baz'}) @@ -5795,7 +5795,7 @@ async def test_cortex_mirror_culled(self): url02 = core02.getLocalUrl() opts = {'vars': {'url01': url01, 'url02': url02}} - strim = 'return($lib.cell.trimNexsLog(consumers=$lib.list($url01, $url02), timeout=$lib.null))' + strim = 'return($lib.cell.trimNexsLog(consumers=($url01, $url02), timeout=$lib.null))' await core00.nodes('[ inet:ipv4=11.0.0.0/28 ]') ips00 = await core00.count('inet:ipv4') diff --git a/synapse/tests/test_lib_ast.py b/synapse/tests/test_lib_ast.py index d8092afdbf..a90ece527e 100644 --- a/synapse/tests/test_lib_ast.py +++ b/synapse/tests/test_lib_ast.py @@ -1157,10 +1157,10 @@ async def test_ast_lift_filt_array(self): nodes = await core.nodes('[ test:arrayprop="*" :ints=(1, 2, 3) ]') nodes = await core.nodes('[ test:arrayprop="*" :ints=(100, 101, 102) ]') - nodes = await core.nodes('test:arrayprop +:ints=$lib.list(1,2,3)') + nodes = await core.nodes('test:arrayprop +:ints=([1,2,3])') self.len(1, nodes) - nodes = await core.nodes('test:arrayprop:ints=$lib.list(1,2,3)') + nodes = await core.nodes('test:arrayprop:ints=([1,2,3])') self.len(1, nodes) with self.raises(s_exc.NoSuchProp): @@ -1319,7 +1319,7 @@ async def test_ast_subquery_value(self): nodes = await core.nodes(q) self.len(1, nodes) - nodes = await core.nodes('[ test:arrayprop=* :strs={return ($lib.list(a,b,c,d))} ]') + nodes = await core.nodes('[ test:arrayprop=* :strs={return ((a,b,c,d))} ]') self.len(1, nodes) self.len(4, nodes[0].get('strs')) @@ -1957,7 +1957,7 @@ async def test_function(self): self.len(0, await core.nodes('init { function x() { return((0)) } }')) # Can't use a mutable variable as a default - q = '$var=$lib.list(1,2,3) function badargs(x=foo, y=$var) {} $badargs()' + q = '$var=([1,2,3]) function badargs(x=foo, y=$var) {} $badargs()' msgs = await core.stormlist(q) erfo = [m for m in msgs if m[0] == 'err'][0] self.eq(erfo[1][0], 'StormRuntimeError') @@ -2740,7 +2740,7 @@ async def test_ast_storm_readonly(self): async def test_ast_yield(self): async with self.getTestCore() as core: - q = '$nodes = $lib.list() [ inet:asn=10 inet:asn=20 ] $nodes.append($node) | spin | yield $nodes' + q = '$nodes = () [ inet:asn=10 inet:asn=20 ] $nodes.append($node) | spin | yield $nodes' nodes = await core.nodes(q) self.len(2, nodes) diff --git a/synapse/tests/test_lib_layer.py b/synapse/tests/test_lib_layer.py index 9564900704..412f4cf63b 100644 --- a/synapse/tests/test_lib_layer.py +++ b/synapse/tests/test_lib_layer.py @@ -54,7 +54,7 @@ async def test_layer_verify(self): self.eq(errors[1][0], 'NoPropIndex') errors = await core.callStorm(''' - $retn = $lib.list() + $retn = () for $mesg in $lib.layer.get().verify() { $retn.append($mesg) } diff --git a/synapse/tests/test_lib_storm.py b/synapse/tests/test_lib_storm.py index fbb05f9dbe..4a9584e568 100644 --- a/synapse/tests/test_lib_storm.py +++ b/synapse/tests/test_lib_storm.py @@ -254,7 +254,7 @@ async def test_lib_storm_emit(self): emit bar } function makelist() { - $retn = $lib.list() + $retn = () for $item in $generate() { $retn.append($item) } return($retn) } @@ -267,7 +267,7 @@ async def test_lib_storm_emit(self): emit $node.repr() } function makelist() { - $retn = $lib.list() + $retn = () for $item in $generate() { $retn.append($item) } return($retn) } @@ -706,7 +706,7 @@ async def test_storm_ifcond_fix(self): return((0)) } - $alerts = $lib.list() + $alerts = () { $alerts.append($node.repr()) } $bool = $stuff($alerts) @@ -1035,7 +1035,7 @@ async def test_lib_storm_basics(self): opts = {'view': view} self.len(0, await core.callStorm(''' - $list = $lib.list() + $list = () $layr = $lib.view.get().layers.0 for $item in $layr.getStorNodes() { $list.append($item) @@ -1048,7 +1048,7 @@ async def test_lib_storm_basics(self): await core.callStorm('inet:ipv4=11.22.33.44 [ +(blahverb)> { inet:asn=99 } ]', opts=opts) sodes = await core.callStorm(''' - $list = $lib.list() + $list = () $layr = $lib.view.get().layers.0 for $item in $layr.getStorNodes() { $list.append($item) @@ -1057,7 +1057,7 @@ async def test_lib_storm_basics(self): self.len(2, sodes) ipv4 = await core.callStorm(''' - $list = $lib.list() + $list = () $layr = $lib.view.get().layers.0 for ($buid, $sode) in $layr.getStorNodes() { yield $buid @@ -1199,7 +1199,7 @@ async def test_lib_storm_basics(self): self.len(0, await core.nodes('diff', opts=opts)) self.len(0, await core.callStorm(''' - $list = $lib.list() + $list = () for ($buid, $sode) in $lib.view.get().layers.0.getStorNodes() { $list.append($buid) } @@ -1480,9 +1480,9 @@ async def get(self, name): ''')) self.eq(('foo', 'bar', 'baz'), await core.callStorm(''' - return($lib.list( // do foo thing - /* hehe */ foo /* hehe */ , /* hehe */ bar /* hehe */ , /* hehe */ baz /* hehe */ - )) + return(([ // do foo thing + /* hehe */ "foo" /* hehe */ , /* hehe */ "bar" /* hehe */ , /* hehe */ "baz" /* hehe */ + ])) ''')) # surrogate escapes are allowed @@ -3380,7 +3380,7 @@ async def test_storm_tee(self): self.eq(nodes[4].ndef, ('inet:ipv4', 0x01020304)) # Queries can be a heavy list - q = '$list = $lib.list(${ -> * }, ${ <- * }, ${ -> edge:refs:n2 :n1 -> * }) inet:ipv4=1.2.3.4 | tee --join $list' + q = '$list = ([${ -> * }, ${ <- * }, ${ -> edge:refs:n2 :n1 -> * }]) inet:ipv4=1.2.3.4 | tee --join $list' nodes = await core.nodes(q) self.len(5, nodes) self.eq(nodes[0].ndef, ('inet:asn', 0)) @@ -3390,22 +3390,22 @@ async def test_storm_tee(self): self.eq(nodes[4].ndef, ('inet:ipv4', 0x01020304)) # A empty list of queries still works as an nop - q = '$list = $lib.list() | tee $list' + q = '$list = () | tee $list' msgs = await core.stormlist(q) self.len(2, msgs) self.eq(('init', 'fini'), [m[0] for m in msgs]) - q = 'inet:ipv4=1.2.3.4 $list = $lib.list() | tee --join $list' + q = 'inet:ipv4=1.2.3.4 $list = () | tee --join $list' msgs = await core.stormlist(q) self.len(3, msgs) self.eq(('init', 'node', 'fini'), [m[0] for m in msgs]) - q = '$list = $lib.list() | tee --parallel $list' + q = '$list = () | tee --parallel $list' msgs = await core.stormlist(q) self.len(2, msgs) self.eq(('init', 'fini'), [m[0] for m in msgs]) - q = 'inet:ipv4=1.2.3.4 $list = $lib.list() | tee --parallel --join $list' + q = 'inet:ipv4=1.2.3.4 $list = () | tee --parallel --join $list' msgs = await core.stormlist(q) self.len(3, msgs) self.eq(('init', 'node', 'fini'), [m[0] for m in msgs]) @@ -3659,7 +3659,7 @@ async def agenr(): fork = await core.callStorm('return( $lib.view.get().fork().iden )') q = ''' - $nodes = $lib.list() + $nodes = () view.exec $view { inet:ipv4=1.2.3.4 $nodes.append($node) } | for $n in $nodes { yield $n @@ -3669,7 +3669,7 @@ async def agenr(): self.stormIsInErr('Node is not from the current view.', msgs) q = ''' - $nodes = $lib.list() + $nodes = () view.exec $view { for $x in ${ inet:ipv4=1.2.3.4 } { $nodes.append($x) } } | for $n in $nodes { yield $n @@ -3684,7 +3684,7 @@ async def agenr(): # Nodes lifted from another view and referred to by iden() works q = ''' - $nodes = $lib.list() + $nodes = () view.exec $view { inet:ipv4=1.2.3.4 $nodes.append($node) } | for $n in $nodes { yield $n.iden() @@ -3694,7 +3694,7 @@ async def agenr(): self.len(1, nodes) q = ''' - $nodes = $lib.list() + $nodes = () view.exec $view { for $x in ${ inet:ipv4=1.2.3.4 } { $nodes.append($x) } } | for $n in $nodes { yield $n.iden() diff --git a/synapse/tests/test_lib_stormhttp.py b/synapse/tests/test_lib_stormhttp.py index 1fd1fa48e1..ac22b0ac5c 100644 --- a/synapse/tests/test_lib_stormhttp.py +++ b/synapse/tests/test_lib_stormhttp.py @@ -518,11 +518,11 @@ async def test_storm_http_post(self): self.eq(data.get('body'), 'MTIzNA==') q = ''' - $fields=$lib.list( - ({"name": "foo", "value": "bar"}), - ({"name": "foo", "value": "bar2"}), - ({"name": "baz", "value": "cool"}) - ) + $fields=([ + {"name": "foo", "value": "bar"}, + {"name": "foo", "value": "bar2"}, + {"name": "baz", "value": "cool"} + ]) $resp = $lib.inet.http.post($url, fields=$fields, ssl_verify=$lib.false) return ( $resp.json() ) ''' diff --git a/synapse/tests/test_lib_stormlib_auth.py b/synapse/tests/test_lib_stormlib_auth.py index 40d71d4e87..9a4bfbb7d9 100644 --- a/synapse/tests/test_lib_stormlib_auth.py +++ b/synapse/tests/test_lib_stormlib_auth.py @@ -427,7 +427,7 @@ async def test_stormlib_auth_userjson(self): await core.callStorm('$lib.user.json.set(hi, hehe, prop=foo)') items = await core.callStorm(''' - $list = $lib.list() + $list = () for $item in $lib.user.json.iter() { $list.append($item) } return($list) ''') @@ -437,7 +437,7 @@ async def test_stormlib_auth_userjson(self): )) items = await core.callStorm(''' - $list = $lib.list() + $list = () for $item in $lib.user.json.iter(path=bye) { $list.append($item) } return($list) ''') @@ -731,7 +731,7 @@ async def test_stormlib_auth_base(self): ''')) # user roles can be set in bulk - roles = await core.callStorm('''$roles=$lib.list() + roles = await core.callStorm('''$roles=() $role=$lib.auth.roles.byname(admins) $roles.append($role.iden) $role=$lib.auth.roles.byname(all) $roles.append($role.iden) $lib.auth.users.byname(visi).setRoles($roles) @@ -791,7 +791,7 @@ async def test_stormlib_auth_base(self): visi = await core.callStorm(''' $rule = $lib.auth.ruleFromText(hehe.haha) $visi = $lib.auth.users.byname(visi) - $visi.setRules($lib.list($rule)) + $visi.setRules(([$rule])) return($visi) ''') self.eq(((True, ('hehe', 'haha')),), visi['rules']) @@ -834,7 +834,7 @@ async def test_stormlib_auth_base(self): ninjas = await core.callStorm(''' $rule = $lib.auth.ruleFromText(hehe.haha) $ninjas = $lib.auth.roles.byname(ninjas) - $ninjas.setRules($lib.list($rule)) + $ninjas.setRules(([$rule])) return($ninjas) ''') self.eq(((True, ('hehe', 'haha')),), ninjas['rules']) diff --git a/synapse/tests/test_lib_stormlib_modelext.py b/synapse/tests/test_lib_stormlib_modelext.py index 85f8b619d4..26862f0503 100644 --- a/synapse/tests/test_lib_stormlib_modelext.py +++ b/synapse/tests/test_lib_stormlib_modelext.py @@ -122,7 +122,7 @@ async def test_lib_stormlib_modelext_base(self): self.none(core.model.edge(('inet:user', '_copies', None))) # Underscores can exist in extended names but only at specific locations - q = '''$l =$lib.list('str', ({})) $d=({"doc": "Foo"}) + q = '''$l =(['str', {}]) $d=({"doc": "Foo"}) $lib.model.ext.addFormProp('test:str', '_test:_myprop', $l, $d) ''' self.none(await core.callStorm(q)) @@ -144,13 +144,13 @@ async def test_lib_stormlib_modelext_base(self): await core.callStorm(q) with self.raises(s_exc.BadPropDef): - q = '''$l =$lib.list('str', ({})) $d=({"doc": "Foo"}) + q = '''$l =(['str', {}]) $d=({"doc": "Foo"}) $lib.model.ext.addFormProp('test:str', '_test:_my^prop', $l, $d) ''' await core.callStorm(q) with self.raises(s_exc.BadPropDef): - q = '''$l =$lib.list('str', ({})) $d=({"doc": "Foo"}) + q = '''$l =(['str', {}]) $d=({"doc": "Foo"}) $lib.model.ext.addFormProp('test:str', '_test::_myprop', $l, $d) ''' await core.callStorm(q) diff --git a/synapse/tests/test_lib_stormlib_scrape.py b/synapse/tests/test_lib_stormlib_scrape.py index b41f758dbf..1b71d215c6 100644 --- a/synapse/tests/test_lib_stormlib_scrape.py +++ b/synapse/tests/test_lib_stormlib_scrape.py @@ -24,7 +24,7 @@ async def test_storm_lib_scrape_iface(self): The helper does require a named match for valu this is extracted. */ function scrape(text) { - $ret = $lib.list() + $ret = () for ($valu, $info) in $lib.scrape.genMatches($text, $modRe) { $ret.append(($modForm, $valu, $info)) } @@ -49,7 +49,7 @@ async def test_storm_lib_scrape_iface(self): Example of an storm module that scraps and matches on hzzp enfanged urls. */ function scrape(text) { - $ret = $lib.list() + $ret = () for ($valu, $info) in $lib.scrape.genMatches($text, $modRe, fangs=$modFangs) { $ret.append(($modForm, $valu, $info)) } @@ -105,7 +105,7 @@ async def test_storm_lib_scrape_iface(self): self.stormIsInPrint('inet:url=https://giggles.com/mallory.html', msgs) self.stormIsInPrint("'match': 'hzzps[:]\\\\giggles.com/mallory.html'", msgs) - cq = '''$ret=$lib.list() + cq = '''$ret=() for ($form, $valu) in $lib.scrape.ndefs($text) { $ret.append(($form, $valu)) } @@ -180,7 +180,7 @@ async def test_storm_lib_scrape(self): self.eq(result, {'inet:ipv4=16909060': 1, 'inet:fqdn=foo.bar': 1, 'inet:fqdn=woot.com': 1}) # $lib.scrape.context() - this is currently just wrapping s_scrape.contextscrape - query = '''$list = $lib.list() for $info in $lib.scrape.context($text) + query = '''$list = () for $info in $lib.scrape.context($text) { $list.append($info) } fini { return ( $list ) } ''' diff --git a/synapse/tests/test_lib_stormlib_spooled.py b/synapse/tests/test_lib_stormlib_spooled.py index b3cf4f3c31..35fadfc664 100644 --- a/synapse/tests/test_lib_stormlib_spooled.py +++ b/synapse/tests/test_lib_stormlib_spooled.py @@ -19,7 +19,7 @@ async def test_lib_spooled_set(self): q = ''' $set = $lib.spooled.set() - $set.adds($lib.list(1, 2, 3, 4)) + $set.adds((1, 2, 3, 4)) return($set) ''' valu = await core.callStorm(q) diff --git a/synapse/tests/test_lib_stormlib_xml.py b/synapse/tests/test_lib_stormlib_xml.py index 838cb031c6..580da20fe0 100644 --- a/synapse/tests/test_lib_stormlib_xml.py +++ b/synapse/tests/test_lib_stormlib_xml.py @@ -38,7 +38,7 @@ async def test_stormlib_xml(self): async with self.getTestCore() as core: valu = await core.callStorm(''' - $retn = $lib.list() + $retn = () $root = $lib.xml.parse($xmltext) for $elem in $root { $retn.append(( @@ -56,7 +56,7 @@ async def test_stormlib_xml(self): )) valu = await core.callStorm(''' - $retn = $lib.list() + $retn = () $root = $lib.xml.parse($xmltext) for $elem in $root.find(country) { $retn.append(( @@ -74,7 +74,7 @@ async def test_stormlib_xml(self): )) valu = await core.callStorm(''' - $retn = $lib.list() + $retn = () $root = $lib.xml.parse($xmltext) for $elem in $root.find(rank) { $retn.append($elem.text) @@ -84,7 +84,7 @@ async def test_stormlib_xml(self): self.eq(valu, ('1', '4', '68')) valu = await core.callStorm(''' - $retn = $lib.list() + $retn = () $root = $lib.xml.parse($xmltext) for $elem in $root.find(rank, nested=$lib.false) { $retn.append($elem.text) @@ -94,7 +94,7 @@ async def test_stormlib_xml(self): self.eq(valu, ()) valu = await core.callStorm(''' - $retn = $lib.list() + $retn = () $root = $lib.xml.parse($xmltext) for $elem in $root.find(rank, nested=$lib.false) { $retn.append($elem.text) diff --git a/synapse/tests/test_lib_stormtypes.py b/synapse/tests/test_lib_stormtypes.py index de87a802bd..686a4c75ad 100644 --- a/synapse/tests/test_lib_stormtypes.py +++ b/synapse/tests/test_lib_stormtypes.py @@ -196,7 +196,7 @@ async def test_stormtypes_jsonstor(self): await core.callStorm('$lib.jsonstor.set(hi, hehe, prop=foo)') items = await core.callStorm(''' - $list = $lib.list() + $list = () for $item in $lib.jsonstor.iter(bye) { $list.append($item) } return($list) ''') @@ -573,9 +573,9 @@ async def test_storm_lib_base(self): self.eq(2, await core.callStorm('$x = asdf return($x.find(d))')) self.eq(None, await core.callStorm('$x = asdf return($x.find(v))')) - self.eq(('f', 'o', 'o'), await core.callStorm('$x = $lib.list() $x.extend((f, o, o)) return($x)')) - self.eq(('o', 'o', 'b', 'a'), await core.callStorm('$x = $lib.list(f, o, o, b, a, r) return($x.slice(1, 5))')) - self.eq(('o', 'o', 'b', 'a', 'r'), await core.callStorm('$x = $lib.list(f, o, o, b, a, r) return($x.slice(1))')) + self.eq(('f', 'o', 'o'), await core.callStorm('$x = () $x.extend((f, o, o)) return($x)')) + self.eq(('o', 'o', 'b', 'a'), await core.callStorm('$x = (f, o, o, b, a, r) return($x.slice(1, 5))')) + self.eq(('o', 'o', 'b', 'a', 'r'), await core.callStorm('$x = (f, o, o, b, a, r) return($x.slice(1))')) self.true(await core.callStorm('return($lib.trycast(inet:ipv4, 1.2.3.4).0)')) self.false(await core.callStorm('return($lib.trycast(inet:ipv4, asdf).0)')) @@ -595,11 +595,11 @@ async def test_storm_lib_base(self): self.false(await core.callStorm('$x=(foo,bar) return($x.has((foo,bar)))')) await core.addStormPkg(pdef) - nodes = await core.nodes('[ inet:asn=$lib.min(20, $lib.list(0x30)) ]') + nodes = await core.nodes('[ inet:asn=$lib.min(20, (0x30)) ]') self.len(1, nodes) self.eq(20, nodes[0].ndef[1]) - nodes = await core.nodes('[ inet:asn=$lib.min(20, $lib.list(10, 30)) ]') + nodes = await core.nodes('[ inet:asn=$lib.min(20, (10, 30)) ]') self.len(1, nodes) self.eq(10, nodes[0].ndef[1]) @@ -744,7 +744,7 @@ async def test_storm_lib_base(self): await core.nodes('$lib.print($lib.len($true))', opts=opts) self.eq(cm.exception.get('mesg'), 'Object builtins.bool does not have a length.') - mesgs = await core.stormlist('$lib.print($lib.list(1,(2),3))') + mesgs = await core.stormlist('$lib.print((1,(2),3))') self.stormIsInPrint("['1', 2, '3']", mesgs) mesgs = await core.stormlist('$lib.print(${ $foo=bar })') @@ -768,7 +768,7 @@ async def test_storm_lib_base(self): mesgs = await core.stormlist('$lib.print($lib.queue.add(testq))') self.stormIsInPrint("queue: testq", mesgs) - mesgs = await core.stormlist('$lib.pprint($lib.list(1,2,3))') + mesgs = await core.stormlist('$lib.pprint((1,2,3))') self.stormIsInPrint("('1', '2', '3')", mesgs) mesgs = await core.stormlist('$lib.pprint(({"foo": "1", "bar": "2"}))') @@ -788,7 +788,7 @@ async def test_storm_lib_base(self): # lib.guid() opts = {'vars': {'x': {'foo': 'bar'}, 'y': ['foo']}} guid00 = await core.callStorm('return($lib.guid($x, $y))', opts=opts) - guid01 = await core.callStorm('$x=({"foo": "bar"}) $y=$lib.list(foo) return($lib.guid($x, $y))') + guid01 = await core.callStorm('$x=({"foo": "bar"}) $y=(foo,) return($lib.guid($x, $y))') self.eq(guid00, guid01) guid00 = await core.callStorm('return($lib.guid(foo))') @@ -1519,8 +1519,8 @@ async def test_storm_lib_bytes_json(self): async def test_storm_lib_list(self): async with self.getTestCore() as core: # Base List object behavior - q = '''// $lib.list ctor - $list=$lib.list(1,2,3) + q = ''' + $list=(1,2,3) // __len__ $lib.print('List size is {len}', len=$lib.len($list)) // aiter/iter method @@ -1541,7 +1541,7 @@ async def test_storm_lib_list(self): } $lib.print('Sum is now {sum}', sum=$sum) // Empty lists may also be made - $elst=$lib.list() + $elst=() $lib.print('elst size is {len}', len=$lib.len($elst)) ''' msgs = await core.stormlist(q) @@ -1672,7 +1672,7 @@ async def test_storm_lib_list(self): self.eq([1, 3, 4], await core.callStorm('$list = ([1, 2, 3, 4]) $list.pop(1) return($list)')) with self.raises(s_exc.StormRuntimeError) as exc: - await core.callStorm('$lib.list().pop()') + await core.callStorm('$foo=() $foo.pop()') self.eq(exc.exception.get('mesg'), 'pop from empty list') with self.raises(s_exc.StormRuntimeError) as exc: @@ -1702,6 +1702,9 @@ async def test_storm_lib_list(self): out = await core.callStorm(q, opts=opts) self.eq(out, ["foo", "baz"]) + msgs = await core.stormlist('$list = $lib.list(foo, bar)') + self.stormIsInWarn('$lib.list() is deprecated. Use ([]) instead.', msgs) + async def test_storm_layer_getstornode(self): async with self.getTestCore() as core: @@ -2139,14 +2142,14 @@ def __eq__(self, othr): # List q = ''' - $list = $lib.list(1, 2, 3) + $list = (1, 2, 3) $set = $lib.set($list) ''' msgs = await core.stormlist(q) self.stormIsInErr('is mutable and cannot be used in a set', msgs) q = ''' - $list = $lib.list(1, 2, 3, 1, 2, 3, 1, 2, 3) + $list = (1, 2, 3, 1, 2, 3, 1, 2, 3) $set = $lib.set() $set.adds($list) $lib.print('There are {count} items in the set', count=$lib.len($set)) @@ -2155,7 +2158,7 @@ def __eq__(self, othr): self.stormIsInPrint('There are 3 items in the set', msgs) q = ''' - $list = $lib.list($lib.list(4, 5, 6, 7), $lib.list(1, 2, 3, 4)) + $list = ((4, 5, 6, 7), (1, 2, 3, 4)) $set = $lib.set() $set.adds($list) $lib.print('There are {count} items in the set', count=$lib.len($set)) @@ -2327,7 +2330,7 @@ async def test_storm_path(self): q = ''' inet:fqdn=vertex.link - $path.meta.foobar = $lib.list('neato', 'burrito') + $path.meta.foobar = ('neato', 'burrito') ''' msgs = [mesg async for mesg in proxy.storm(q)] pode = [m[1] for m in msgs if m[0] == 'node'][0] @@ -2350,7 +2353,7 @@ async def test_storm_path(self): q = ''' inet:fqdn=vertex.link - $path.meta.$node = $lib.list('foo', 'bar') + $path.meta.$node = ('foo', 'bar') ''' msgs = [mesg async for mesg in proxy.storm(q)] pode = [m[1] for m in msgs if m[0] == 'node'][0] @@ -3840,7 +3843,7 @@ async def test_storm_lib_view(self): # Get the main view mainiden = await core.callStorm('return($lib.view.get().iden)') altview = await core.callStorm(''' - $layers = $lib.list() + $layers = () for $layer in $lib.view.get().layers { $layers.append($layer.iden) } @@ -3885,7 +3888,7 @@ async def test_storm_lib_view(self): # List the views in the cortex q = ''' - $views = $lib.list() + $views = () for $view in $lib.view.list() { $views.append($view.iden) } @@ -4310,7 +4313,7 @@ async def test_storm_view_deporder(self): view2['iden'], ) self.eq(expect, await core.callStorm(''' - $views = $lib.list() + $views = () for $view in $lib.view.list(deporder=$lib.true) { $views.append($view.iden) } @@ -5445,7 +5448,7 @@ async def test_stormtypes_toprim(self): self.eq(valu['none'], None) self.eq(valu['bool'], True) - q = '$list = $lib.list() $list.append(foo) $list.append(bar) return($list)' + q = '$list = () $list.append(foo) $list.append(bar) return($list)' self.eq(('foo', 'bar'), await core.callStorm(q)) self.eq({'foo': 'bar'}, await core.callStorm('$dict = ({}) $dict.foo = bar return($dict)')) q = '$tally = $lib.stats.tally() $tally.inc(foo) $tally.inc(foo) return($tally)' @@ -5541,7 +5544,7 @@ async def test_stormtypes_layer_edits(self): await core.nodes('[inet:ipv4=1.2.3.4]') # TODO: should we asciify the buid here so it is json compatible? - q = '''$list = $lib.list() + q = '''$list = () for ($offs, $edit) in $lib.layer.get().edits(wait=$lib.false) { $list.append($edit) } @@ -6003,7 +6006,7 @@ async def test_iter(self): self.len(2, nodes) # set adds - ret = await core.callStorm('$x=$lib.set() $y=$lib.list(1,2,3) $x.adds($y) return($x)') + ret = await core.callStorm('$x=$lib.set() $y=(1,2,3) $x.adds($y) return($x)') self.eq({'1', '2', '3'}, ret) ret = await core.callStorm('$x=$lib.set() $y=({"foo": "1", "bar": "2"}) $x.adds($y) return($x)') @@ -6019,7 +6022,7 @@ async def test_iter(self): self.eq({'a', 'b', 'c', 'd'}, ret) # set rems - ret = await core.callStorm('$x=$lib.set(1,2,3) $y=$lib.list(1,2) $x.rems($y) return($x)') + ret = await core.callStorm('$x=$lib.set(1,2,3) $y=(1,2) $x.rems($y) return($x)') self.eq({'3'}, ret) scmd = ''' @@ -6043,7 +6046,7 @@ async def test_iter(self): self.eq({'d', 'c'}, ret) # str join - ret = await core.callStorm('$x=$lib.list(foo,bar,baz) $y=$lib.str.join("-", $x) return($y)') + ret = await core.callStorm('$x=(foo,bar,baz) $y=$lib.str.join("-", $x) return($y)') self.eq('foo-bar-baz', ret) ret = await core.callStorm('$y=$lib.str.join("-", (foo, bar, baz)) return($y)') @@ -6093,7 +6096,7 @@ async def test_storm_lib_axon(self): self.eq(nodes[0].ndef[0], 'file:bytes') sha256, size, created = nodes[0].get('sha256'), nodes[0].get('size'), nodes[0].get('.created') - items = await core.callStorm('$x=$lib.list() for $i in $lib.axon.list() { $x.append($i) } return($x)') + items = await core.callStorm('$x=() for $i in $lib.axon.list() { $x.append($i) } return($x)') self.eq([(0, sha256, size)], items) # test $lib.axon.del() @@ -6108,21 +6111,21 @@ async def test_storm_lib_axon(self): self.eq((True, False), await core.callStorm('return($lib.axon.dels(($sha256, $sha256)))', opts=delopts)) self.false(await core.callStorm('return($lib.axon.del($sha256))', opts=delopts)) - items = await core.callStorm('$x=$lib.list() for $i in $lib.axon.list() { $x.append($i) } return($x)') + items = await core.callStorm('$x=() for $i in $lib.axon.list() { $x.append($i) } return($x)') self.len(0, items) msgs = await core.stormlist(f'wget --no-ssl-verify https://127.0.0.1:{port}/api/v1/newp') self.stormIsInWarn('HTTP code 404', msgs) - self.len(1, await core.callStorm('$x=$lib.list() for $i in $lib.axon.list() { $x.append($i) } return($x)')) + self.len(1, await core.callStorm('$x=() for $i in $lib.axon.list() { $x.append($i) } return($x)')) size, sha256 = await core.callStorm('return($lib.axon.put($buf))', opts={'vars': {'buf': b'foo'}}) - items = await core.callStorm('$x=$lib.list() for $i in $lib.axon.list() { $x.append($i) } return($x)') + items = await core.callStorm('$x=() for $i in $lib.axon.list() { $x.append($i) } return($x)') self.len(2, items) self.eq((2, sha256, size), items[1]) - items = await core.callStorm('$x=$lib.list() for $i in $lib.axon.list(2) { $x.append($i) } return($x)') + items = await core.callStorm('$x=() for $i in $lib.axon.list(2) { $x.append($i) } return($x)') self.eq([(2, sha256, size)], items) # test request timeout @@ -6144,28 +6147,28 @@ async def timeout(self): opts = {'vars': {'sha256': asdfitem[1]}} self.eq(('asdf',), await core.callStorm(''' - $items = $lib.list() + $items = () for $item in $lib.axon.readlines($sha256) { $items.append($item) } return($items) ''', opts=opts)) opts = {'vars': {'sha256': linesitem[1]}} self.eq(('vertex.link', 'woot.com'), await core.callStorm(''' - $items = $lib.list() + $items = () for $item in $lib.axon.readlines($sha256) { $items.append($item) } return($items) ''', opts=opts)) opts = {'vars': {'sha256': jsonsitem[1]}} self.eq(({'fqdn': 'vertex.link'}, {'fqdn': 'woot.com'}), await core.callStorm(''' - $items = $lib.list() + $items = () for $item in $lib.axon.jsonlines($sha256) { $items.append($item) } return($items) ''', opts=opts)) async def waitlist(): items = await core.callStorm(''' - $x=$lib.list() + $x=() for $i in $lib.axon.list(2, wait=$lib.true, timeout=1) { $x.append($i) } @@ -6235,7 +6238,7 @@ async def waitlist(): opts = {'vars': {'sha256': s_common.ehex(bin256)}} with self.raises(s_exc.BadDataValu): self.eq('', await core.callStorm(''' - $items = $lib.list() + $items = () for $item in $lib.axon.readlines($sha256, errors=$lib.null) { $items.append($item) } @@ -6243,7 +6246,7 @@ async def waitlist(): ''', opts=opts)) self.eq(('/$A\x00_v4\x1b',), await core.callStorm(''' - $items = $lib.list() + $items = () for $item in $lib.axon.readlines($sha256, errors=ignore) { $items.append($item) } return($items) ''', opts=opts)) diff --git a/synapse/tests/test_lib_view.py b/synapse/tests/test_lib_view.py index 15fcd1bf12..a35ff78231 100644 --- a/synapse/tests/test_lib_view.py +++ b/synapse/tests/test_lib_view.py @@ -547,7 +547,7 @@ async def test_lib_view_storNodeEdits(self): await core.nodes('inet:ipv4=0 | delnode') edits = await core.callStorm(''' - $nodeedits = $lib.list() + $nodeedits = () for ($offs, $edits) in $lib.layer.get().edits(wait=$lib.false) { $nodeedits.extend($edits) } diff --git a/synapse/tools/storm.py b/synapse/tools/storm.py index 233cffcd74..b91501c1f0 100644 --- a/synapse/tools/storm.py +++ b/synapse/tools/storm.py @@ -345,7 +345,7 @@ async def _get_tag_completions(self, prefix='', limit=100): depth = prefix.count('.') + 1 q = ''' - $rslt = $lib.list() + $rslt = () if ($prefix != '') { syn:tag=$lib.regex.replace("\\.$", '', $prefix) } syn:tag^=$prefix +:depth<=$depth From 2b4422c05e0f8e6ff661a5fa5a26aaa5ffb9b204 Mon Sep 17 00:00:00 2001 From: blackout Date: Wed, 15 Jan 2025 14:14:52 -0500 Subject: [PATCH 07/19] SYN-8479: synapse to use xdist for test splitting (#4069) --- .circleci/config.yml | 40 ++++++++++--------------- conftest.py | 28 +++++++++++++++++ synapse/tests/test_lib_aha.py | 19 ++++++++---- synapse/tests/test_tools_healthcheck.py | 8 ++--- 4 files changed, 61 insertions(+), 34 deletions(-) create mode 100644 conftest.py diff --git a/.circleci/config.yml b/.circleci/config.yml index 053969af59..bb551aa5cc 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -59,18 +59,10 @@ commands: command: | . venv/bin/activate mkdir test-reports - circleci tests glob synapse/tests/test_*.py synapse/vendor/**/test_*.py | circleci tests run --split-by=timings --command "xargs python3 -m pytest -v -s -rs --durations 6 --maxfail 6 -p no:logging --junitxml=test-reports/junit.xml -o junit_family=xunit1 ${COVERAGE_ARGS}" - - check_runner_has_work: - description: Check that this runner has work to do - steps: - - run: - name: Check workload - command: | - mkdir -p ./tmp && \ - >./tmp/tests.txt && \ - circleci tests glob synapse/tests/test_*.py synapse/vendor/**/test_*.py | circleci tests run --split-by=timings --command ">./tmp/tests.txt xargs echo" - [ -s tmp/tests.txt ] || circleci-agent step halt #if there are no tests, terminate execution after this step + circleci tests glob synapse/tests/test_*.py synapse/vendor/**/test_*.py | \ + circleci tests run \ + --timings-type=name \ + --command="xargs python3 -m pytest -n 8 --dist worksteal -v -rs --durations 6 -p no:logging --junitxml=test-reports/junit.xml -o junit_family=xunit1 ${COVERAGE_ARGS}" test_steps_doc: description: "Documentation test steps" @@ -121,7 +113,15 @@ commands: steps: - checkout - - check_runner_has_work + - run: + # Run this first so we fail on syntax errors before installing a bunch + # of stuff and doing a bunch of work. It's easier now that we're only + # a single runner using xdist. + name: syntax + command: | + pip install "pycodestyle>=2.10.0,<3.0.0" + if [ -n "${RUN_SYNTAX}" ]; then pycodestyle synapse; fi; + if [ -n "${RUN_SYNTAX}" ]; then pycodestyle scripts; fi; - run: name: checkout regression repo @@ -144,15 +144,8 @@ commands: - ./venv key: v5-venv-{{ .Environment.CIRCLE_JOB }}-{{ .Branch }}-{{ checksum "pyproject.toml" }}-{{ checksum "/tmp/python.version" }} - - run: - name: syntax - command: | - . venv/bin/activate - if [ -n "${RUN_SYNTAX}" ]; then pycodestyle synapse; fi; - if [ -n "${RUN_SYNTAX}" ]; then pycodestyle scripts; fi; - - - do_test_execution + - do_report_coverage - store_test_results: @@ -439,7 +432,7 @@ commands: jobs: python311: - parallelism: 8 + resource_class: xlarge docker: - image: cimg/python:3.11 environment: @@ -456,7 +449,7 @@ jobs: - test_steps_python python311_replay: - parallelism: 6 + resource_class: xlarge docker: - image: cimg/python:3.11 environment: @@ -473,7 +466,6 @@ jobs: - test_steps_python doctests: - parallelism: 1 docker: - image: cimg/python:3.11 environment: diff --git a/conftest.py b/conftest.py new file mode 100644 index 0000000000..203ae901c1 --- /dev/null +++ b/conftest.py @@ -0,0 +1,28 @@ +import os +import sys +import warnings + +import synapse.common as s_common + +THROW = False + +def audithook(event, args): + if event == 'socket.bind': + _, addr = args + if isinstance(addr, tuple) and (port := addr[1]) != 0: + + testname = os.environ.get('PYTEST_CURRENT_TEST', '').split(' ')[0] + + mesg = f'Synapse tests should not bind to fixed ports: {testname=} {port=}' + warnings.warn(mesg) + + if THROW: + raise RuntimeError(mesg) + +def pytest_sessionstart(session): + if s_common.envbool('SYNDEV_AUDIT_PORT_BINDS'): + sys.addaudithook(audithook) + + if s_common.envbool('SYNDEV_AUDIT_PORT_BINDS_RAISE'): + global THROW + THROW = True diff --git a/synapse/tests/test_lib_aha.py b/synapse/tests/test_lib_aha.py index cd149601da..9f26a6314f 100644 --- a/synapse/tests/test_lib_aha.py +++ b/synapse/tests/test_lib_aha.py @@ -59,7 +59,7 @@ async def test_lib_aha_clone(self): self.len(ahacount, await proxy0.getAhaUrls()) self.len(ahacount, await proxy0.getAhaServers()) - purl = await proxy0.addAhaClone(zoinks) + purl = await proxy0.addAhaClone(zoinks, port=0) conf1 = {'clone': purl} async with self.getTestAha(conf=conf1, dirn=dir1) as aha1: @@ -574,7 +574,7 @@ async def test_lib_aha_provision(self): } s_common.yamlsave(axonconf, axonpath, 'cell.yaml') - argv = (axonpath, '--auth-passwd', 'rootbeer') + argv = (axonpath, '--auth-passwd', 'rootbeer', '--https', '0') async with await s_axon.Axon.initFromArgv(argv) as axon: # opts were copied through successfully @@ -1161,7 +1161,8 @@ async def test_aha_provision_longname(self): aconf = { 'aha:name': 'aha', 'aha:network': networkname, - 'provision:listen': f'ssl://aha.{networkname}:0' + 'dmon:listen': f'ssl://aha.{networkname}:0', + 'provision:listen': f'ssl://aha.{networkname}:0', } name = aconf.get('aha:name') netw = aconf.get('aha:network') @@ -1300,11 +1301,17 @@ async def test_aha_provision_listen_dns_name(self): conf = { 'aha:network': 'synapse', 'dns:name': 'here.loop.vertex.link', + 'dmon:listen': 'ssl://0.0.0.0:0?hostname=here.loop.vertex.link&ca=synapse', } - mesg = 'provision listening: ssl://0.0.0.0:27272?hostname=here.loop.vertex.link' - with self.getAsyncLoggerStream('synapse.lib.aha', mesg) as stream: + + orig = s_aha.AhaCell._getProvListen + def _getProvListen(_self): + ret = orig(_self) + self.eq(ret, 'ssl://0.0.0.0:27272?hostname=here.loop.vertex.link') + return 'ssl://0.0.0.0:0?hostname=here.loop.vertex.link' + + with mock.patch('synapse.lib.aha.AhaCell._getProvListen', _getProvListen): async with self.getTestCell(s_aha.AhaCell, conf=conf) as aha: - self.true(await stream.wait(timeout=6)) # And the URL works with our listener :) provurl = await aha.addAhaUserEnroll('bob.grey') async with await s_telepath.openurl(provurl) as prox: diff --git a/synapse/tests/test_tools_healthcheck.py b/synapse/tests/test_tools_healthcheck.py index a4bcbf964c..ec6d80d748 100644 --- a/synapse/tests/test_tools_healthcheck.py +++ b/synapse/tests/test_tools_healthcheck.py @@ -45,7 +45,7 @@ async def sleep(*args, **kwargs): await asyncio.sleep(0.6) core.addHealthFunc(sleep) outp.clear() - retn = await s_t_healthcheck.main(['-c', curl, '-t', '0.2'], outp) + retn = await s_t_healthcheck.main(['-c', curl, '-t', '0.4'], outp) self.eq(retn, 1) resp = json.loads(str(outp)) self.eq(resp.get('components')[0].get('name'), 'error') @@ -58,7 +58,7 @@ async def sleep(*args, **kwargs): _, port = await core.dmon.listen('tcp://127.0.0.1:0') root = await core.auth.getUserByName('root') await root.setPasswd('secret') - retn = await s_t_healthcheck.main(['-c', f'tcp://root:newp@127.0.0.1:{port}/cortex', '-t', '0.2'], outp) + retn = await s_t_healthcheck.main(['-c', f'tcp://root:newp@127.0.0.1:{port}/cortex', '-t', '0.4'], outp) self.eq(retn, 1) resp = json.loads(str(outp)) self.eq(resp.get('components')[0].get('name'), 'error') @@ -70,7 +70,7 @@ async def sleep(*args, **kwargs): logger.info('Checking without perms') outp.clear() - retn = await s_t_healthcheck.main(['-c', f'tcp://visi:secret@127.0.0.1:{port}/cortex', '-t', '0.2'], outp) + retn = await s_t_healthcheck.main(['-c', f'tcp://visi:secret@127.0.0.1:{port}/cortex', '-t', '0.4'], outp) self.eq(retn, 1) resp = json.loads(str(outp)) self.eq(resp.get('components')[0].get('name'), 'error') @@ -83,7 +83,7 @@ async def sleep(*args, **kwargs): await core.fini() await asyncio.sleep(0) outp.clear() - retn = await s_t_healthcheck.main(['-c', curl, '-t', '0.2'], outp) + retn = await s_t_healthcheck.main(['-c', curl, '-t', '0.4'], outp) self.eq(retn, 1) resp = json.loads(str(outp)) self.eq(resp.get('components')[0].get('name'), 'error') From 827f1ad6474a1ca36d8a640a2f34b478324cca98 Mon Sep 17 00:00:00 2001 From: James Gross <45212823+rakuy0@users.noreply.github.com> Date: Wed, 15 Jan 2025 16:12:56 -0500 Subject: [PATCH 08/19] Prevent some IndexError and AttributeError exceptions (SYN-8488 SYN-8523) (#4068) Co-authored-by: blackout --- changes/033795080031a65ba1ce50687ba174aa.yaml | 7 +++ changes/32327cef477d8fa55925e86841fc4fc8.yaml | 6 ++ synapse/lib/ast.py | 1 - synapse/lib/snap.py | 51 +++++++++++++---- synapse/tests/test_lib_storm.py | 55 +++++++++++++++++-- 5 files changed, 104 insertions(+), 16 deletions(-) create mode 100644 changes/033795080031a65ba1ce50687ba174aa.yaml create mode 100644 changes/32327cef477d8fa55925e86841fc4fc8.yaml diff --git a/changes/033795080031a65ba1ce50687ba174aa.yaml b/changes/033795080031a65ba1ce50687ba174aa.yaml new file mode 100644 index 0000000000..982976e8e4 --- /dev/null +++ b/changes/033795080031a65ba1ce50687ba174aa.yaml @@ -0,0 +1,7 @@ +--- +desc: Fixed an issue where the dictionary based guid constructor could raise unclear + Python ``IndexError`` exceptions, and instead raise ``BadTypeValu`` exceptions detailing + the problem. +prs: [] +type: bug +... diff --git a/changes/32327cef477d8fa55925e86841fc4fc8.yaml b/changes/32327cef477d8fa55925e86841fc4fc8.yaml new file mode 100644 index 0000000000..c3d5ea6457 --- /dev/null +++ b/changes/32327cef477d8fa55925e86841fc4fc8.yaml @@ -0,0 +1,6 @@ +--- +desc: Fixed an issue where invalid dictionary constructor values would result in unhandled + Python ``AttributeError`` exceptions leaking into the Storm runtime. +prs: [] +type: bug +... diff --git a/synapse/lib/ast.py b/synapse/lib/ast.py index c82edb583e..6b171d0218 100644 --- a/synapse/lib/ast.py +++ b/synapse/lib/ast.py @@ -4119,7 +4119,6 @@ async def feedfunc(): if not runtsafe: - first = True async for node, path in genr: # must reach back first to trigger sudo / etc diff --git a/synapse/lib/snap.py b/synapse/lib/snap.py index 656f44a252..80d334f173 100644 --- a/synapse/lib/snap.py +++ b/synapse/lib/snap.py @@ -362,7 +362,7 @@ async def _set(self, prop, valu, norminfo=None, ignore_ro=False): try: valu, norminfo = prop.type.norm(valu) except s_exc.BadTypeValu as e: - oldm = e.errinfo.get('mesg') + oldm = e.get('mesg') e.update({'prop': prop.name, 'form': prop.form.name, 'mesg': f'Bad prop value {prop.full}={valu!r} : {oldm}'}) @@ -1404,25 +1404,54 @@ async def _addGuidNodeByDict(self, form, vals, props=None): trycast = vals.pop('$try', False) addprops = vals.pop('$props', None) - if addprops is not None: - props.update(addprops) - try: - for name, valu in list(props.items()): + if not vals: + mesg = f'No values provided for form {form.full}' + raise s_exc.BadTypeValu(mesg=mesg) + + for name, valu in list(props.items()): + try: props[name] = form.reqProp(name).type.norm(valu) + except s_exc.BadTypeValu as e: + mesg = e.get('mesg') + e.update({ + 'prop': name, + 'form': form.name, + 'mesg': f'Bad value for prop {form.name}:{name}: {mesg}', + }) + raise e - for name, valu in vals.items(): + if addprops is not None: + for name, valu in addprops.items(): + try: + props[name] = form.reqProp(name).type.norm(valu) + except s_exc.BadTypeValu as e: + mesg = e.get("mesg") + if not trycast: + e.update({ + 'prop': name, + 'form': form.name, + 'mesg': f'Bad value for prop {form.name}:{name}: {mesg}' + }) + raise e + await self.warn(f'Skipping bad value for prop {form.name}:{name}: {mesg}') + + for name, valu in vals.items(): + try: prop = form.reqProp(name) norm, norminfo = prop.type.norm(valu) norms[name] = (prop, norm, norminfo) proplist.append((name, norm)) - except s_exc.BadTypeValu as e: - if not trycast: raise - mesg = e.errinfo.get('mesg') - await self.warn(f'Bad value for prop {name}: {mesg}') - return + except s_exc.BadTypeValu as e: + mesg = e.get('mesg') + e.update({ + 'prop': name, + 'form': form.name, + 'mesg': f'Bad value for prop {form.name}:{name}: {mesg}', + }) + raise e proplist.sort() diff --git a/synapse/tests/test_lib_storm.py b/synapse/tests/test_lib_storm.py index 4a9584e568..100c51a47c 100644 --- a/synapse/tests/test_lib_storm.py +++ b/synapse/tests/test_lib_storm.py @@ -53,10 +53,6 @@ async def test_lib_storm_guidctor(self): with self.raises(s_exc.BadTypeValu): await core.nodes('[ ou:org=({"hq": "woot"}) ]') - msgs = await core.stormlist('[ ou:org=({"hq": "woot", "$try": true}) ]') - self.len(0, [m for m in msgs if m[0] == 'node']) - self.stormIsInWarn('Bad value for prop hq: valu is not a guid', msgs) - nodes05 = await core.nodes('[ ou:org=({"name": "vertex", "$props": {"motto": "for the people"}}) ]') self.len(1, nodes05) self.eq('vertex', nodes05[0].get('name')) @@ -96,6 +92,57 @@ async def test_lib_storm_guidctor(self): self.len(1, nodes12) self.ne(nodes11[0].ndef, nodes12[0].ndef) + # GUID ctor has a short-circuit where it tries to find an existing ndef before it does, + # some property deconfliction, and `
=({})` when pushed through guid generation gives + # back the same guid as `=()`, which if we're not careful could lead to an + # inconsistent case where you fail to make a node because you don't provide any props, + # make a node with that matching ndef, and then run that invalid GUID ctor query again, + # and have it return back a node due to the short circuit. So test that we're consistent here. + with self.raises(s_exc.BadTypeValu): + await core.nodes('[ ou:org=({}) ]') + + self.len(1, await core.nodes('[ ou:org=() ]')) + + with self.raises(s_exc.BadTypeValu): + await core.nodes('[ ou:org=({}) ]') + + msgs = await core.stormlist('[ ou:org=({"$props": {"desc": "lol"}})]') + self.len(0, [m for m in msgs if m[0] == 'node']) + self.stormIsInErr('No values provided for form ou:org', msgs) + + msgs = await core.stormlist('[ou:org=({"name": "burrito corp", "$props": {"phone": "lolnope"}})]') + self.len(0, [m for m in msgs if m[0] == 'node']) + self.stormIsInErr('Bad value for prop ou:org:phone: requires a digit string', msgs) + + with self.raises(s_exc.BadTypeValu): + await core.nodes('[ ou:org=({"$try": true}) ]') + + # $try only affects $props + msgs = await core.stormlist('[ ou:org=({"founded": "lolnope", "$try": true}) ]') + self.len(0, [m for m in msgs if m[0] == 'node']) + self.stormIsInErr('Bad value for prop ou:org:founded: Unknown time format for lolnope', msgs) + + msgs = await core.stormlist('[ou:org=({"name": "burrito corp", "$try": true, "$props": {"phone": "lolnope", "desc": "burritos man"}})]') + nodes = [m for m in msgs if m[0] == 'node'] + self.len(1, nodes) + node = nodes[0][1] + props = node[1]['props'] + self.none(props.get('phone')) + self.eq(props.get('name'), 'burrito corp') + self.eq(props.get('desc'), 'burritos man') + self.stormIsInWarn('Skipping bad value for prop ou:org:phone: requires a digit string', msgs) + + await self.asyncraises(s_exc.BadTypeValu, core.addNode(core.auth.rootuser, 'ou:org', {'name': 'org name 77', 'phone': 'lolnope'}, props={'desc': 'an org desc'})) + + await self.asyncraises(s_exc.BadTypeValu, core.addNode(core.auth.rootuser, 'ou:org', {'name': 'org name 77'}, props={'desc': 'an org desc', 'phone': 'lolnope'})) + + node = await core.addNode(core.auth.rootuser, 'ou:org', {'$try': True, '$props': {'phone': 'invalid'}, 'name': 'org name 77'}, props={'desc': 'an org desc'}) + self.nn(node) + props = node[1]['props'] + self.none(props.get('phone')) + self.eq(props.get('name'), 'org name 77') + self.eq(props.get('desc'), 'an org desc') + async def test_lib_storm_jsonexpr(self): async with self.getTestCore() as core: From a20d9105deb91d91829732852b5b9965d4190f6d Mon Sep 17 00:00:00 2001 From: epiphyte Date: Wed, 15 Jan 2025 16:17:10 -0500 Subject: [PATCH 09/19] Update codecov config to reflect the expectations for a single build. --- codecov.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codecov.yml b/codecov.yml index 54a7fadba4..97ec918e07 100644 --- a/codecov.yml +++ b/codecov.yml @@ -1,6 +1,6 @@ codecov: notify: - after_n_builds: 8 + after_n_builds: 1 coverage: status: From c01a617b378e984919de0d32d2cfedcadce4d9c5 Mon Sep 17 00:00:00 2001 From: epiphyte Date: Wed, 15 Jan 2025 16:22:44 -0500 Subject: [PATCH 10/19] Also update the codecov comment threshold. --- codecov.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codecov.yml b/codecov.yml index 97ec918e07..b609f09312 100644 --- a/codecov.yml +++ b/codecov.yml @@ -12,4 +12,4 @@ coverage: target: 96% comment: - after_n_builds: 8 \ No newline at end of file + after_n_builds: 1 From 8dfdfa8e979887ca7b665b69f811e859fa71cea4 Mon Sep 17 00:00:00 2001 From: vEpiphyte Date: Wed, 15 Jan 2025 17:10:08 -0500 Subject: [PATCH 11/19] $lib.cache.fixed - convert StormCtrlFlow exceptions to StormRuntimeError (SYN-8513) (#4073) Previously these returned `None` but that hides possible errors. --------- Co-authored-by: blackout --- changes/b4642a502f49353787b4f6632a6a6566.yaml | 3 +- synapse/lib/stormlib/cache.py | 8 +++- synapse/tests/test_lib_stormlib_cache.py | 44 ++++++++++++++++--- 3 files changed, 46 insertions(+), 9 deletions(-) diff --git a/changes/b4642a502f49353787b4f6632a6a6566.yaml b/changes/b4642a502f49353787b4f6632a6a6566.yaml index 0a2cae37b9..c0558c9b79 100644 --- a/changes/b4642a502f49353787b4f6632a6a6566.yaml +++ b/changes/b4642a502f49353787b4f6632a6a6566.yaml @@ -3,7 +3,8 @@ desc: Fixed an issue with the Storm loop and generator keywords, ``continue``, ` and ``stop``. Using these keywords outside of a loop or generator function will now raise a ``StormRuntimeError`` exception. Using these keywords to tear down the Storm runtime will now emit an ``err`` message with the type ``StormRuntimeError`` and a - message indicating the invalid use of the keywords. + message indicating the invalid use of the keywords. The use of these keywords or + ``$lib.exit()`` in ``$lib.cache.fixed`` callbacks will now raise a ``StormRuntimeError``. prs: [] type: bug ... diff --git a/synapse/lib/stormlib/cache.py b/synapse/lib/stormlib/cache.py index 2233825b44..60428357c2 100644 --- a/synapse/lib/stormlib/cache.py +++ b/synapse/lib/stormlib/cache.py @@ -172,8 +172,12 @@ async def _runCallback(self, key): await asyncio.sleep(0) except s_stormctrl.StormReturn as e: return await s_stormtypes.toprim(e.item) - except s_stormctrl.StormCtrlFlow: - pass + except s_stormctrl.StormCtrlFlow as e: + name = e.__class__.__name__ + if hasattr(e, 'statement'): + name = e.statement + exc = s_exc.StormRuntimeError(mesg=f'Storm control flow "{name}" not allowed in cache callbacks.') + raise exc from None async def _reqKey(self, key): if s_stormtypes.ismutable(key): diff --git a/synapse/tests/test_lib_stormlib_cache.py b/synapse/tests/test_lib_stormlib_cache.py index 8d3da6da0a..5fc748de21 100644 --- a/synapse/tests/test_lib_stormlib_cache.py +++ b/synapse/tests/test_lib_stormlib_cache.py @@ -138,19 +138,51 @@ async def test_storm_lib_cache_fixed(self): self.none(await core.callStorm('return($lib.cache.fixed("if (0) { return(yup) }").get(foo))')) ## control flow exceptions don't propagate up - rets = await core.callStorm(''' + msgs = await core.stormlist(''' $cache = $lib.cache.fixed( ${ if ($cache_key < (2)) { return (`key={$cache_key}`) } else { break } } ) - $rets = ([]) + for $i in $lib.range(4) { + $lib.print(`{$cache.get($i)}`) + } + ''') + self.stormIsInPrint('key=1', msgs) + self.stormNotInPrint('key=2', msgs) + self.stormIsInErr('Storm control flow "break" not allowed in cache callbacks.', msgs) + + msgs = await core.stormlist(''' + $cache = $lib.cache.fixed( ${ if ($cache_key < (2)) { return (`key={$cache_key}`) } else { continue } } ) for $i in $lib.range(4) { - $rets.append($cache.get($i)) + $lib.print(`{$cache.get($i)}`) } + ''') + self.stormIsInPrint('key=1', msgs) + self.stormNotInPrint('key=2', msgs) + self.stormIsInErr('Storm control flow "continue" not allowed in cache callbacks.', msgs) - $rets.append(`i={$i}`) - return($rets) + msgs = await core.stormlist(''' + $cache = $lib.cache.fixed( ${ if ($cache_key < (2)) { return (`key={$cache_key}`) } else { stop } } ) + + for $i in $lib.range(4) { + $lib.print(`{$cache.get($i)}`) + } ''') - self.eq(['key=0', 'key=1', None, None, 'i=3'], rets) + self.stormIsInPrint('key=1', msgs) + self.stormNotInPrint('key=2', msgs) + self.stormIsInErr('Storm control flow "stop" not allowed in cache callbacks.', msgs) + + msgs = await core.stormlist(''' + $cache = $lib.cache.fixed( + ${ if ($cache_key < (2)) { return (`key={$cache_key}`) } else { $lib.exit(mesg=newp) } } + ) + + for $i in $lib.range(4) { + $lib.print(`{$cache.get($i)}`) + } + ''') + self.stormIsInPrint('key=1', msgs) + self.stormNotInPrint('key=2', msgs) + self.stormIsInErr('Storm control flow "StormExit" not allowed in cache callbacks.', msgs) ## control flow scoped inside the callback rets = await core.callStorm(""" From df25a3295c82bb8cd6bd3a67aefe61a6ca605f38 Mon Sep 17 00:00:00 2001 From: Cisphyx Date: Thu, 16 Jan 2025 09:52:33 -0500 Subject: [PATCH 12/19] Fix function type detection (SYN-8534, SYN-8536) (#4066) --- changes/303e4458db5d4c5f9f39813b5be41ad7.yaml | 6 + changes/7a4e69c153170cce7ed678912c3857a2.yaml | 6 + synapse/lib/ast.py | 81 +++++++-- synapse/tests/test_cortex.py | 8 + synapse/tests/test_lib_ast.py | 162 ++++++++++++++++++ 5 files changed, 247 insertions(+), 16 deletions(-) create mode 100644 changes/303e4458db5d4c5f9f39813b5be41ad7.yaml create mode 100644 changes/7a4e69c153170cce7ed678912c3857a2.yaml diff --git a/changes/303e4458db5d4c5f9f39813b5be41ad7.yaml b/changes/303e4458db5d4c5f9f39813b5be41ad7.yaml new file mode 100644 index 0000000000..2d4886a75c --- /dev/null +++ b/changes/303e4458db5d4c5f9f39813b5be41ad7.yaml @@ -0,0 +1,6 @@ +--- +desc: Fixed an issue in Storm functions where using the return keyword in a subquery + used as a value could incorrectly change the function type. +prs: [] +type: bug +... diff --git a/changes/7a4e69c153170cce7ed678912c3857a2.yaml b/changes/7a4e69c153170cce7ed678912c3857a2.yaml new file mode 100644 index 0000000000..44de852536 --- /dev/null +++ b/changes/7a4e69c153170cce7ed678912c3857a2.yaml @@ -0,0 +1,6 @@ +--- +desc: Fixed an issue in Storm where attempting to iterate a non-iterable object would + raise a Python exception rather than a StormRuntimeError. +prs: [] +type: bug +... diff --git a/synapse/lib/ast.py b/synapse/lib/ast.py index 6b171d0218..a3433647fc 100644 --- a/synapse/lib/ast.py +++ b/synapse/lib/ast.py @@ -134,27 +134,27 @@ def prepare(self): pass def hasAstClass(self, clss): - hasast = self.hasast.get(clss) - if hasast is not None: + if (hasast := self.hasast.get(clss)) is not None: return hasast - retn = False + retn = self._hasAstClass(clss) + self.hasast[clss] = retn + return retn + + def _hasAstClass(self, clss): for kid in self.kids: if isinstance(kid, clss): - retn = True - break + return True - if isinstance(kid, (EditPropSet, EditCondPropSet, Function, CmdOper)): + if isinstance(kid, (Edit, Function, CmdOper, SetVarOper, SetItemOper, VarListSetOper, Value, N1Walk, LiftOper)): continue if kid.hasAstClass(clss): - retn = True - break + return True - self.hasast[clss] = retn - return retn + return False def optimize(self): [k.optimize() for k in self.kids] @@ -930,6 +930,9 @@ async def getCatchBlock(self, name, runt, path=None): class CatchBlock(AstNode): + def _hasAstClass(self, clss): + return self.kids[1].hasAstClass(clss) + async def run(self, runt, genr): async for item in self.kids[2].run(runt, genr): yield item @@ -963,6 +966,9 @@ async def catches(self, name, runt, path=None): class ForLoop(Oper): + def _hasAstClass(self, clss): + return self.kids[2].hasAstClass(clss) + def getRuntVars(self, runt): runtsafe = self.kids[1].isRuntSafe(runt) @@ -998,6 +1004,14 @@ async def run(self, runt, genr): valu = () async with contextlib.aclosing(s_coro.agen(valu)) as agen: + + try: + agen, _ = await pullone(agen) + except TypeError: + styp = await s_stormtypes.totype(valu, basetypes=True) + mesg = f"'{styp}' object is not iterable: {s_common.trimText(repr(valu))}" + raise self.kids[1].addExcInfo(s_exc.StormRuntimeError(mesg=mesg, type=styp)) from None + async for item in agen: if isinstance(name, (list, tuple)): @@ -1064,6 +1078,13 @@ async def run(self, runt, genr): valu = () async with contextlib.aclosing(s_coro.agen(valu)) as agen: + try: + agen, _ = await pullone(agen) + except TypeError: + styp = await s_stormtypes.totype(valu, basetypes=True) + mesg = f"'{styp}' object is not iterable: {s_common.trimText(repr(valu))}" + raise self.kids[1].addExcInfo(s_exc.StormRuntimeError(mesg=mesg, type=styp)) from None + async for item in agen: if isinstance(name, (list, tuple)): @@ -1109,6 +1130,9 @@ async def run(self, runt, genr): class WhileLoop(Oper): + def _hasAstClass(self, clss): + return self.kids[1].hasAstClass(clss) + async def run(self, runt, genr): subq = self.kids[1] node = None @@ -1162,20 +1186,21 @@ async def run(self, runt, genr): await asyncio.sleep(0) async def pullone(genr): - gotone = None - async for gotone in genr: - break + empty = False + try: + gotone = await genr.__anext__() + except StopAsyncIteration: + empty = True async def pullgenr(): - - if gotone is None: + if empty: return yield gotone async for item in genr: yield item - return pullgenr(), gotone is None + return pullgenr(), empty class CmdOper(Oper): @@ -1385,6 +1410,14 @@ async def run(self, runt, genr): class SwitchCase(Oper): + def _hasAstClass(self, clss): + + for kid in self.kids[1:]: + if kid.hasAstClass(clss): + return True + + return False + def prepare(self): self.cases = {} self.defcase = None @@ -4811,6 +4844,22 @@ class IfClause(AstNode): class IfStmt(Oper): + def _hasAstClass(self, clss): + + clauses = self.kids + + if not isinstance(clauses[-1], IfClause): + if clauses[-1].hasAstClass(clss): + return True + + clauses = clauses[:-1] + + for clause in clauses: + if clause.kids[1].hasAstClass(clss): + return True + + return False + def prepare(self): if isinstance(self.kids[-1], IfClause): self.elsequery = None diff --git a/synapse/tests/test_cortex.py b/synapse/tests/test_cortex.py index 7d6b8c6108..2cd50e42da 100644 --- a/synapse/tests/test_cortex.py +++ b/synapse/tests/test_cortex.py @@ -5230,6 +5230,14 @@ async def test_storm_forloop(self): self.eq(('inet:fqdn', 'nest.com'), nodes[0].ndef) self.eq(('inet:fqdn', 'nest.com'), nodes[1].ndef) + with self.raises(s_exc.StormRuntimeError) as err: + await core.nodes('[ it:dev:int=1 ] for $n in $node.value() { }') + self.isin("'int' object is not iterable: 1", err.exception.errinfo.get('mesg')) + + with self.raises(s_exc.StormRuntimeError) as err: + await core.nodes('for $n in { .created return($node) } { }') + self.isin("'node' object is not iterable", err.exception.errinfo.get('mesg')) + async def test_storm_whileloop(self): async with self.getTestCore() as core: diff --git a/synapse/tests/test_lib_ast.py b/synapse/tests/test_lib_ast.py index a90ece527e..0c0727f49f 100644 --- a/synapse/tests/test_lib_ast.py +++ b/synapse/tests/test_lib_ast.py @@ -4524,3 +4524,165 @@ async def test_ast_varlistset(self): text = '($x, $y) = (1)' with self.raises(s_exc.StormRuntimeError): await core.nodes(text) + + async def test_ast_functypes(self): + + async with self.getTestCore() as core: + + async def verify(q, isin=False): + msgs = await core.stormlist(q) + if isin: + self.stormIsInPrint('yep', msgs) + else: + self.stormNotInPrint('newp', msgs) + self.len(1, [m for m in msgs if m[0] == 'node']) + self.stormHasNoErr(msgs) + + q = ''' + function foo() { + for $n in { return((newp,)) } { $lib.print($n) } + } + [ it:dev:str=test ] + $foo() + ''' + await verify(q) + + q = ''' + function foo() { + while { return((newp,)) } { $lib.print(newp) break } + } + [ it:dev:str=test ] + $foo() + ''' + await verify(q) + + q = ''' + function foo() { + switch $lib.print({ return(newp) }) { *: { $lib.print(newp) } } + } + [ it:dev:str=test ] + $foo() + ''' + await verify(q) + + q = ''' + function foo() { + switch $foo { *: { $lib.print(yep) return() } } + } + [ it:dev:str=test ] + $foo() + ''' + await verify(q, isin=True) + + q = ''' + function foo() { + if { return(newp) } { $lib.print(newp) } + } + [ it:dev:str=test ] + $foo() + ''' + await verify(q) + + q = ''' + function foo() { + if (false) { $lib.print(newp) } + elif { return(newp) } { $lib.print(newp) } + } + [ it:dev:str=test ] + $foo() + ''' + await verify(q) + + q = ''' + function foo() { + if (false) { $lib.print(newp) } + elif (true) { $lib.print(yep) return() } + } + [ it:dev:str=test ] + $foo() + ''' + await verify(q) + + q = ''' + function foo() { + if (false) { $lib.print(newp) } + elif (false) { $lib.print(newp) } + else { $lib.print(yep) return() } + } + [ it:dev:str=test ] + $foo() + ''' + await verify(q, isin=True) + + q = ''' + function foo() { + [ it:dev:str=foo +(refs)> { $lib.print(newp) return() } ] + } + [ it:dev:str=test ] + $foo() + ''' + await verify(q) + + q = ''' + function foo() { + $lib.print({ return(newp) }) + } + [ it:dev:str=test ] + $foo() + ''' + await verify(q) + + q = ''' + function foo() { + $x = { $lib.print(newp) return() } + } + [ it:dev:str=test ] + $foo() + ''' + await verify(q) + + q = ''' + function foo() { + ($x, $y) = { $lib.print(newp) return((foo, bar)) } + } + [ it:dev:str=test ] + $foo() + ''' + await verify(q) + + q = ''' + function foo() { + $x = ({}) + $x.y = { $lib.print(newp) return((foo, bar)) } + } + [ it:dev:str=test ] + $foo() + ''' + await verify(q) + + q = ''' + function foo() { + .created -({$lib.print(newp) return(refs)})> * + } + [ it:dev:str=test ] + $foo() + ''' + await verify(q) + + q = ''' + function foo() { + try { $lib.raise(boom) } catch { $lib.print(newp) return(newp) } as e {} + } + [ it:dev:str=test ] + $foo() + ''' + await verify(q) + + q = ''' + function foo() { + it:dev:str={ $lib.print(newp) return(test) } + } + [ it:dev:str=test ] + $foo() + ''' + await verify(q) From ce88a740894943f32813159f695f226a6d66ee50 Mon Sep 17 00:00:00 2001 From: Cisphyx Date: Thu, 16 Jan 2025 12:09:45 -0500 Subject: [PATCH 13/19] Include X-Synapse-Version header in pkg.load requests (SYN-8474) (#4074) --- changes/8221696241d0c8e0142d663ec70cc6da.yaml | 6 ++++++ synapse/lib/storm.py | 4 +++- synapse/tests/test_lib_storm.py | 3 +++ 3 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 changes/8221696241d0c8e0142d663ec70cc6da.yaml diff --git a/changes/8221696241d0c8e0142d663ec70cc6da.yaml b/changes/8221696241d0c8e0142d663ec70cc6da.yaml new file mode 100644 index 0000000000..08facb0633 --- /dev/null +++ b/changes/8221696241d0c8e0142d663ec70cc6da.yaml @@ -0,0 +1,6 @@ +--- +desc: Updated the ``pkg.load`` Storm command to include an ``X-Synapse-Version`` HTTP header + in requests. +prs: [] +type: feat +... diff --git a/synapse/lib/storm.py b/synapse/lib/storm.py index c09d1738e7..a453f17684 100644 --- a/synapse/lib/storm.py +++ b/synapse/lib/storm.py @@ -984,7 +984,9 @@ $ssl = $lib.true if $cmdopts.ssl_noverify { $ssl = $lib.false } - $resp = $lib.inet.http.get($cmdopts.url, ssl_verify=$ssl) + $headers = ({'X-Synapse-Version': $lib.str.join('.', $lib.version.synapse())}) + + $resp = $lib.inet.http.get($cmdopts.url, ssl_verify=$ssl, headers=$headers) if ($resp.code != 200) { $lib.warn("pkg.load got HTTP code: {code} for URL: {url}", code=$resp.code, url=$cmdopts.url) diff --git a/synapse/tests/test_lib_storm.py b/synapse/tests/test_lib_storm.py index 100c51a47c..9d94327b1f 100644 --- a/synapse/tests/test_lib_storm.py +++ b/synapse/tests/test_lib_storm.py @@ -2742,6 +2742,7 @@ async def test_storm_pkg_load(self): class PkgHandler(s_httpapi.Handler): async def get(self, name): + assert self.request.headers.get('X-Synapse-Version') == s_version.verstring if name == 'notok': self.sendRestErr('FooBar', 'baz faz') @@ -2751,6 +2752,8 @@ async def get(self, name): class PkgHandlerRaw(s_httpapi.Handler): async def get(self, name): + assert self.request.headers.get('X-Synapse-Version') == s_version.verstring + self.set_header('Content-Type', 'application/json') return self.write(pkg) From 942daef86a6014411201a1c647ed0c6c45c3cc4b Mon Sep 17 00:00:00 2001 From: vEpiphyte Date: Thu, 16 Jan 2025 13:30:23 -0500 Subject: [PATCH 14/19] Update aiohttp-socks (#4075) Blocks https://github.com/vertexproject/vtx-base-image/pull/797 --- pyproject.toml | 2 +- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 1aea172b4f..e3046ff69c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,7 +32,7 @@ dependencies = [ 'regex>=2022.9.11', 'PyYAML>=5.4,<6.1.0', 'aiohttp>=3.10.0,<4.0', - 'aiohttp-socks>=0.9.0,<0.10.0', + 'aiohttp-socks>=0.9.0,<0.11.0', 'aioimaplib>=1.1.0,<1.2.0', 'aiosmtplib>=3.0.0,<3.1.0', 'prompt_toolkit>=3.0.29,<3.1.0', diff --git a/requirements.txt b/requirements.txt index bd52fb24e1..6211ef28e0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ tornado>=6.2.0,<7.0.0 regex>=2022.9.11 PyYAML>=5.4,<6.1.0 aiohttp>=3.10.0,<4.0 -aiohttp-socks>=0.9.0,<0.10.0 +aiohttp-socks>=0.9.0,<0.11.0 aioimaplib>=1.1.0,<1.2.0 aiosmtplib>=3.0.0,<3.1.0 prompt_toolkit>=3.0.29,<3.1.0 From 8e081535e55c545edb1e456a2348c77c9c32e4ea Mon Sep 17 00:00:00 2001 From: OCBender <181250370+OCBender@users.noreply.github.com> Date: Thu, 16 Jan 2025 13:30:36 -0500 Subject: [PATCH 15/19] Deprecate $lib.text() (SYN-8482) (#4072) Co-authored-by: bender Co-authored-by: vEpiphyte --- changes/d4f3dc440ad8e7181a9a30cc4bea2c41.yaml | 5 +++++ synapse/lib/stormtypes.py | 7 +++++- synapse/tests/test_cortex.py | 2 +- synapse/tests/test_lib_ast.py | 6 ++--- synapse/tests/test_lib_stormtypes.py | 22 +++++-------------- 5 files changed, 21 insertions(+), 21 deletions(-) create mode 100644 changes/d4f3dc440ad8e7181a9a30cc4bea2c41.yaml diff --git a/changes/d4f3dc440ad8e7181a9a30cc4bea2c41.yaml b/changes/d4f3dc440ad8e7181a9a30cc4bea2c41.yaml new file mode 100644 index 0000000000..2f293cfb0d --- /dev/null +++ b/changes/d4f3dc440ad8e7181a9a30cc4bea2c41.yaml @@ -0,0 +1,5 @@ +--- +desc: Deprecated ``$lib.text()``. Please use a list to append strings to, and then use ``$lib.str.join()`` to join them on demand. +prs: [] +type: deprecation +... diff --git a/synapse/lib/stormtypes.py b/synapse/lib/stormtypes.py index 8d27565100..f0fcaed157 100644 --- a/synapse/lib/stormtypes.py +++ b/synapse/lib/stormtypes.py @@ -1308,7 +1308,8 @@ class LibBase(Lib): cli> storm if $lib.false { $lib.print('Is True') } else { $lib.print('Is False') } Is False''', 'type': 'boolean', }, - {'name': 'text', 'desc': 'Get a Storm Text object.', + {'name': 'text', 'desc': 'Get a Storm Text object. This is deprecated; please use a list to append strings to, and then use ``$lib.str.join()`` to join them on demand.', + 'deprecated': {'eolvers': '3.0.0'}, 'type': {'type': 'function', '_funcname': '_text', 'args': ( {'name': '*args', 'type': 'str', @@ -1687,6 +1688,10 @@ async def _list(self, *vals): @stormfunc(readonly=True) async def _text(self, *args): + s_common.deprecated('$lib.text()', curv='2.194.0') + runt = s_scope.get('runt') + if runt: + await runt.snap.warnonce('$lib.text() is deprecated. Please use a list to append strings to, and then use ``$lib.str.join()`` to join them on demand.') valu = ''.join(args) return Text(valu) diff --git a/synapse/tests/test_cortex.py b/synapse/tests/test_cortex.py index 2cd50e42da..f0116db892 100644 --- a/synapse/tests/test_cortex.py +++ b/synapse/tests/test_cortex.py @@ -5579,7 +5579,7 @@ async def test_storm_ifstmt(self): self.len(1, nodes) async def test_storm_order(self): - q = '''[test:str=foo :hehe=bar] $tvar=$lib.text() $tvar.add(1) $tvar.add(:hehe) $lib.print($tvar.str()) ''' + q = '''[test:str=foo :hehe=bar] $tvar=() $tvar.append(1) $tvar.append(:hehe) $lib.print($lib.str.join('', $tvar)) ''' async with self.getTestCore() as core: mesgs = await core.stormlist(q) self.stormIsInPrint('1bar', mesgs) diff --git a/synapse/tests/test_lib_ast.py b/synapse/tests/test_lib_ast.py index 0c0727f49f..54a6181252 100644 --- a/synapse/tests/test_lib_ast.py +++ b/synapse/tests/test_lib_ast.py @@ -292,11 +292,11 @@ async def test_ast_runtsafe_bug(self): async with self.getTestCore() as core: q = ''' [test:str=another :hehe=asdf] - $s = $lib.text("Foo") + $s = ("Foo",) $newvar=:hehe -.created - $s.add("yar {x}", x=$newvar) - $lib.print($s.str()) + $s.append("yar {x}", x=$newvar) + $lib.print($lib.str.join('', $s)) ''' mesgs = await core.stormlist(q) prints = [m[1]['mesg'] for m in mesgs if m[0] == 'print'] diff --git a/synapse/tests/test_lib_stormtypes.py b/synapse/tests/test_lib_stormtypes.py index 686a4c75ad..b5fd051494 100644 --- a/synapse/tests/test_lib_stormtypes.py +++ b/synapse/tests/test_lib_stormtypes.py @@ -1570,26 +1570,13 @@ async def test_storm_lib_list(self): ret = await core.callStorm(q) self.eq(ret, ('bar', 'baz', 'foo',)) - # Sort a few text objects - q = '$foo=$lib.text(foo) $bar=$lib.text(bar) $baz=$lib.text(baz) $v=($foo, $bar, $baz) $v.sort() return ($v)' - ret = await core.callStorm(q) - self.eq(ret, ('bar', 'baz', 'foo',)) - # incompatible sort types with self.raises(s_exc.StormRuntimeError): await core.callStorm('$v=(foo,bar,(1)) $v.sort() return ($v)') - # mix Prims and heavy objects - with self.raises(s_exc.StormRuntimeError): - q = '$foo=$lib.text(foo) $bar=$lib.text(bar) $v=($foo, aString, $bar,) $v.sort() return ($v)' - await core.callStorm(q) - q = '$l = (1, 2, (3), 4, 1, (3), 3, asdf) return ( $l.unique() )' self.eq(['1', '2', 3, '4', '3', 'asdf'], await core.callStorm(q)) - q = '$a=$lib.text(hehe) $b=$lib.text(haha) $c=$lib.text(hehe) $foo=($a, $b, $c) return ($foo.unique())' - self.eq(['hehe', 'haha'], await core.callStorm(q)) - await core.addUser('lowuser1') await core.addUser('lowuser2') q = ''' @@ -1807,6 +1794,7 @@ async def test_storm_csv(self): async def test_storm_text(self): async with self.getTestCore() as core: + # $lib.text() is deprecated (SYN-8482); test ensures the object works as expected until removed nodes = await core.nodes(''' [ test:int=10 ] $text=$lib.text(hehe) { +test:int>=10 $text.add(haha) } [ test:str=$text.str() ] +test:str''') @@ -1820,6 +1808,10 @@ async def test_storm_text(self): self.stormIsInPrint('8', msgs) self.stormIsInPrint('13', msgs) + msgs = await core.stormlist('help --verbose $lib.text') + self.stormIsInPrint('Warning', msgs) + self.stormIsInPrint('$lib.text`` has been deprecated and will be removed in version 3.0.0', msgs) + async def test_storm_set(self): async with self.getTestCore() as core: @@ -2215,7 +2207,7 @@ def __eq__(self, othr): # text q = ''' - $text = $lib.text(beepboopgetthejedi) + $text = () $text.append(beepboopgetthejedi) $set = $lib.set($text) ''' msgs = await core.stormlist(q) @@ -3430,8 +3422,6 @@ async def test_storm_lib_vars_type(self): self.eq('telepath:proxy:method', await core.callStorm('return( $lib.vars.type($lib.telepath.open($url).getCellInfo) )', opts)) self.eq('telepath:proxy:genrmethod', await core.callStorm('return( $lib.vars.type($lib.telepath.open($url).storm) )', opts)) - self.eq('text', await core.callStorm('return ( $lib.vars.type($lib.text(hehe)) )')) - self.eq('node', await core.callStorm('[test:str=foo] return ($lib.vars.type($node))')) self.eq('node:props', await core.callStorm('[test:str=foo] return ($lib.vars.type($node.props))')) self.eq('node:data', await core.callStorm('[test:str=foo] return ($lib.vars.type($node.data))')) From 39047319c9fd94ea46485d87aa4d00e4d7f9540d Mon Sep 17 00:00:00 2001 From: vEpiphyte Date: Thu, 16 Jan 2025 17:10:10 -0500 Subject: [PATCH 16/19] Changelog for v2.194.0 (#4077) --- CHANGELOG.rst | 59 ++++++ changes/033795080031a65ba1ce50687ba174aa.yaml | 7 - changes/1d7ec4bf8dbac3b96ecf801d95a7ab4c.yaml | 6 - changes/303e4458db5d4c5f9f39813b5be41ad7.yaml | 6 - changes/32327cef477d8fa55925e86841fc4fc8.yaml | 6 - changes/4a4fe1c2eb09b8a712af72dc6ce41c38.yaml | 8 - changes/7a4e69c153170cce7ed678912c3857a2.yaml | 6 - changes/8221696241d0c8e0142d663ec70cc6da.yaml | 6 - changes/b4642a502f49353787b4f6632a6a6566.yaml | 10 - changes/d4f3dc440ad8e7181a9a30cc4bea2c41.yaml | 5 - changes/e1b7c7693e7454c32ca657a2bed734d5.yaml | 5 - changes/fd2d79b0daf0705278a48e86b15524c7.yaml | 6 - changes/ff623ded36292878c995dfcf1874daf4.yaml | 5 - .../model_updates/update_v2_194_0.rst | 191 ++++++++++++++++++ synapse/tools/changelog.py | 10 +- 15 files changed, 256 insertions(+), 80 deletions(-) delete mode 100644 changes/033795080031a65ba1ce50687ba174aa.yaml delete mode 100644 changes/1d7ec4bf8dbac3b96ecf801d95a7ab4c.yaml delete mode 100644 changes/303e4458db5d4c5f9f39813b5be41ad7.yaml delete mode 100644 changes/32327cef477d8fa55925e86841fc4fc8.yaml delete mode 100644 changes/4a4fe1c2eb09b8a712af72dc6ce41c38.yaml delete mode 100644 changes/7a4e69c153170cce7ed678912c3857a2.yaml delete mode 100644 changes/8221696241d0c8e0142d663ec70cc6da.yaml delete mode 100644 changes/b4642a502f49353787b4f6632a6a6566.yaml delete mode 100644 changes/d4f3dc440ad8e7181a9a30cc4bea2c41.yaml delete mode 100644 changes/e1b7c7693e7454c32ca657a2bed734d5.yaml delete mode 100644 changes/fd2d79b0daf0705278a48e86b15524c7.yaml delete mode 100644 changes/ff623ded36292878c995dfcf1874daf4.yaml create mode 100644 docs/synapse/userguides/model_updates/update_v2_194_0.rst diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 767b36375b..e4270249b3 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -5,6 +5,65 @@ ***************** Synapse Changelog ***************** +v2.194.0 - 2025-01-16 +===================== + +Model Changes +------------- +- Added ``alts`` definitions to the following forms: ``geo:place``, + ``it:prod:soft``, ``it:prod:softver``, ``ou:campaign``, ``ou:conference``, + ``ou:goal``, ``ou:industry``, ``pol:country``, ``ps:contact``, ``ps:person``, + ``risk:threat``, ``risk:tool:software``, and ``risk:vuln``. + (`#4064 `_) +- See :ref:`userguide_model_v2_194_0` for more detailed model changes. + +Features and Enhancements +------------------------- +- Added syntax for conditional node property edit operators in Storm. + (`#4046 `_) +- Updated the ``pkg.load`` Storm command to include an ``X-Synapse-Version`` + HTTP header in requests. + (`#4074 `_) + +Bugfixes +-------- +- Fixed an issue with the Storm loop and generator keywords, ``continue``, + ``break``, and ``stop``. Using these keywords outside of a loop or generator + function will now raise a ``StormRuntimeError`` exception. Using these + keywords to tear down the Storm runtime will now emit an ``err`` message with + the type ``StormRuntimeError`` and a message indicating the invalid use of + the keywords. The use of these keywords or ``$lib.exit()`` in + ``$lib.cache.fixed`` callbacks will now raise a ``StormRuntimeError``. + (`#4025 `_) + (`#4073 `_) +- Fixed a Cortex cron scheduler loop error during a mirror promotion. + (`#4058 `_) +- Fixed bug in password complexity rules where setting a password to (null) or + None would fail. + (`#4059 `_) +- Fixed an issue in Storm where attempting to iterate a non-iterable object + would raise a Python exception rather than a ``StormRuntimeError``. + (`#4066 `_) +- Fixed an issue in Storm functions where using the return keyword in a + subquery used as a value could incorrectly change the function type. + (`#4066 `_) +- Fixed an issue where invalid dictionary constructor values would result in + unhandled Python ``AttributeError`` exceptions leaking into the Storm + runtime. + (`#4068 `_) +- Fixed an issue where the dictionary based guid constructor could raise + unclear Python ``IndexError`` exceptions. It now raises ``BadTypeValu`` + exceptions detailing the problem. + (`#4068 `_) + +Deprecations +------------ +- The Storm function ``$lib.list()`` has been deprecated, in favor of using the + ``()`` or ``([])`` style syntax for directly declaring a list in Storm. + (`#4071 `_) +- Deprecated ``$lib.text()``. Please use a list to append strings to, and then + use ``$lib.str.join()`` to join them on demand. + (`#4072 `_) v2.193.0 - 2025-01-06 ===================== diff --git a/changes/033795080031a65ba1ce50687ba174aa.yaml b/changes/033795080031a65ba1ce50687ba174aa.yaml deleted file mode 100644 index 982976e8e4..0000000000 --- a/changes/033795080031a65ba1ce50687ba174aa.yaml +++ /dev/null @@ -1,7 +0,0 @@ ---- -desc: Fixed an issue where the dictionary based guid constructor could raise unclear - Python ``IndexError`` exceptions, and instead raise ``BadTypeValu`` exceptions detailing - the problem. -prs: [] -type: bug -... diff --git a/changes/1d7ec4bf8dbac3b96ecf801d95a7ab4c.yaml b/changes/1d7ec4bf8dbac3b96ecf801d95a7ab4c.yaml deleted file mode 100644 index c57ab40869..0000000000 --- a/changes/1d7ec4bf8dbac3b96ecf801d95a7ab4c.yaml +++ /dev/null @@ -1,6 +0,0 @@ ---- -desc: The Storm function ``$lib.list()`` has been deprecated, in favor of using the ``()`` or - ``([])`` style syntax for directly declaring a list in Storm. -prs: [] -type: deprecation -... diff --git a/changes/303e4458db5d4c5f9f39813b5be41ad7.yaml b/changes/303e4458db5d4c5f9f39813b5be41ad7.yaml deleted file mode 100644 index 2d4886a75c..0000000000 --- a/changes/303e4458db5d4c5f9f39813b5be41ad7.yaml +++ /dev/null @@ -1,6 +0,0 @@ ---- -desc: Fixed an issue in Storm functions where using the return keyword in a subquery - used as a value could incorrectly change the function type. -prs: [] -type: bug -... diff --git a/changes/32327cef477d8fa55925e86841fc4fc8.yaml b/changes/32327cef477d8fa55925e86841fc4fc8.yaml deleted file mode 100644 index c3d5ea6457..0000000000 --- a/changes/32327cef477d8fa55925e86841fc4fc8.yaml +++ /dev/null @@ -1,6 +0,0 @@ ---- -desc: Fixed an issue where invalid dictionary constructor values would result in unhandled - Python ``AttributeError`` exceptions leaking into the Storm runtime. -prs: [] -type: bug -... diff --git a/changes/4a4fe1c2eb09b8a712af72dc6ce41c38.yaml b/changes/4a4fe1c2eb09b8a712af72dc6ce41c38.yaml deleted file mode 100644 index 2d77bd4367..0000000000 --- a/changes/4a4fe1c2eb09b8a712af72dc6ce41c38.yaml +++ /dev/null @@ -1,8 +0,0 @@ ---- -desc: 'Added ``alts`` definitions to the following forms: ``geo:place``, ``it:prod:soft``, - ``it:prod:softver``, ``ou:campaign``, ``ou:conference``, ``ou:goal``, ``ou:industry``, - ``pol:country``, ``ps:contact``, ``ps:person``, ``risk:threat``, ``risk:tool:software``, - and ``risk:vuln``.' -prs: [] -type: model -... diff --git a/changes/7a4e69c153170cce7ed678912c3857a2.yaml b/changes/7a4e69c153170cce7ed678912c3857a2.yaml deleted file mode 100644 index 44de852536..0000000000 --- a/changes/7a4e69c153170cce7ed678912c3857a2.yaml +++ /dev/null @@ -1,6 +0,0 @@ ---- -desc: Fixed an issue in Storm where attempting to iterate a non-iterable object would - raise a Python exception rather than a StormRuntimeError. -prs: [] -type: bug -... diff --git a/changes/8221696241d0c8e0142d663ec70cc6da.yaml b/changes/8221696241d0c8e0142d663ec70cc6da.yaml deleted file mode 100644 index 08facb0633..0000000000 --- a/changes/8221696241d0c8e0142d663ec70cc6da.yaml +++ /dev/null @@ -1,6 +0,0 @@ ---- -desc: Updated the ``pkg.load`` Storm command to include an ``X-Synapse-Version`` HTTP header - in requests. -prs: [] -type: feat -... diff --git a/changes/b4642a502f49353787b4f6632a6a6566.yaml b/changes/b4642a502f49353787b4f6632a6a6566.yaml deleted file mode 100644 index c0558c9b79..0000000000 --- a/changes/b4642a502f49353787b4f6632a6a6566.yaml +++ /dev/null @@ -1,10 +0,0 @@ ---- -desc: Fixed an issue with the Storm loop and generator keywords, ``continue``, ``break``, - and ``stop``. Using these keywords outside of a loop or generator function will now - raise a ``StormRuntimeError`` exception. Using these keywords to tear down the Storm - runtime will now emit an ``err`` message with the type ``StormRuntimeError`` and a - message indicating the invalid use of the keywords. The use of these keywords or - ``$lib.exit()`` in ``$lib.cache.fixed`` callbacks will now raise a ``StormRuntimeError``. -prs: [] -type: bug -... diff --git a/changes/d4f3dc440ad8e7181a9a30cc4bea2c41.yaml b/changes/d4f3dc440ad8e7181a9a30cc4bea2c41.yaml deleted file mode 100644 index 2f293cfb0d..0000000000 --- a/changes/d4f3dc440ad8e7181a9a30cc4bea2c41.yaml +++ /dev/null @@ -1,5 +0,0 @@ ---- -desc: Deprecated ``$lib.text()``. Please use a list to append strings to, and then use ``$lib.str.join()`` to join them on demand. -prs: [] -type: deprecation -... diff --git a/changes/e1b7c7693e7454c32ca657a2bed734d5.yaml b/changes/e1b7c7693e7454c32ca657a2bed734d5.yaml deleted file mode 100644 index 9cb004fb88..0000000000 --- a/changes/e1b7c7693e7454c32ca657a2bed734d5.yaml +++ /dev/null @@ -1,5 +0,0 @@ ---- -desc: Added syntax for conditional node property edit operators in Storm. -prs: [] -type: feat -... diff --git a/changes/fd2d79b0daf0705278a48e86b15524c7.yaml b/changes/fd2d79b0daf0705278a48e86b15524c7.yaml deleted file mode 100644 index 2d96761501..0000000000 --- a/changes/fd2d79b0daf0705278a48e86b15524c7.yaml +++ /dev/null @@ -1,6 +0,0 @@ ---- -desc: Fixed bug in password complexity rules where setting a password to (null) or - None would fail. -prs: [] -type: bug -... diff --git a/changes/ff623ded36292878c995dfcf1874daf4.yaml b/changes/ff623ded36292878c995dfcf1874daf4.yaml deleted file mode 100644 index 8f3ecc2210..0000000000 --- a/changes/ff623ded36292878c995dfcf1874daf4.yaml +++ /dev/null @@ -1,5 +0,0 @@ ---- -desc: Fixed a Cortex cron scheduler loop error during a mirror promotion. -prs: [] -type: bug -... diff --git a/docs/synapse/userguides/model_updates/update_v2_194_0.rst b/docs/synapse/userguides/model_updates/update_v2_194_0.rst new file mode 100644 index 0000000000..decd10fd96 --- /dev/null +++ b/docs/synapse/userguides/model_updates/update_v2_194_0.rst @@ -0,0 +1,191 @@ + + +.. _userguide_model_v2_194_0: + +###################### +v2.194.0 Model Updates +###################### + +The following model updates were made during the ``v2.194.0`` Synapse release. + +************** +New Properties +************** + +``inet:service:message`` + The form had the following property added to it: + + ``repost`` + The original message reposted by this message. + + +``it:prod:soft`` + The form had the following property added to it: + + ``id`` + An ID for the software. + + +``ps:contact`` + The form had the following property added to it: + + ``bio`` + A brief bio provided for the contact. + + + +****************** +Updated Properties +****************** + +``geo:place`` + The form had the following property updated: + + + The property ``name`` had the alternative property names added to its definition. + + +``it:prod:soft`` + The form had the following property updated: + + + The property ``name`` had the alternative property names added to its definition. + + +``it:prod:softver`` + The form had the following property updated: + + + The property ``name`` had the alternative property names added to its definition. + + +``ou:campaign`` + The form had the following properties updated: + + + The property ``goal`` had the alternative property names added to its definition. + + + The property ``name`` had the alternative property names added to its definition. + + +``ou:conference`` + The form had the following property updated: + + + The property ``name`` had the alternative property names added to its definition. + + +``ou:goal`` + The form had the following property updated: + + + The property ``name`` had the alternative property names added to its definition. + + +``ou:industry`` + The form had the following property updated: + + + The property ``name`` had the alternative property names added to its definition. + + +``pol:country`` + The form had the following property updated: + + The property ``name`` had the alternative property names added to its definition. + + +``ps:contact`` + The form had the following properties updated: + + + The property ``email`` had the alternative property names added to its definition. + + + The property ``id:number`` had the alternative property names added to its + definition. + + + The property ``lang`` had the alternative property names added to its definition. + + + The property ``name`` had the alternative property names added to its definition. + + + The property ``orgname`` had the alternative property names added to its definition. + + + The property ``title`` had the alternative property names added to its definition. + + + The property ``user`` had the alternative property names added to its definition. + + +``ps:person`` + The form had the following property updated: + + + The property ``name`` had the alternative property names added to its definition. + + +``risk:threat`` + The form had the following property updated: + + + The property ``org:name`` had the alternative property names added to its + definition. + + +``risk:tool:software`` + The form had the following property updated: + + + The property ``soft:name`` had the alternative property names added to its + definition. + + +``risk:vuln`` + The form had the following property updated: + + + The property ``name`` had the alternative property names added to its definition. + + +``tel:mob:telem`` + The form had the following property updated: + + + The property ``adid`` had the following docstring added: + + The advertising ID of the mobile telemetry sample. + + + +**************** +Deprecated Types +**************** + +The following forms have been marked as deprecated: + + +* ``it:os:android:aaid`` +* ``it:os:ios:idfa`` + + + +********************* +Deprecated Properties +********************* + +``tel:mob:telem`` + The form had the following properties deprecated: + + + ``aaid`` + Deprecated. Please use ``:adid``. + + + ``idfa`` + Deprecated. Please use ``:adid``. + diff --git a/synapse/tools/changelog.py b/synapse/tools/changelog.py index 1180ad309b..2ed9968a96 100644 --- a/synapse/tools/changelog.py +++ b/synapse/tools/changelog.py @@ -227,12 +227,11 @@ def _compareForms(self, curv, oldv, outp: s_output.OutPut) -> dict: if nkeys - okeys: # We've added a key to the prop def. - raise s_exc.NoSuchImpl(mesg='Have not implemented support for a prop def having a key added') + updated_props[prop] = {'type': 'addkey', 'keys': list(nkeys - okeys)} if okeys - nkeys: # We've removed a key from the prop def. updated_props[prop] = {'type': 'delkey', 'keys': list(okeys - nkeys)} - continue # Check if type change happened, we'll want to document that. ctyp = cpinfo.get('type') @@ -334,12 +333,11 @@ def _compareIfaces(self, curv, oldv, outp: s_output.OutPut) -> dict: if nkeys - okeys: # We've added a key to the prop def. - raise s_exc.NoSuchImpl(mesg='Have not implemented support for a prop def having a key added') + updated_props[prop] = {'type': 'addkey', 'keys': list(nkeys - okeys)} if okeys - nkeys: # We've removed a key from the prop def. updated_props[prop] = {'type': 'delkey', 'keys': list(okeys - nkeys)} - continue # Check if type change happened, we'll want to document that. ctyp = cpinfo.get('type') @@ -660,6 +658,8 @@ def _gen_model_rst(version, model_ref, changes, current_model, outp: s_output.Ou f' to {pnfo.get("new_type")}.' elif ptyp == 'delkey': mesg = f'The property ``{prop}`` had the ``{pnfo.get("keys")}`` keys removed from its definition.' + elif ptyp == 'addkey': + mesg = f'The property ``{prop}`` had the ``{pnfo.get("keys")}`` keys added to its definition.' else: raise s_exc.NoSuchImpl(mesg=f'pnfo.type={ptyp} not supported.') lines = [ @@ -677,6 +677,8 @@ def _gen_model_rst(version, model_ref, changes, current_model, outp: s_output.Ou f' to {pnfo.get("new_type")}.' elif ptyp == 'delkey': mesg = f'The property ``{prop}`` had the ``{pnfo.get("keys")}`` keys removed from its definition.' + elif ptyp == 'addkey': + mesg = f'The property ``{prop}`` had the ``{pnfo.get("keys")}`` keys added to its definition.' else: raise s_exc.NoSuchImpl(mesg=f'pnfo.type={ptyp} not supported.') From 1c22144f1d33060c228aaa2a1566508d83bba1ca Mon Sep 17 00:00:00 2001 From: epiphyte Date: Thu, 16 Jan 2025 18:25:59 -0500 Subject: [PATCH 17/19] Add missing whitespace to changelog header. --- CHANGELOG.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index e4270249b3..90834914af 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -5,6 +5,7 @@ ***************** Synapse Changelog ***************** + v2.194.0 - 2025-01-16 ===================== From fb479d376805f963f11c5029310325c13ef2c883 Mon Sep 17 00:00:00 2001 From: epiphyte Date: Thu, 16 Jan 2025 18:26:08 -0500 Subject: [PATCH 18/19] =?UTF-8?q?Bump=20version:=202.193.0=20=E2=86=92=202?= =?UTF-8?q?.194.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- pyproject.toml | 2 +- synapse/lib/version.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 89dfaa784a..9564022df0 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 2.193.0 +current_version = 2.194.0 commit = True tag = True tag_message = diff --git a/pyproject.toml b/pyproject.toml index e3046ff69c..676fecdad9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = 'setuptools.build_meta' [project] name = 'synapse' -version = '2.193.0' +version = '2.194.0' authors = [ { name = 'The Vertex Project LLC', email = 'root@vertex.link'}, ] diff --git a/synapse/lib/version.py b/synapse/lib/version.py index 2323c8728f..4dd026471a 100644 --- a/synapse/lib/version.py +++ b/synapse/lib/version.py @@ -223,6 +223,6 @@ def reqVersion(valu, reqver, ############################################################################## # The following are touched during the release process by bumpversion. # Do not modify these directly. -version = (2, 193, 0) +version = (2, 194, 0) verstring = '.'.join([str(x) for x in version]) commit = '' From ace3727bdb30e1cc2cfb146ddbd537a54900d9c8 Mon Sep 17 00:00:00 2001 From: epiphyte Date: Thu, 16 Jan 2025 18:26:53 -0500 Subject: [PATCH 19/19] Add model ref for v2.194.0 --- ...d376805f963f11c5029310325c13ef2c883.yaml.gz | Bin 0 -> 127390 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 changes/modelrefs/model_2.194.0_fb479d376805f963f11c5029310325c13ef2c883.yaml.gz diff --git a/changes/modelrefs/model_2.194.0_fb479d376805f963f11c5029310325c13ef2c883.yaml.gz b/changes/modelrefs/model_2.194.0_fb479d376805f963f11c5029310325c13ef2c883.yaml.gz new file mode 100644 index 0000000000000000000000000000000000000000..4b104e0148b61f8676851b4561a2bef7826172bc GIT binary patch literal 127390 zcmV)8K*qlxiwFR!m5FBp|LnbMliaqFHu!yh1>=0!vEQ{UsU@}T+ITmjk7Uo$JGQKm z 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

>IM`YcaB;YQ!FwM@$wH$R&f+Q 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%0AtWVC7M$+;+fyM{lp|8%j!yNvoj}6 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 cHLEH9X