From c17a6e167e5eada68aa4dcf7dfccacf8ce235351 Mon Sep 17 00:00:00 2001 From: Arpit Jain Date: Mon, 29 Jun 2026 11:53:28 +0900 Subject: [PATCH] Fix KeyError: 'id' in io filters _normalize on name-only choice lists Some filter choice lists (e.g. the target_group filter) carry items with only a 'name' key, not 'value' or 'id'. _normalize assumed 'id' as the fallback key and raised KeyError: 'id', which broke tio.assets.bulk_delete() on tenants that still have target groups. Pick the first key that actually exists ('value', 'id', then 'name') and skip items missing it. Fixes #1009 Signed-off-by: Arpit Jain --- tenable/io/filters.py | 18 ++++++++++++++---- tests/io/test_filters.py | 21 +++++++++++++++++++++ 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/tenable/io/filters.py b/tenable/io/filters.py index 456165f75..df0e89622 100644 --- a/tenable/io/filters.py +++ b/tenable/io/filters.py @@ -44,10 +44,20 @@ def _normalize(self, filterset: list[dict[str, Any]]) -> dict[str, Any]: # is a list of dictionary items, and in other cases the "list" # is a list of string values. if isinstance(item['control']['list'][0], dict): - key = 'value' if 'value' in item['control']['list'][0] else 'id' - datablock['choices'] = [ - str(i[key]) for i in item['control']['list'] - ] + # The list items don't always carry the same key. Most use + # 'value' or 'id', but some (e.g. the target_group filter) + # only provide a 'name'. Pick the first key that exists + # rather than assuming 'id', which raised KeyError before. + first = item['control']['list'][0] + key = next( + (k for k in ('value', 'id', 'name') if k in first), None + ) + if key is not None: + datablock['choices'] = [ + str(i[key]) + for i in item['control']['list'] + if key in i + ] elif isinstance(item['control']['list'], list): datablock['choices'] = [str(i) for i in item['control']['list']] if 'regex' in item['control']: diff --git a/tests/io/test_filters.py b/tests/io/test_filters.py index 1f5215064..089631b1e 100644 --- a/tests/io/test_filters.py +++ b/tests/io/test_filters.py @@ -167,3 +167,24 @@ def test_filters_credentials_false_filters(api): check(data[1], 'readable_name', str, allow_none=True) check(data[1], 'operators', list, allow_none=True) check(data[1], 'control', dict, allow_none=True) + + +def test_normalize_name_only_choices(api): + """ + test that _normalize handles list items that only carry a 'name' key + (e.g. the target_group filter) instead of raising KeyError: 'id' + """ + filterset = [ + { + 'name': 'target_group', + 'operators': ['eq', 'neq'], + 'control': { + 'type': 'dropdown_multi', + 'list': [{'name': 'mygroup'}], + }, + } + ] + result = getattr(api.filters, '_normalize')(filterset) + assert result['target_group']['choices'] == ['mygroup'] + assert result['target_group']['operators'] == ['eq', 'neq'] + assert result['target_group']['pattern'] is None