From 412a0ab8afcc5c5fcac38ce61b7f58d1a09dee3c Mon Sep 17 00:00:00 2001 From: Riley V Date: Tue, 2 Sep 2025 13:43:43 +1000 Subject: [PATCH 01/11] extra attribs retrievable yeehaw --- qualysapi/api_actions.py | 10 +++++++++- qualysapi/api_objects.py | 6 +++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/qualysapi/api_actions.py b/qualysapi/api_actions.py index 48d1d33..39ebe04 100644 --- a/qualysapi/api_actions.py +++ b/qualysapi/api_actions.py @@ -621,7 +621,6 @@ def listAppliances(self): def getTag(self, tag_name=None,tag_id=None): #TODO: fix recursion where multiple layers of child tags exist #TODO: enable searching by all types of search parameters through arguments passed - #TODO: retrieve additional attributes such as rule for dynamic tags, criticality call = "search/am/tag" if (tag_name is not None) and (tag_id is not None): logger.error('Error: unable to search, both tag name and id provided') @@ -641,6 +640,9 @@ def getTag(self, tag_name=None,tag_id=None): item_data["colour"] = item.find("color").text if item.find('color') is not None else None item_data['description'] = item.find('description').text if item.find('description') is not None else None item_data['has_children'] = item.find('children') if item.find('children') is not None else None + item_data['rule_type'] = item.find('ruleType').text if item.find('ruleType') is not None else None + item_data['rule_value'] = item.find('ruleText').text if item.find('ruleText') is not None else None + item_data['criticality'] = item.find('criticalityScore').text if item.find('criticalityScore') is not None else None if item_data['has_children'] is not None: self.child_tags_list = [] for list in tree.iter('children'): @@ -658,6 +660,9 @@ def getTag(self, tag_name=None,tag_id=None): modified=item_data["modified"], description=item_data['description'], child_tags=self.child_tags_list, + criticality=item_data['criticality'], + dynamic=item_data['rule_type'], + dynamic_rule=item_data['rule_value'], ) else: return Tag( @@ -667,6 +672,9 @@ def getTag(self, tag_name=None,tag_id=None): created=item_data["created"], modified=item_data["modified"], description=item_data['description'], + criticality=item_data['criticality'], + dynamic=item_data['rule_type'], + dynamic_rule=item_data['rule_value'], ) if items_found > 1: #TODO: return multiple tags for name-based search? diff --git a/qualysapi/api_objects.py b/qualysapi/api_objects.py index bd74826..5ceb338 100644 --- a/qualysapi/api_objects.py +++ b/qualysapi/api_objects.py @@ -1,5 +1,5 @@ import datetime - +import zoneinfo as TZ # from lxml import objectify @@ -219,13 +219,13 @@ def __init__(self, name: str, id: int, colour: str, created:str, modified:str,ch date = process_created[0].split("-") time = process_created[1].split(":") self.created = datetime.datetime( - int(date[0]), int(date[1]), int(date[2]), int(time[0]), int(time[1]), int(time[2]) + int(date[0]), int(date[1]), int(date[2]), int(time[0]), int(time[1]), int(time[2]), tzinfo=TZ.ZoneInfo("UTC") #Qualys defaults to UTC it seems ) process_modified = str(modified).replace("T", " ").replace("Z", "").split(" ") date = process_modified[0].split("-") time = process_modified[1].split(":") self.modified = datetime.datetime( - int(date[0]), int(date[1]), int(date[2]), int(time[0]), int(time[1]), int(time[2]) + int(date[0]), int(date[1]), int(date[2]), int(time[0]), int(time[1]), int(time[2]), tzinfo=TZ.ZoneInfo("UTC") #Qualys defaults to UTC it seems ) self.description = description self.child_tags = child_tags From fe32cb3e4fa88c257b5fa17065c935608f1d098e Mon Sep 17 00:00:00 2001 From: Riley V Date: Tue, 2 Sep 2025 13:55:17 +1000 Subject: [PATCH 02/11] better type hinting --- qualysapi/api_actions.py | 46 ++++++++++++++++++++-------------------- qualysapi/api_objects.py | 2 +- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/qualysapi/api_actions.py b/qualysapi/api_actions.py index 39ebe04..0742ac8 100644 --- a/qualysapi/api_actions.py +++ b/qualysapi/api_actions.py @@ -618,7 +618,7 @@ def listAppliances(self): return scanner_array - def getTag(self, tag_name=None,tag_id=None): + def getTag(self, tag_name: str | None = None,tag_id: int | None = None): #TODO: fix recursion where multiple layers of child tags exist #TODO: enable searching by all types of search parameters through arguments passed call = "search/am/tag" @@ -633,24 +633,24 @@ def getTag(self, tag_name=None,tag_id=None): tree =tagData.find('data') for item in tree.findall('Tag'): item_data = {} - item_data["id"] = item.find("id").text if item.find('id') is not None else None - item_data["name"] = item.find("name").text if item.find('name') is not None else None - item_data["created"] = item.find("created").text if item.find('created') is not None else None - item_data["modified"] = item.find("modified").text if item.find('modified') is not None else None - item_data["colour"] = item.find("color").text if item.find('color') is not None else None - item_data['description'] = item.find('description').text if item.find('description') is not None else None - item_data['has_children'] = item.find('children') if item.find('children') is not None else None - item_data['rule_type'] = item.find('ruleType').text if item.find('ruleType') is not None else None - item_data['rule_value'] = item.find('ruleText').text if item.find('ruleText') is not None else None - item_data['criticality'] = item.find('criticalityScore').text if item.find('criticalityScore') is not None else None - if item_data['has_children'] is not None: + item_data["id"] = int(item.find("id").text) if item.find('id') is not None else None + item_data["name"] = str(item.find("name").text) if item.find('name') is not None else None + item_data["created"] = str(item.find("created").text) if item.find('created') is not None else None + item_data["modified"] = str(item.find("modified").text) if item.find('modified') is not None else None + item_data["colour"] = str(item.find("color").text) if item.find('color') is not None else None + item_data['description'] = str(item.find('description').text) if item.find('description') is not None else None + item_data['has_children'] = True if item.find('children') is not None else False + item_data['rule_type'] = str(item.find('ruleType').text) if item.find('ruleType') is not None else None + item_data['rule_value'] = str(item.find('ruleText').text) if item.find('ruleText') is not None else None + item_data['criticality'] = int(item.find('criticalityScore').text) if item.find('criticalityScore') is not None else None + if item_data['has_children']: self.child_tags_list = [] for list in tree.iter('children'): for items in list.iter('list'): for tags in items.iter('TagSimple'): - single_tag = tags.find('id').text if item.find('id') is not None else None + single_tag = int(tags.find('id').text) if item.find('id') is not None else None if single_tag is not None: - tag = self.getTag(id=single_tag) + tag = self.getTag(tag_id=single_tag) self.child_tags_list.append(tag) return Tag( name=item_data["name"], @@ -686,22 +686,22 @@ def getTag(self, tag_name=None,tag_id=None): logger.warning(f'Warning: unable to find tag: {value}') return None - def editTag(self, tag: Tag, new_name=None, new_colour=None): + def editTag(self, tag: Tag, name: str | None = None, colour: str | None = None, criticality: int | str | None = None,ruleType: str | None = None,ruleText: str | None = None): #TODO: allow passing attributes as dict of attributes #TODO: add additional editable attributes to function call = f'update/am/tag/{str(tag.id)}' - if new_colour is not None: + if colour is not None: colour_validation = re.compile(r'#([A-Fa-f0-9]){6}') - if not colour_validation.fullmatch(new_colour): - logger.error(f'Error: colour is not valid hex code: {new_colour}') + if not colour_validation.fullmatch(colour): + logger.error(f'Error: colour is not valid hex code: {colour}') return None else: - if new_name is not None: - parameters = f"""{new_name}{new_colour}""" + if name is not None: + parameters = f"""{name}{colour}""" else: - parameters = f"""{new_colour}""" - elif (new_name is not None) and (new_colour is None): - parameters = f"""{new_name}""" + parameters = f"""{colour}""" + elif (name is not None) and (colour is None): + parameters = f"""{name}""" else: logger.error('Error: Colour and name both None') return None diff --git a/qualysapi/api_objects.py b/qualysapi/api_objects.py index 5ceb338..d8cd96d 100644 --- a/qualysapi/api_objects.py +++ b/qualysapi/api_objects.py @@ -210,7 +210,7 @@ def __init__(self, id: int, uuid: str, name: str, network_id: int, software_vers self.status = status class Tag: - def __init__(self, name: str, id: int, colour: str, created:str, modified:str,child_tags=None,description=None,criticality=None,dynamic=None,dynamic_rule=None): + def __init__(self, name: str, id: int, colour: str, created:str, modified:str,child_tags: list | None = None,description: str | None = None,criticality: int | None = None,dynamic: str | None = None,dynamic_rule: str | None = None): self.name = str(name) self.id = int(id) self.colour = str(colour) From e62528d08623b254fa94d21d0e747e58108e0c84 Mon Sep 17 00:00:00 2001 From: Riley V Date: Mon, 15 Sep 2025 16:19:26 +1000 Subject: [PATCH 03/11] add criticality, description as editable/creatable fields, rejig createTag and editTag to build parameters instead of big blob --- qualysapi/api_actions.py | 38 ++++++++++++++++++++++---------------- qualysapi/api_objects.py | 7 ++----- 2 files changed, 24 insertions(+), 21 deletions(-) diff --git a/qualysapi/api_actions.py b/qualysapi/api_actions.py index 0742ac8..3f780f3 100644 --- a/qualysapi/api_actions.py +++ b/qualysapi/api_actions.py @@ -661,7 +661,7 @@ def getTag(self, tag_name: str | None = None,tag_id: int | None = None): description=item_data['description'], child_tags=self.child_tags_list, criticality=item_data['criticality'], - dynamic=item_data['rule_type'], + rule_type=item_data['rule_type'], dynamic_rule=item_data['rule_value'], ) else: @@ -673,7 +673,7 @@ def getTag(self, tag_name: str | None = None,tag_id: int | None = None): modified=item_data["modified"], description=item_data['description'], criticality=item_data['criticality'], - dynamic=item_data['rule_type'], + rule_type=item_data['rule_type'], dynamic_rule=item_data['rule_value'], ) if items_found > 1: @@ -686,25 +686,25 @@ def getTag(self, tag_name: str | None = None,tag_id: int | None = None): logger.warning(f'Warning: unable to find tag: {value}') return None - def editTag(self, tag: Tag, name: str | None = None, colour: str | None = None, criticality: int | str | None = None,ruleType: str | None = None,ruleText: str | None = None): - #TODO: allow passing attributes as dict of attributes + def editTag(self, tag: Tag, name: str | None = None, colour: str | None = None, criticality: int | None = None,rule_type: str | None = None,rule_text: str | None = None,child_tags: list | None = None,child_tag_action: str | None = None,description: str | None = None): + #TODO: allow passing attributes as dict of attributes? #TODO: add additional editable attributes to function call = f'update/am/tag/{str(tag.id)}' + parameters = """""" if colour is not None: colour_validation = re.compile(r'#([A-Fa-f0-9]){6}') if not colour_validation.fullmatch(colour): logger.error(f'Error: colour is not valid hex code: {colour}') return None else: - if name is not None: - parameters = f"""{name}{colour}""" - else: - parameters = f"""{colour}""" - elif (name is not None) and (colour is None): - parameters = f"""{name}""" - else: - logger.error('Error: Colour and name both None') - return None + parameters += f"""{colour}""" + if name is not None: + parameters +=f"""{name}""" + if criticality is not None and int(criticality) < 6 and int(criticality) > 0: + parameters +=f"""{int(criticality)}""" + if description is not None: + parameters += f"""{description}""" + parameters += """""" tagData = ET.fromstring(self.request(api_call=call,http_method="POST",data=parameters,api_version="gav").encode("utf-8")) for item in tagData.findall('responseCode'): if item.text == 'SUCCESS': @@ -717,9 +717,9 @@ def editTag(self, tag: Tag, name: str | None = None, colour: str | None = None, logger.error('Error: Tag failed to update') return None - def createTag(self, name: str, colour=None): + def createTag(self, name: str, colour: str | None = None, criticality: int | None = None,rule_type: str | None = None,rule_text: str | None = None,child_tags: list | None = None,child_tag_action: str | None = None,description: str | None = None): #TODO: Validation of creation - #TODO: ability to create tags with criticality, child tags, dynamic rules + #TODO: ability to create tags with child tags, dynamic rules #TODO: allow passing attributes for tag as dict of attribs call = 'create/am/tag' colour_validation = re.compile(r'#([A-Fa-f0-9]){6}') @@ -728,7 +728,13 @@ def createTag(self, name: str, colour=None): if not colour_validation.fullmatch(colour): logger.error(f'Error: colour provided is not valid hex code: {colour}') return None - parameters = f"""{name}{colour}""" + parameters = f"""{name}""" + if criticality is not None and int(criticality) < 6 and int(criticality) > 0: + parameters +=f"""{int(criticality)}""" + if description is not None: + parameters += f"""{description}""" + parameters += f"""""" + tagData = ET.fromstring(self.request(api_call=call,http_method="POST",data=parameters,api_version="gav").encode("utf-8")) for item in tagData.findall('responseCode'): if item.text == 'SUCCESS': diff --git a/qualysapi/api_objects.py b/qualysapi/api_objects.py index d8cd96d..f8c8f83 100644 --- a/qualysapi/api_objects.py +++ b/qualysapi/api_objects.py @@ -1,8 +1,5 @@ import datetime import zoneinfo as TZ -# from lxml import objectify - - class Host: def __init__(self, dns, id, host_type,created,modified,ip=None,last_scan=None,tags=None): self.dns = str(dns) @@ -210,7 +207,7 @@ def __init__(self, id: int, uuid: str, name: str, network_id: int, software_vers self.status = status class Tag: - def __init__(self, name: str, id: int, colour: str, created:str, modified:str,child_tags: list | None = None,description: str | None = None,criticality: int | None = None,dynamic: str | None = None,dynamic_rule: str | None = None): + def __init__(self, name: str, id: int, colour: str, created:str, modified:str,child_tags: list | None = None,description: str | None = None,criticality: int | None = None,rule_type: str | None = None,dynamic_rule: str | None = None): self.name = str(name) self.id = int(id) self.colour = str(colour) @@ -229,7 +226,7 @@ def __init__(self, name: str, id: int, colour: str, created:str, modified:str,ch ) self.description = description self.child_tags = child_tags - self.dynamic = dynamic + self.rule_type = rule_type self.dynamic_rule = dynamic_rule self.criticality = criticality def __repr__(self): From eea4b8010fa1d9de1b2b217a4ef0cf83a8463adf Mon Sep 17 00:00:00 2001 From: Riley V Date: Thu, 2 Oct 2025 11:24:29 +1000 Subject: [PATCH 04/11] scaffolding of rule validation for dynamic tag rules --- qualysapi/api_actions.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/qualysapi/api_actions.py b/qualysapi/api_actions.py index 3f780f3..e97f062 100644 --- a/qualysapi/api_actions.py +++ b/qualysapi/api_actions.py @@ -10,6 +10,20 @@ child_tags_list = None logger = logging.getLogger(__name__) class QGActions: + def ruleValidator(self, rule_type: str, rule_body: str): + #TODO: validator for some of the rule types + match rule_type: + case 'network_range': + #IP range validation + print(rule_body) + case 'gav': + #idk honestly, but somehow i can i guess??? + print(rule_body) + case 'asset_search': + #this needs to be xml ugh + print(rule_body) + + def getHost(self, host_name=None, host_id=None, verbose=False): if verbose: call = 'rest/2.0/get/am/asset' @@ -712,7 +726,7 @@ def editTag(self, tag: Tag, name: str | None = None, colour: str | None = None, tag_id = None for item in tree.findall('Tag'): tag_id = item.find('id').text if item.find('id') is not None else None - return self.getTag(tag_id=tag_id) + return self.getTag(tag_id=tag_id) #required as returned tag on success does not contain all necessary attributes else: logger.error('Error: Tag failed to update') return None From 830c9f38f1c7a995de50fb4073933518901a96a2 Mon Sep 17 00:00:00 2001 From: Riley V Date: Thu, 2 Oct 2025 14:35:12 +1000 Subject: [PATCH 05/11] we do a little validation (of dynamic rules) --- qualysapi/api_actions.py | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/qualysapi/api_actions.py b/qualysapi/api_actions.py index e97f062..b139720 100644 --- a/qualysapi/api_actions.py +++ b/qualysapi/api_actions.py @@ -2,6 +2,7 @@ import time from urllib import parse as urlparse import re +import ipaddress # from lxml import objectify import xml.etree.ElementTree as ET from qualysapi.api_objects import * @@ -11,16 +12,22 @@ logger = logging.getLogger(__name__) class QGActions: def ruleValidator(self, rule_type: str, rule_body: str): - #TODO: validator for some of the rule types + #TODO: validator for the harder rule types match rule_type: - case 'network_range': + case 'NETWORK_RANGE': #IP range validation - print(rule_body) - case 'gav': + addresses = rule_body.split('-') + valid_addresses = 0 + for address in addresses: + try: + ip = ipaddress.IPv4Address(address) + valid_addresses += 1 + except ValueError: + return False + return valid_addresses > 0 + case 'GLOBAL_ASSET_VIEW': #idk honestly, but somehow i can i guess??? - print(rule_body) - case 'asset_search': - #this needs to be xml ugh + return True #testing lol print(rule_body) @@ -718,6 +725,12 @@ def editTag(self, tag: Tag, name: str | None = None, colour: str | None = None, parameters +=f"""{int(criticality)}""" if description is not None: parameters += f"""{description}""" + if rule_type is not None and rule_text is not None: + if rule_type in ("STATIC","GROOVY","OS_REGEX","NETWORK_RANGE","NAME_CONTAINS","INSTALLED_SOFTWARE","OPEN_PORTS","VULN_EXIST","ASSET_SEARCH","CLOUD_ASSET","BUSINESS_INFORMATION","GLOBAL_ASSET_VIEW","NETWORK_RANGE","TAG_SET") and self.ruleValidator(rule_type,rule_text): + parameters += f"""{rule_type}{rule_text}""" + else: + logger.error(f'Error: Rule could not be validated: {rule_type} with value {rule_text}') + return None parameters += """""" tagData = ET.fromstring(self.request(api_call=call,http_method="POST",data=parameters,api_version="gav").encode("utf-8")) for item in tagData.findall('responseCode'): From 36fdd243fa29b032dbcaa68182ac7b7fde510e27 Mon Sep 17 00:00:00 2001 From: Riley V Date: Thu, 2 Oct 2025 14:42:00 +1000 Subject: [PATCH 06/11] i forgor to add to the create function too, let's fix that --- qualysapi/api_actions.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/qualysapi/api_actions.py b/qualysapi/api_actions.py index b139720..74b2116 100644 --- a/qualysapi/api_actions.py +++ b/qualysapi/api_actions.py @@ -760,6 +760,12 @@ def createTag(self, name: str, colour: str | None = None, criticality: int | Non parameters +=f"""{int(criticality)}""" if description is not None: parameters += f"""{description}""" + if rule_type is not None and rule_text is not None: + if rule_type in ("STATIC","GROOVY","OS_REGEX","NETWORK_RANGE","NAME_CONTAINS","INSTALLED_SOFTWARE","OPEN_PORTS","VULN_EXIST","ASSET_SEARCH","CLOUD_ASSET","BUSINESS_INFORMATION","GLOBAL_ASSET_VIEW","NETWORK_RANGE","TAG_SET") and self.ruleValidator(rule_type,rule_text): + parameters += f"""{rule_type}{rule_text}""" + else: + logger.error(f'Error: Rule could not be validated: {rule_type} with value {rule_text}') + return None parameters += f"""""" tagData = ET.fromstring(self.request(api_call=call,http_method="POST",data=parameters,api_version="gav").encode("utf-8")) From c6ccc33e6b45d39d9e13820c1d7859ecaf4646ac Mon Sep 17 00:00:00 2001 From: Riley V Date: Thu, 2 Oct 2025 14:53:39 +1000 Subject: [PATCH 07/11] minor rework to tag creation return value, update todo --- qualysapi/api_actions.py | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/qualysapi/api_actions.py b/qualysapi/api_actions.py index 74b2116..f7d2806 100644 --- a/qualysapi/api_actions.py +++ b/qualysapi/api_actions.py @@ -709,7 +709,7 @@ def getTag(self, tag_name: str | None = None,tag_id: int | None = None): def editTag(self, tag: Tag, name: str | None = None, colour: str | None = None, criticality: int | None = None,rule_type: str | None = None,rule_text: str | None = None,child_tags: list | None = None,child_tag_action: str | None = None,description: str | None = None): #TODO: allow passing attributes as dict of attributes? - #TODO: add additional editable attributes to function + #TODO: child tags call = f'update/am/tag/{str(tag.id)}' parameters = """""" if colour is not None: @@ -745,8 +745,7 @@ def editTag(self, tag: Tag, name: str | None = None, colour: str | None = None, return None def createTag(self, name: str, colour: str | None = None, criticality: int | None = None,rule_type: str | None = None,rule_text: str | None = None,child_tags: list | None = None,child_tag_action: str | None = None,description: str | None = None): - #TODO: Validation of creation - #TODO: ability to create tags with child tags, dynamic rules + #TODO: ability to create tags with child tags #TODO: allow passing attributes for tag as dict of attribs call = 'create/am/tag' colour_validation = re.compile(r'#([A-Fa-f0-9]){6}') @@ -773,19 +772,8 @@ def createTag(self, name: str, colour: str | None = None, criticality: int | Non if item.text == 'SUCCESS': tree =tagData.find('data') for item in tree.findall('Tag'): - item_data = {} - item_data["id"] = item.find("id").text if item.find('id') is not None else None - item_data["name"] = item.find("name").text if item.find('name') is not None else None - item_data["created"] = item.find("created").text if item.find('created') is not None else None - item_data["modified"] = item.find("modified").text if item.find('modified') is not None else None - item_data["colour"] = item.find("color").text if item.find('color') is not None else None - return Tag( - item_data["name"], - item_data["id"], - item_data["colour"], - item_data["created"], - item_data["modified"], - ) + id = int(item.find("id").text) if item.find('id') is not None else None + return self.getTag(tag_id=id) else: logger.error(f'Error: unable to create tag: {name}') return None From 370662deae87d80ff1a9401f7aae1bc030bb50a5 Mon Sep 17 00:00:00 2001 From: Riley V Date: Thu, 2 Oct 2025 15:46:24 +1000 Subject: [PATCH 08/11] tags (mostly) done --- qualysapi/api_actions.py | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/qualysapi/api_actions.py b/qualysapi/api_actions.py index f7d2806..f9fd8f6 100644 --- a/qualysapi/api_actions.py +++ b/qualysapi/api_actions.py @@ -731,6 +731,23 @@ def editTag(self, tag: Tag, name: str | None = None, colour: str | None = None, else: logger.error(f'Error: Rule could not be validated: {rule_type} with value {rule_text}') return None + if child_tag_action in ('set','remove') and child_tags is not None: + if child_tag_action == 'set': + parameters += f"""""" + for item in child_tags: + if type(item) == str: #Creates a new tag (thanks API doco for not mentioning this) + parameters += f"""{item}""" + elif type(item) == Tag: #Adds an existing tag as a child + parameters += f"""{item.id}""" + parameters += f"""""" + if child_tag_action == 'remove': + parameters += f"""""" + for item in child_tags: + if type(item) == Tag: + parameters += f"""{item.id}""" + elif type(item) == int: + parameters += f"""{int(item)}""" + parameters += f"""""" parameters += """""" tagData = ET.fromstring(self.request(api_call=call,http_method="POST",data=parameters,api_version="gav").encode("utf-8")) for item in tagData.findall('responseCode'): @@ -744,7 +761,7 @@ def editTag(self, tag: Tag, name: str | None = None, colour: str | None = None, logger.error('Error: Tag failed to update') return None - def createTag(self, name: str, colour: str | None = None, criticality: int | None = None,rule_type: str | None = None,rule_text: str | None = None,child_tags: list | None = None,child_tag_action: str | None = None,description: str | None = None): + def createTag(self, name: str, colour: str | None = None, criticality: int | None = None,rule_type: str | None = None,rule_text: str | None = None,child_tags: list | None = None,description: str | None = None): #TODO: ability to create tags with child tags #TODO: allow passing attributes for tag as dict of attribs call = 'create/am/tag' @@ -765,6 +782,14 @@ def createTag(self, name: str, colour: str | None = None, criticality: int | Non else: logger.error(f'Error: Rule could not be validated: {rule_type} with value {rule_text}') return None + if child_tags is not None: + parameters += f"""""" + for item in child_tags: + if type(item) == Tag: + parameters += f"""{item.id}""" + elif type(item) == str: + parameters += f"""{item}""" + parameters += f"""""" parameters += f"""""" tagData = ET.fromstring(self.request(api_call=call,http_method="POST",data=parameters,api_version="gav").encode("utf-8")) From f12364b5f4fec33b1414386c376823e88be5c021 Mon Sep 17 00:00:00 2001 From: Riley V Date: Mon, 6 Oct 2025 13:52:20 +1100 Subject: [PATCH 09/11] remove extra print debug statement --- qualysapi/connector.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qualysapi/connector.py b/qualysapi/connector.py index 18ad9a2..2ab470d 100644 --- a/qualysapi/connector.py +++ b/qualysapi/connector.py @@ -415,7 +415,7 @@ def request( # self.rate_limit_remaining[api_call], # ) response = request.text - print(response) + # print(response) # if ( # "1960" in response # and "This API cannot be run again until" in response From cb49dc14bba7f5d90624d2868cb5b95ffe59b404 Mon Sep 17 00:00:00 2001 From: Riley V Date: Mon, 6 Oct 2025 13:52:30 +1100 Subject: [PATCH 10/11] i think that's all for tags?? --- qualysapi/api_actions.py | 166 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 159 insertions(+), 7 deletions(-) diff --git a/qualysapi/api_actions.py b/qualysapi/api_actions.py index f9fd8f6..df1f0af 100644 --- a/qualysapi/api_actions.py +++ b/qualysapi/api_actions.py @@ -700,7 +700,7 @@ def getTag(self, tag_name: str | None = None,tag_id: int | None = None): if items_found > 1: #TODO: return multiple tags for name-based search? value = str(tag_id) if tag_id is not None else tag_name - logger.warning(f'Warning: multiple results returned for tag: {value}') + logger.warning(f'Warning: multiple results returned for tag: {value}, use findTags() instead') return None #for now... if items_found < 1: value = str(tag_id) if tag_id is not None else tag_name @@ -708,8 +708,6 @@ def getTag(self, tag_name: str | None = None,tag_id: int | None = None): return None def editTag(self, tag: Tag, name: str | None = None, colour: str | None = None, criticality: int | None = None,rule_type: str | None = None,rule_text: str | None = None,child_tags: list | None = None,child_tag_action: str | None = None,description: str | None = None): - #TODO: allow passing attributes as dict of attributes? - #TODO: child tags call = f'update/am/tag/{str(tag.id)}' parameters = """""" if colour is not None: @@ -771,7 +769,7 @@ def createTag(self, name: str, colour: str | None = None, criticality: int | Non if not colour_validation.fullmatch(colour): logger.error(f'Error: colour provided is not valid hex code: {colour}') return None - parameters = f"""{name}""" + parameters = f"""{name}{colour}""" if criticality is not None and int(criticality) < 6 and int(criticality) > 0: parameters +=f"""{int(criticality)}""" if description is not None: @@ -785,9 +783,9 @@ def createTag(self, name: str, colour: str | None = None, criticality: int | Non if child_tags is not None: parameters += f"""""" for item in child_tags: - if type(item) == Tag: + if type(item) == Tag: #this adds an existing tag as a child parameters += f"""{item.id}""" - elif type(item) == str: + elif type(item) == str: #this creates a tag with {name}, even if such a tag exists parameters += f"""{item}""" parameters += f"""""" parameters += f"""""" @@ -818,4 +816,158 @@ def deleteTag(self, tag: Tag): logger.error(f'Error: deletion failed for tag {tag.name}') return False else: - return False \ No newline at end of file + return False + + def findTags(self, criteria, operator, search_value): + call = "search/am/tag" + parameters= f"""""" + match criteria: + case 'id': + #I'm not really sure why you'd want to use this one like this but I'll build it anyway + logger.info(f'Criteria: {criteria}') + if operator in ('EQUALS','NOT EQUALS','GREATER','LESSER') and type(search_value) == int: + parameters += f"""{search_value}""" + elif type(search_value) == int: + logger.error(f'Error: invalid operator: {operator} for criteria: {criteria}') + return None + else: + logger.error(f'Error: invalid query \"{criteria} {operator} {search_value}\"') + case 'name': + #check operators + logger.info(f'Criteria: {criteria}') + if operator in ('EQUALS','NOT EQUALS','CONTAINS'): + parameters += f"""{search_value}""" + else: + logger.error(f'Error: invalid query \"{criteria} {operator} {search_value}\"') + case 'parent': + #check for id + logger.info(f'Criteria: {criteria}') + if type(search_value) == int: + parameters += f"""{search_value}""" + else: + logger.error(f'Error: invalid query \"{criteria} {operator} {search_value}\"') + case 'ruleType': + #check operators + logger.info(f'Criteria: {criteria}') + if criteria in ("STATIC","GROOVY","OS_REGEX","NETWORK_RANGE","NAME_CONTAINS","INSTALLED_SOFTWARE","OPEN_PORTS","VULN_EXIST","ASSET_SEARCH","CLOUD_ASSET","BUSINESS_INFORMATION","GLOBAL_ASSET_VIEW","NETWORK_RANGE","TAG_SET") and operator in ('EQUALS','NOT EQUALS'): + parameters += f"""{search_value}""" + else: + logger.error(f'Error: invalid query \"{criteria} {operator} {search_value}\"') + case 'provider': + #check against list of providers + logger.info(f'Criteria: {criteria}') + if criteria in ('EC2', 'AZURE', 'GCP', 'IBM', 'OCI', 'Alibaba') and operator in ('EQUALS','NOT EQUALS'): + parameters += f"""{search_value}""" + else: + logger.error(f'Error: invalid query \"{criteria} {operator} {search_value}\"') + case 'color': + logger.info(f'Criteria: {criteria}') + colour_validation = re.compile(r'#([A-Fa-f0-9]){6}') + if colour_validation.fullmatch(search_value) and operator in ('EQUALS','NOT EQUALS'): + parameters += f"""{search_value}""" + else: + logger.error(f'Error: invalid query \"{criteria} {operator} {search_value}\"') + parameters += f"""""" + + tagData = ET.fromstring(self.request(api_call=call,http_method="POST",data=parameters,api_version="gav").encode("utf-8")) + items_found = int(tagData.find('count').text) + if items_found == 1: + tree =tagData.find('data') + for item in tree.findall('Tag'): + item_data = {} + item_data["id"] = int(item.find("id").text) if item.find('id') is not None else None + item_data["name"] = str(item.find("name").text) if item.find('name') is not None else None + item_data["created"] = str(item.find("created").text) if item.find('created') is not None else None + item_data["modified"] = str(item.find("modified").text) if item.find('modified') is not None else None + item_data["colour"] = str(item.find("color").text) if item.find('color') is not None else None + item_data['description'] = str(item.find('description').text) if item.find('description') is not None else None + item_data['has_children'] = True if item.find('children') is not None else False + item_data['rule_type'] = str(item.find('ruleType').text) if item.find('ruleType') is not None else None + item_data['rule_value'] = str(item.find('ruleText').text) if item.find('ruleText') is not None else None + item_data['criticality'] = int(item.find('criticalityScore').text) if item.find('criticalityScore') is not None else None + if item_data['has_children']: + self.child_tags_list = [] + for list in tree.iter('children'): + for items in list.iter('list'): + for tags in items.iter('TagSimple'): + single_tag = int(tags.find('id').text) if item.find('id') is not None else None + if single_tag is not None: + tag = self.getTag(tag_id=single_tag) + self.child_tags_list.append(tag) + return Tag( + name=item_data["name"], + id=item_data["id"], + colour=item_data["colour"], + created=item_data["created"], + modified=item_data["modified"], + description=item_data['description'], + child_tags=self.child_tags_list, + criticality=item_data['criticality'], + rule_type=item_data['rule_type'], + dynamic_rule=item_data['rule_value'], + ) + else: + return Tag( + name=item_data["name"], + id=item_data["id"], + colour=item_data["colour"], + created=item_data["created"], + modified=item_data["modified"], + description=item_data['description'], + criticality=item_data['criticality'], + rule_type=item_data['rule_type'], + dynamic_rule=item_data['rule_value'], + ) + if items_found > 1: + tags_list = [] + tree =tagData.find('data') + for item in tree.findall('Tag'): + item_data = {} + item_data["id"] = int(item.find("id").text) if item.find('id') is not None else None + item_data["name"] = str(item.find("name").text) if item.find('name') is not None else None + item_data["created"] = str(item.find("created").text) if item.find('created') is not None else None + item_data["modified"] = str(item.find("modified").text) if item.find('modified') is not None else None + item_data["colour"] = str(item.find("color").text) if item.find('color') is not None else None + item_data['description'] = str(item.find('description').text) if item.find('description') is not None else None + item_data['has_children'] = True if item.find('children') is not None else False + item_data['rule_type'] = str(item.find('ruleType').text) if item.find('ruleType') is not None else None + item_data['rule_value'] = str(item.find('ruleText').text) if item.find('ruleText') is not None else None + item_data['criticality'] = int(item.find('criticalityScore').text) if item.find('criticalityScore') is not None else None + if item_data['has_children']: + self.child_tags_list = [] + for list in tree.iter('children'): + for items in list.iter('list'): + for tags in items.iter('TagSimple'): + single_tag = int(tags.find('id').text) if item.find('id') is not None else None + if single_tag is not None: + tag = self.getTag(tag_id=single_tag) + self.child_tags_list.append(tag) + tags_list.append(Tag( + name=item_data["name"], + id=item_data["id"], + colour=item_data["colour"], + created=item_data["created"], + modified=item_data["modified"], + description=item_data['description'], + child_tags=self.child_tags_list, + criticality=item_data['criticality'], + rule_type=item_data['rule_type'], + dynamic_rule=item_data['rule_value'], + )) + else: + tags_list.append(Tag( + name=item_data["name"], + id=item_data["id"], + colour=item_data["colour"], + created=item_data["created"], + modified=item_data["modified"], + description=item_data['description'], + criticality=item_data['criticality'], + rule_type=item_data['rule_type'], + dynamic_rule=item_data['rule_value'], + )) + return tags_list + + if items_found < 1: + logger.warning(f'Warning: unable to find tags matching: {criteria} {operator} {search_value}') + return None \ No newline at end of file From 7e049fe11a82ff8b742f9b8d858c57700776b78d Mon Sep 17 00:00:00 2001 From: Riley V Date: Mon, 6 Oct 2025 13:55:36 +1100 Subject: [PATCH 11/11] remove todos and other cleanup --- qualysapi/api_actions.py | 41 +--------------------------------------- 1 file changed, 1 insertion(+), 40 deletions(-) diff --git a/qualysapi/api_actions.py b/qualysapi/api_actions.py index df1f0af..7f4cd3f 100644 --- a/qualysapi/api_actions.py +++ b/qualysapi/api_actions.py @@ -501,42 +501,6 @@ def listScans(self, launched_after="", state="", target="", type="", user_login= return scanArray -# don't need this anymore I reckon -# def listChildTags(self, tag_name=None, tag_id=None, filename=None): -# if tag_id: -# files = ( -# """ -# -# """ -# + tag_id -# + """ -# -# """ -# ) -# elif filename: -# files = open(filename, "rb").read() -# elif tag_name: -# files = ( -# """ -# -# """ -# + tag_name -# + """ -# -# """ -# ).encode("ascii", "ignore") - -# call = "/qps/rest/2.0/search/am/tag" -# parameters = files -# response = objectify.fromstring( -# self.request(call, parameters, api_version=2, http_method="post").encode("utf-8") -# ) -# childs = list() -# for child in response.getchildren()[3][0].Tag.children.list.getchildren(): -# childs.append(child.getchildren()) - -# return childs - def launchScan(self, title, option_title, iscanner_name, asset_groups="", ip=""): # TODO: Add ability to scan by tag. call = "/api/2.0/fo/scan/" @@ -641,7 +605,6 @@ def listAppliances(self): def getTag(self, tag_name: str | None = None,tag_id: int | None = None): #TODO: fix recursion where multiple layers of child tags exist - #TODO: enable searching by all types of search parameters through arguments passed call = "search/am/tag" if (tag_name is not None) and (tag_id is not None): logger.error('Error: unable to search, both tag name and id provided') @@ -698,7 +661,6 @@ def getTag(self, tag_name: str | None = None,tag_id: int | None = None): dynamic_rule=item_data['rule_value'], ) if items_found > 1: - #TODO: return multiple tags for name-based search? value = str(tag_id) if tag_id is not None else tag_name logger.warning(f'Warning: multiple results returned for tag: {value}, use findTags() instead') return None #for now... @@ -760,8 +722,7 @@ def editTag(self, tag: Tag, name: str | None = None, colour: str | None = None, return None def createTag(self, name: str, colour: str | None = None, criticality: int | None = None,rule_type: str | None = None,rule_text: str | None = None,child_tags: list | None = None,description: str | None = None): - #TODO: ability to create tags with child tags - #TODO: allow passing attributes for tag as dict of attribs + #TODO: allow passing attributes for tag as dict of attribs? call = 'create/am/tag' colour_validation = re.compile(r'#([A-Fa-f0-9]){6}') if colour is None: