From b1ea578808c03e90a995aeced15cb124d08de897 Mon Sep 17 00:00:00 2001 From: Pascal Weber Date: Thu, 12 Oct 2023 15:22:29 +0200 Subject: [PATCH] Initial commit --- .gitignore | 6 + LICENSE | 21 ++++ NHSuite.py | 60 +++++++++ README-ERROR.md | 111 ++++++++++++++++ README.md | 238 +++++++++++++++++++++++++++++++++++ config.txt | 9 ++ qradarzoldaxclass.py | 292 +++++++++++++++++++++++++++++++++++++++++++ qradarzoldaxlib.py | 166 ++++++++++++++++++++++++ 8 files changed, 903 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100755 NHSuite.py create mode 100644 README-ERROR.md create mode 100644 README.md create mode 100644 config.txt create mode 100644 qradarzoldaxclass.py create mode 100644 qradarzoldaxlib.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aea5bfe --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +app.log +__pycache__ +network_hierarchy.csv +config +serverchain.pem +error.log diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8bfacc2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018-2023 Pascal Weber (zoldax) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/NHSuite.py b/NHSuite.py new file mode 100755 index 0000000..a3fa783 --- /dev/null +++ b/NHSuite.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 + +""" +NHSuite.py + +A utility script to export, import and fetch domain from the QRadar Network Hierarchy. +Author: Pascal Weber (zoldax) + +Requirements: +- qradarzoldaxlib: A library to interact with QRadar API. +- qradarzoldaxclass : A library containing my NH Class and methods + +Author : Pascal Weber +""" + +import csv +import re +import json +import argparse +import ipaddress +import os +import qradarzoldaxlib +from qradarzoldaxclass import QRadarNetworkHierarchy +from datetime import datetime +from typing import Union + +def main(): + """Main function to handle command-line arguments and execute desired actions.""" + qradar_nh = QRadarNetworkHierarchy() + + parser = argparse.ArgumentParser(description="QRadar Network Hierarchy Suite by Pascal Weber (zoldax) / Abakus Sécurité") + parser.add_argument('-e', '--export-file', nargs='?', const="network_hierarchy.csv", default=None, metavar="FILENAME", help="Export network hierarchy to a CSV file. If no filename is provided, it will default to 'network_hierarchy.csv'.") + parser.add_argument('-i', '--import-file', type=str, metavar="IMPORT_FILENAME", help="Import network hierarchy from a CSV file") + parser.add_argument('--check-domain', action='store_true', help="Fetch and display domain information from QRadar") + parser.add_argument('--check-version', action='store_true', help="Retrieve and display QRadar current system information") + + args = parser.parse_args() + + if args.export_file: + lines_exported = qradar_nh.write_network_hierarchy_to_csv(args.export_file) + print(f"{lines_exported} lines exported successfully! (including col headers)") + + elif args.import_file: + lines_imported = qradar_nh.import_csv_to_qradar(args.import_file) + if isinstance(lines_imported, int) and lines_imported > 0: + print(f"{lines_imported} lines imported successfully!") + else: + print("Data import failed.") + + elif args.check_domain: + qradar_nh.check_domain() + + elif args.check_version: + qradarzoldaxlib.print_qradar_version() + + else: + parser.print_help() + +if __name__ == "__main__": + main() diff --git a/README-ERROR.md b/README-ERROR.md new file mode 100644 index 0000000..ea49ceb --- /dev/null +++ b/README-ERROR.md @@ -0,0 +1,111 @@ +# ⚙️ QRadar Interaction Library: Error Logging + +### 🖋️ Author +- **Pascal Weber (zoldax)** + +## 🚫 Errors Logged in `qradarzoldaxlib` +The library, `qradarzoldaxlib.py`, offers functionalities for interaction with the QRadar API. Below is a breakdown of potential errors that may be logged: + +### 1. 📑 `read_config` Function + +This function reads configuration data from a JSON file. + +#### Error Scenarios: + +- **🚫 File Not Found**: + ``` + Error reading configuration file [filename] : [filename] does not exist + ``` + +- **🔣 JSON Decoding Error**: + ``` + Error reading configuration file [filename] : [specific JSON decoding error message] + ``` + +- **❗ Unexpected Errors**: + ``` + Unexpected error occurred while reading [filename] : [error details] + ``` + +### 2. 🌐 `make_request` Function + +This function sends an HTTP request (either GET or PUT) to a specified URL. + +#### Error Scenarios: + +- **❌ Unsupported HTTP Method**: + ``` + Unsupported HTTP method: [method] + ``` + +- **⚠️ Request Exception or HTTP Error**: + ``` + Error occurred during request: [error details] + ``` + +- **❗ Unexpected Errors**: + ``` + An unexpected error occurred: [error details] + ``` + +### 3. 🆔 `get_app_id` Function + +This function fetches the application ID for a given app name from QRadar. + +#### Note: +If the function fails to fetch applications from the QRadar API, no specific error message is logged, but the function returns an empty string. + +## 🚫 Errors Logged in `qradarzoldaxclass.py` + +### 📢 Errors + +- **Configuration Reading 📚** + - `config.txt not found.` + - `Failed to decode JSON from config.txt.` + +- **Network Hierarchy Fetching 🌐** + - `Unexpected data format received: {hierarchy_data}` + +- **CSV Import to QRadar 📤** + - 🚫 `Backup failed. Aborting the import process for safety.` + - When: The safety mode is ON, and backup of the current network hierarchy fails. + - Location: Method `import_csv_to_qradar`. + + - ❌ `Invalid cidr {row['cidr']} for id {row['id']} - abort import.` + - When: The provided CIDR in the CSV is invalid. + - Location: Method `import_csv_to_qradar`. + + - ❌ `Invalid group name {row['group']} for id {row['id']} - abort import -.` + - When: The provided group name in the CSV is invalid. + - Location: Method `import_csv_to_qradar`. + + - ⚠️ `Invalid location format {location_val} for id {row['id']} - import continue - but value set to null.` + - When: The provided location format in the CSV is invalid. + - Location: Method `import_csv_to_qradar`. + + - ⚠️ `Invalid country code {country_code_val} for id {row['id']} - import continue - but value set to null.` + - When: The provided country code in the CSV is invalid. + - Location: Method `import_csv_to_qradar`. + + - 🚫 `Failed to import data from {csv_filename}` + - When: There's an error while making a request to import the data. + - Location: Method `import_csv_to_qradar`. + + - ❗ `File {csv_filename} not found.` + - When: The specified CSV file for import is not found. + - Location: Method `import_csv_to_qradar`. + + - ❗ `Error reading CSV file {csv_filename}.` + - When: There's an error reading the CSV file. + - Location: Method `import_csv_to_qradar`. + + - 🚫 `An unexpected error occurred: {e}` + - When: Any unexpected error occurs. + - Location: Method `import_csv_to_qradar`. + +- **Domain Information Checking 🌍** + - `Unexpected data format received: {domain_data}` + +- **Backup of Current Network Hierarchy 💾** + - `Failed to create a backup due to the following error: {e}` + diff --git a/README.md b/README.md new file mode 100644 index 0000000..52fa2bd --- /dev/null +++ b/README.md @@ -0,0 +1,238 @@ +# 🛠️NHSuite + +NHSuite allows users to efficiently manage their QRadar Network Hierarchy. Utilizing the provided QRadar API, users can seamlessly export, import, and check domain-specific hierarchies in a CSV format. + +--- +# Table of Contents +- [🛠️NHSuite](#️qradarnhsuite) + - [📌 Details](#-details) + - [📖 Description](#-description) + - [🛠️Usage](#️usage) + - [1. Exporting the Network Hierarchy to CSV](#1-exporting-the-network-hierarchy-to-csv) + - [2. Importing Network Hierarchy from CSV](#2-importing-network-hierarchy-from-csv) + - [3. Checking Domain Information](#3-checking-domain-information) + - [3. Checking QRadar System Information](#3-checking-qradar-system-information) + - [📦 Requirements](#-requirements) + - [📥 Inputs](#-inputs) + - [📤 Outputs](#-outputs) + - [🔑 Functionalities & Key Function](#-functionalities--key-function) + - [🛠Configuration: `config.txt`](#configuration-configtxt) + - [1. `ip_QRadar`](#1-ip_qradar) + - [2. `auth`](#2-auth) + - [3. `Version`](#3-version) + - [4. `Accept`](#4-accept) + - [5. `verify_ssl`](#5-verify_ssl) + - [6. `ssl_cert_path`](#6-ssl_cert_path) + - [7. `safety` Parameter](#7-safety-parameter) + - [🔐 SSL API Connection Support](#-ssl-api-connection-support) + - [🚫Error Handling](#error-handling) + - [📝 Notes](#-notes) + - [📜 Disclaimer](#-disclaimer) +--- + +## 📌 Details +- **Name**: NHSuite.py +- **Description**: A utility script designed to interface with the QRadar Network Hierarchy. Export, import, and fetch domain functionalities are provided. +- **Author**: Pascal Weber (zoldax) / Abakus Sécurité + +## 📖 Description +NHSuite allows users to efficiently manage their QRadar Network Hierarchy. Utilizing the provided QRadar API, users can seamlessly export, import, and check domain-specific hierarchies in a CSV format. + +At its heart, the tool aims to help — to offer a more straightforward approach to network hierarchy management, whether you're directly interfacing with QRadar or operating from a remote 🐧 Linux machine. Its functionalities, from error handling to data export, serve the user's needs without pretense. +  +The tool isn't just about functionality—it's about adaptability. In the modern software development world, Continuous Integration and Continuous Deployment (CI/CD) have become foundational principles we saw that every day on the operational field. This ensure network hierarchy updates, backup, and changes to be consistently integrated, tested, and deployed to test environnement before production, and is part of a more global projet on my side. + +It's also worth noting that, by hosting the script on GitHub, there's an open invitation for everyone to contribute and improve upon it. It's a collaborative effort, and the real value of the tool will be determined by the community's engagement and feedback 📢 (ideas for example : phpIPAM translation, etc...) +In summary, 🛠 NHSuite 🛠 is a humble attempt to make life a bit easier 🌟. + +## 🛠️Usage + +For detailed usage and command-line options, execute the script with `-h` or `--help`. + +Using NHSuite is straightforward with command-line arguments. Here's a step-by-step guide: + +### 1. Exporting the Network Hierarchy to CSV: +To export the current QRadar Network Hierarchy into a CSV, use the `-e` or `--export-file` argument. You can specify the name of the output file. If you don't, it will default to `network_hierarchy.csv`. + +**Example**: +```bash +# Export to default file name (network_hierarchy.csv) +python NHSuite.py -e + +# Export to a specific file name +python NHSuite.py -e my_network_data.csv +``` + +### 2. Importing Network Hierarchy from CSV: + +If you have a CSV file with the network hierarchy you'd like to import into QRadar, use the `-i` or `--import-file` argument followed by the file's path. +Before importing a new network hierarchy, it's essential to have a backup of your current setup. This tool has a built-in function to facilitate this if the safety parameter on the config.txt file is set to on. + +**Example**: +```bash +# Import from a specific CSV file +python NHSuite.py -i path_to_my_network_data.csv +``` + +### 3. Checking Domain Information: + +To retrieve and display domain information from QRadar, utilize the `--check-domain` flag. + +**Example**: +```bash +# Fetch and display domain information from QRadar +python NHSuite.py --check-domain +``` + +### 3. Checking QRadar System Information: + +To retrieve and display domain information from QRadar, utilize the `--check-version` flag. + +**Example**: +```bash +# Display system information from QRadar +python NHSuite.py --check-version +``` + +## 📦 Requirements +- `qradarzoldaxlib`: A library to interact with QRadar's API. +- `qradarzoldaxclass`: Contain NetworkHierarchy class with methods and decorators. + +## 📥 Inputs +1. `-e`, `--export-file`: Specify a file name to export the network hierarchy to. Defaults to 'network_hierarchy.csv' if no name is provided. +2. `-i`, `--import-file`: Specify a CSV file to import network hierarchy from. +3. `--check-domain`: Fetch and display domain information from QRadar. + +## 📤 Outputs +- CSV File (when exporting) that includes fields such as `id`, `group`, `name`, `cidr`, `description`, `domain_id`, `location`, `country_code`. +- Console prints with domain information when `--check-domain` is used. + +## 🔑 Functionalities & Key Function +1. `fetch_network_hierarchy()`: Fetches the QRadar Network Hierarchy. +2. `write_network_hierarchy_to_csv()`: Exports the fetched network hierarchy to a specified CSV file. +3. `import_csv_to_qradar()`: Imports network hierarchy data from a given CSV to QRadar. +4. `check_domain()`: Fetches and displays domain information from QRadar. +5. Validation functions: Various functions are provided to validate CIDR, location format, country code, and group format. +6. `backup_current_hierarchy()` : Before importing a new network hierarchy, if safety is on backup of the current Network Hierarchy before Import. + +## 🛠Configuration: `config.txt` + +The `config.txt` file contains configuration parameters required for the communication with QRadar. Here's an overview of each parameter: + +### 1. `ip_QRadar` + +This represents the IP address or the domain name of your QRadar instance. + +```json +"ip_QRadar": "qradardemo.zoldaxcorp.lan" +``` + +### 2. `auth` + +This is the authentication token which is used to authenticate your requests to the QRadar API. + +```json +"auth": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" +``` + +### 3. `Version` + +This represents the version of the QRadar API you are targeting. + +```json +"Version": "15.0" +``` + +### 4. `Accept` + +This parameter determines the format in which you'd like the response from the QRadar API. Typically, this would be set to `application/json`. + +```json +"Accept": "application/json" +``` + +### 5. `verify_ssl` + +This parameter determines whether the SSL certificate validation should be performed when connecting to QRadar. + +```json +"verify_ssl": "True" +``` + +- "True" indicates that SSL verification is enabled. +- "False" indicates that SSL verification is disabled. Note: Disabling SSL verification is not recommended for production environments. + +### 6. `ssl_cert_path` + +If you have a custom certificate chain for SSL verification, or if the server's certificate chain isn't recognized by the default set of trusted certificate authorities on your system, you can specify a PEM file containing the entire certificate chain. + +```json +"ssl_cert_path": "/path/to/certchained.pem" +``` + +- If you don't have a custom certificate chain, or if `verify_ssl` is set to "False", you can set this to "None". + +Please ensure that all parameters are properly configured to match your QRadar environment and your preferences. + + +### 7. `safety` Parameter +```json +"safety": "on" +``` + +The `safety` parameter is a switch that can be set to either `on` or `off`. It governs whether a backup of the current network hierarchy is made before performing potentially disruptive operations. + +#### When set to `on`: + +- Before importing or making any changes to the QRadar Network Hierarchy, the tool will first create a backup of the current hierarchy. +- This backup is stored in a directory named `safety`. If this directory doesn't already exist, it will be created. +- The backup filename will follow the format: `backup-before-import-NH-QRadarIP-Timestamp.csv`. +- By having this safety backup, users can restore to a previous state in case of any unintended changes or issues. + +#### When set to `off`: + +- No backup will be created before making changes. +- Users should be cautious when setting this parameter to `off`, especially in production environments, as it might be harder to revert unintended modifications without a recent backup. + +To leverage this feature, ensure you configure the `safety` parameter appropriately in the tool's configuration or command-line arguments. + + +## 🔐 SSL API Connection Support + +For secure communication with the QRadar API, this tool supports SSL verification through two configuration parameters in the `config.txt` file: + +### 1. `verify_ssl` + +This parameter determines whether SSL certificate validation should be performed. + + +- `"True"`: This means that the SSL certificate provided by the server will be validated against the certificate authorities available in your environment. If you have a custom certificate authority or the server's certificate chain isn't in the default set of trusted authorities on your system, you need to specify the path to a PEM file containing the entire certificate chain using the `ssl_cert_path` parameter. Remember to use the right name for the QRadar_ip as IP or fqdn depending on how you created the certificate. + +- `"False"`: This will bypass any SSL certificate validation. This option is not recommended for production environments due to security concerns, as it makes the connection vulnerable to man-in-the-middle attacks. + +### 2. `ssl_cert_path` + +If you have a custom certificate chain or if the server's certificate chain isn't recognized by the default set of trusted certificate authorities on your system, you can specify a PEM file containing the entire certificate chain. + +## 🚫Error Handling +The tool is equipped to handle errors like invalid CIDR format, invalid group name, issues while parsing 'location' and 'country_code' fields, and any unexpected exceptions. Errors are logged using `qradarzoldaxlib.logger.error` on file `error.log`. + +A description of handled errors is on the [README-ERROR.md](README-ERROR.md) file. + +For errors related to the API call of QRadar here is the common API error Message : https://www.ibm.com/docs/en/qradar-common?topic=versions-api-error-messages + +## 📝 Notes + +Always test any modifications in a safe environment. + +Please consult IBM Guidelines for building a Network hierarchy : https://www.ibm.com/docs/en/qradar-on-cloud?topic=hierarchy-guidelines-defining-your-network + +## 📜 Disclaimer: + +All content is without warranty of any kind. Use at your own risk. I assume no liability for the accuracy, correctness, completeness, usefulness, or any damages. + +Q1 LABS, QRADAR and the 'Q' Logo are trademarks or registered trademarks of IBM Corp. All other trademarks are the property of their respective owners. + +IBM, the IBM logo, and ibm.com are trademarks or registered trademarks of International Business Machines Corp., registered in many jurisdictions worldwide. Other product and service names might be trademarks of IBM or other companies. + +---------------------------------------------------------- diff --git a/config.txt b/config.txt new file mode 100644 index 0000000..bae2791 --- /dev/null +++ b/config.txt @@ -0,0 +1,9 @@ +{ + "ip_QRadar": "192.168.10.146", + "auth": "a913b05c-cb81-4d2f-b286-2572f0c4baee", + "Version": "17.0", + "Accept": "application/json", + "verify_ssl": "False", + "ssl_cert_path": "./serverchain.pem", + "safety": "on" +} diff --git a/qradarzoldaxclass.py b/qradarzoldaxclass.py new file mode 100644 index 0000000..13092d9 --- /dev/null +++ b/qradarzoldaxclass.py @@ -0,0 +1,292 @@ +#qradarzoldaxclass.py + +""" +qradarzoldaxclass.py +Author: Pascal Weber (zoldax) +""" + +import csv +import re +import json +import argparse +import ipaddress +import qradarzoldaxlib +import os +from datetime import datetime +from typing import Union + +class QRadarNetworkHierarchy: + """ + A class to manage and interact with the QRadar Network Hierarchy by Pascal Weber (zoldax) + + QRadar Network Hierarchy is a logical representation of a network's structure. + This class provides utilities to validate, fetch, import, and backup the hierarchy. + + Attributes: + ----------- + base_url : str + The base URL for the QRadar API. + + Methods: + -------- + valid_location_format(loc: str) -> bool: + Validates if a given string is in the correct latitude and longitude format. + + valid_country_code_format(code: str) -> bool: + Validates if the provided country code is in the correct two-letter format. + + valid_group_format(group: str) -> bool: + Validates the format of a given group string. + + format_location(location_str: str) -> dict: + Converts a location string into a dictionary format. + + valid_cidr_format(cidr: str) -> bool: + Validates if a given string is in the correct CIDR format. + + fetch_network_hierarchy() -> list: + Fetches the QRadar Network Hierarchy from the API. + + write_network_hierarchy_to_csv(filename: str) -> int: + Fetches QRadar Network Hierarchy and writes it to a CSV file. + + import_csv_to_qradar(csv_filename: str) -> Union[bool, int]: + Imports data from a CSV file into QRadar using the API. + + check_domain() -> None: + Fetches and displays domain information from QRadar. + + backup_current_hierarchy() -> bool: + Backs up the current QRadar Network Hierarchy to a CSV file. + """ + + def __init__(self): + """ + Initialize the QRadarNetworkHierarchy object with the base URL. + """ + self.config = self._read_config() + self.base_url = f"https://{self.config['ip_QRadar']}" + + @staticmethod + def _read_config() -> dict: + """ + Reads the configuration from the config.txt file. + + Returns: + -------- + dict : Configuration as a dictionary. + """ + try: + with open('config.txt', 'r') as f: + return json.load(f) + except FileNotFoundError: + raise Exception("config.txt not found.") + except json.JSONDecodeError: + raise Exception("Failed to decode JSON from config.txt.") + + # Validation Functions + + @staticmethod + def valid_location_format(loc: str) -> bool: + """Validate if the provided string is in the correct lat,long format.""" + pattern = r'^(-?\d+(\.\d+)?),\s*(-?\d+(\.\d+)?)$' + match = re.match(pattern, loc) + if not match: + return False + lat, _, lon, _ = match.groups() + lat, lon = float(lat), float(lon) + + # Check latitude is between -90 and 90 + if not (-90 <= lat <= 90): + return False + + # Check longitude is between -180 and 180 + if not (-180 <= lon <= 180): + return False + + return True + + + + #return -90 <= lat <= 90 and -180 <= lon <= 180 + + @staticmethod + def valid_country_code_format(code: str) -> bool: + """Validate if the provided country code is in the correct format.""" + return bool(re.match(r"^[A-Z]{2}$", code)) + + @staticmethod + def valid_group_format(group: str) -> bool: + """Validate the group format. Allowed characters: Alphanumerics, ., -, _""" + return bool(re.match(r'^[A-Za-z0-9\.\-_]+$', group)) + + @staticmethod + def format_location(location_str: str) -> dict: + """Convert location string (long,lat) to dictionary format.""" + ### Have to change the order like the API, long first then lat """Convert location string (lat,long) to dictionary format.""" + lat, lon = map(float, location_str.split(',')) + return {"type": "Point", "coordinates": [lon, lat]} + + @staticmethod + def valid_cidr_format(cidr: str) -> bool: + """Validate if the provided string is in correct CIDR format.""" + # Check basic format using regex + if not re.match(r"^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})/(\d{1,2})$", cidr): + return False + + # Use ipaddress to validate the correctness of the IP address and prefix + try: + ipaddress.ip_network(cidr) + return True + except ValueError: + return False + + # Functions for NH + + def fetch_network_hierarchy(self): + """Fetch the QRadar Network Hierarchy.""" + url = f"{self.base_url}/api/config/network_hierarchy/networks" + hierarchy_data = qradarzoldaxlib.make_request(url, "GET") + if not isinstance(hierarchy_data, list): + qradarzoldaxlib.logger.error(f"Unexpected data format received: {hierarchy_data}") + return [] + return hierarchy_data + + def write_network_hierarchy_to_csv(self, filename="network_hierarchy.csv"): + """ + Fetch QRadar Network Hierarchy and write it to a CSV file. + + :param filename: The name of the output CSV file. + :return: Number of lines written. + """ + hierarchy_data = self.fetch_network_hierarchy() + + with open(filename, 'w', newline='') as file: + writer = csv.writer(file, delimiter=',', quotechar='"', quoting=csv.QUOTE_MINIMAL) + columns = ["id", "group", "name", "cidr", "description", "domain_id", "location", "country_code"] + writer.writerow(columns) + + for entry in hierarchy_data: + location = entry.get("location", {}) + location_str = 'N/A' + if location.get("type") == "Point" and len(location.get("coordinates", [])) == 2: + location_str = f"{location['coordinates'][1]},{location['coordinates'][0]}" + ## Warning / zoldax : Have to change the order , as QRadar API give the longitude first then the latitude + entry["location"] = location_str + data = [entry.get(key, 'N/A') for key in columns] + writer.writerow(data) + #Pascal test + return len(hierarchy_data) + 1 + + def import_csv_to_qradar(self, csv_filename: str) -> Union[bool, int]: + """ + Import data from the given CSV file to QRadar via the API. + """ + if 'safety' in qradarzoldaxlib.config and qradarzoldaxlib.config['safety'].lower() == "on": + backup_success = self.backup_current_hierarchy() + + if not backup_success: + qradarzoldaxlib.logger.error("Backup failed. Aborting the import process for safety.") + return False + + url = f"{self.base_url}/api/config/network_hierarchy/staged_networks" + network_hierarchy_data = [] + try: + with open(csv_filename, 'r', newline='', encoding='utf-8') as csv_file: + reader = csv.DictReader(csv_file) + for row in reader: + network_obj = { + "id": int(row["id"]), + "name": row["name"], + "description": row["description"], + "domain_id": int(row["domain_id"]), + } + if not self.valid_cidr_format(row["cidr"]): + print(f"Invalid cidr {row['cidr']} for id {row['id']}.") + qradarzoldaxlib.logger.error(f"Invalid cidr {row['cidr']} for id {row['id']} - abort import.") + else: + cidr_val = row.get("cidr", "").strip() + network_obj["cidr"] = cidr_val + + if not self.valid_group_format(row["group"]): + print(f"Invalid group name {row['group']} for id {row['id']}.") + qradarzoldaxlib.logger.error(f"Invalid group name {row['group']} for id {row['id']} - abort import -.") + else: + group_val = row.get("group", "").strip() + network_obj["group"] = group_val + + location_val = row.get("location", "").strip() + if location_val and location_val != "N/A": + if self.valid_location_format(location_val): + formatted_location = self.format_location(location_val) + network_obj["location"] = formatted_location + else: + print(f"Invalid location format {location_val} for id {row['id']} - import continue - but value set to null.") + qradarzoldaxlib.logger.error(f"Invalid location format {location_val} for id {row['id']} - import continue - but value set to null.") + + country_code_val = row.get("country_code", "").strip() + if country_code_val and country_code_val != "N/A": + if self.valid_country_code_format(country_code_val): + network_obj["country_code"] = country_code_val + else: + print(f"Invalid country code {country_code_val} for id {row['id']} - import continue - but value set to null.") + qradarzoldaxlib.logger.error(f"Invalid country code {country_code_val} for id {row['id']} - import continue - but value set to null.") + + network_hierarchy_data.append(network_obj) + #Debug print(f"Network NH info {network_hierarchy_data}") + + response = qradarzoldaxlib.make_request(url, "PUT", params=json.dumps(network_hierarchy_data)) + if response: + return len(network_hierarchy_data) + else: + qradarzoldaxlib.logger.error(f"Failed to import data from {csv_filename}") + return False + + except FileNotFoundError: + qradarzoldaxlib.logger.error(f"File {csv_filename} not found.") + return False + except csv.Error: + qradarzoldaxlib.logger.error(f"Error reading CSV file {csv_filename}.") + return False + except Exception as e: + qradarzoldaxlib.logger.error(f"An unexpected error occurred: {e}") + return False + + def check_domain(self): + """ + Fetch and display the domain information from QRadar. + """ + url = f"{self.base_url}/api/config/domain_management/domains" + domain_data = qradarzoldaxlib.make_request(url, "GET") + + if not isinstance(domain_data, list): + qradarzoldaxlib.logger.error(f"Unexpected data format received: {domain_data}") + return + + for domain in domain_data: + domain_id = domain.get("id", 'N/A') + if domain_id == 0: + domain_name = "DEFAULT_DOMAIN" + else: + domain_name = domain.get("name", 'N/A') or 'N/A' + domain_description = domain.get("description", 'N/A') or 'N/A' + print(f"Domain ID: {domain_id}, Domain Name: {domain_name}, Description: {domain_description}") + + def backup_current_hierarchy(self) -> bool: + """ + Create a backup of the current network hierarchy. + """ + try: + if not os.path.exists('safety'): + os.mkdir('safety') + + timestamp = datetime.now().strftime('%Y%m%d%H%M%S') + backup_filename = f"safety/backup-before-import-NH-{qradarzoldaxlib.config['ip_QRadar']}-{timestamp}.csv" + print(f"Safety parameter is on, actual Network hiearchy backuped in {backup_filename} before import") + + self.write_network_hierarchy_to_csv(backup_filename) + return True + + except Exception as e: + qradarzoldaxlib.logger.warning(f"Failed to create a backup due to the following error: {e}") + return False diff --git a/qradarzoldaxlib.py b/qradarzoldaxlib.py new file mode 100644 index 0000000..cade0a8 --- /dev/null +++ b/qradarzoldaxlib.py @@ -0,0 +1,166 @@ +""" +qradarzoldaxlib.py + +Description: This library provides functionalities to interact with the QRadar API. +It offers tools for reading configuration files, preparing headers, making GET and PUT requests, +and fetching application IDs. + +Author: Pascal Weber + +""" + + +import requests +import json +import urllib3 +import logging +import os +from typing import Union +from typing import Optional + +# Desactivate warning ssl +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +# Set up logging +LOG_FILENAME = 'error.log' +logging.basicConfig(filename=LOG_FILENAME, level=logging.ERROR, format='%(asctime)s - %(levelname)s - %(message)s') +logger = logging.getLogger() + +def read_config(filename: str = 'config.txt') -> dict: + """ + Reads the configuration from a specified JSON file. + + This function attempts to open and read a JSON configuration file. + If the file does not exist, or a JSON decoding error occurs, it logs + the error and returns an empty dictionary. For other unexpected errors, + it logs the error message and also returns an empty dictionary. + + :param filename: The name of the configuration file to read. Defaults to 'config.txt'. + :type filename: str + + :return: A dictionary containing the configuration data if successful, + otherwise an empty dictionary. + :rtype: dict + + Example: + -------- + >>> config_data = read_config('config.txt') + >>> print(config_data) + {'key': 'value'} + """ + try: + if not os.path.exists(filename): + raise FileNotFoundError(f"{filename} does not exist") + + with open(filename, 'r') as file: + config_data = json.load(file) + return config_data + except (FileNotFoundError, json.JSONDecodeError) as e: + logger.error(f"Error reading configuration file {filename} : {e}") + except Exception as e: + logger.error(f"Unexpected error occurred while reading {filename} : {e}") + return {} + +config = {**read_config()} + +def get_qradar_headers() -> dict: + """ + Prepare headers for QRadar HTTP request. + :return: dict containing headers + """ + return { + "SEC": config['auth'], + "Version": config.get('Version', "15.0"), + "Accept": config.get('Accept', "application/json") + } + +def get_verify_option() -> Union[bool, str]: + """ + Returns the appropriate value for the 'verify' parameter in requests. + This could be a boolean (True/False) or a string path to a custom certificate. + """ + # If verify_ssl is explicitly set to False, return False immediately. + if config.get('verify_ssl') == False: + return False + + # If verify_ssl is set to True and ssl_cert_path exists in the config and isn't None or empty string, return its value. + if config.get('verify_ssl') == 'True' and config.get('ssl_cert_path') != 'None': + return config['ssl_cert_path'] + + # In all other cases, return False. + return False + +def make_request(url: str, method: str = "GET", params: Optional[dict] = None) -> dict: + """ + Make a request (GET/PUT) to the specified URL. + :param url: URL to make the request to + :param method: HTTP method ("GET" or "PUT") + :param params: Parameters to be sent with the request + :return: JSON response as a dict if successful, empty dict otherwise + """ + if method not in ["GET", "PUT"]: + logger.error(f"Unsupported HTTP method: {method}") + return {} + + verify_option = get_verify_option() + try: + if method == "GET": + response = requests.get(url, headers=get_qradar_headers(), params=params, verify=verify_option) + else: + response = requests.put(url, headers=get_qradar_headers(), data=params, verify=verify_option) + + response.raise_for_status() + return response.json() + + except (requests.RequestException, requests.exceptions.HTTPError) as e: + logger.error(f"Error occurred during request: {e}") + return {} + except Exception as e: + logger.error(f"An unexpected error occurred: {e}") + return {} + +def get_app_id(app_name: str = "QRadar Use Case Manager") -> str: + """ + Fetch the application ID for a given app name from QRadar. + :param app_name: Name of the app to fetch the ID for + :return: Application ID as a string + """ + url = f"https://{config['ip_QRadar']}/api/gui_app_framework/application_definitions" + apps = make_request(url) + if not apps: + return "" + for app in apps: + manifest = app.get("manifest", {}) + if manifest.get("name") == app_name: + return app.get("application_definition_id", "") + return "" + +def get_system_info() -> dict: + """ + Fetch the system information from the `/api/system/about` API endpoint in QRadar. + + :return: JSON response as a dict containing system information if successful, + empty dict otherwise. + + Example: + -------- + >>> system_info = get_system_info() + >>> print(system_info) + { + "release_name": "7.5.0 UpdatePackage 6", + "build_version": "2021.6.6.20230519190832", + "fips_enabled": false, + "external_version": "7.5.0" + } + """ + url = f"https://{config['ip_QRadar']}/api/system/about" + return make_request(url) + +def print_qradar_version(): + """Retrieve and print QRadar system information.""" + system_info = get_system_info() + print(f"QRadar System Information: {config['ip_QRadar']}") + print(f"release_name: {system_info['release_name']}") + print(f"build_version: {system_info['build_version']}") + print(f"fips_enabled: {system_info['fips_enabled']}") + print(f"external_version: {system_info['external_version']}")