CheckFirewallAndGPForCVEs

python · PAN-OS by Palo Alto Networks

Source

import demistomock as demisto  # noqa: F401
from CommonServerPython import *  # noqa: F401

import traceback
import re
import itertools


def version_compare(v1: str, v2: str) -> int:
    """
    Compare two version strings and return their relative ordering.

    This function compares two version strings by parsing them into numeric components
    and performing a lexicographic comparison. It handles PAN-OS version formats that
    use dots (.) and hyphens (-) as separators.

    Args:
        v1 (str): The first version string to compare (e.g., "10.2.3", "11.0.1-h2").
        v2 (str): The second version string to compare (e.g., "10.2.4", "11.0.0").

    Returns:
        int:
            -1 if v1 < v2 (v1 is older than v2)
             0 if v1 == v2 (versions are equal)
             1 if v1 > v2 (v1 is newer than v2)

    Note:
        - Versions are split on dots (.) and hyphens (-) into components
        - Only the first 3 components are considered for comparison
        - Non-numeric components are treated as 0
        - Missing components are treated as 0 (e.g., "10.2" is treated as "10.2.0")

    Examples:
        version_compare("10.2.3", "10.2.4") returns -1
        version_compare("11.0.0", "10.2.9") returns 1
        version_compare("10.2.3", "10.2.3") returns 0
        version_compare("10.2.3-h1", "10.2.3-h2") returns 0 (hotfix part ignored)
    """

    v1_parts = [int(x) if x.isdigit() else 0 for x in re.split(r"[\.\-]", v1)[:3]]
    v2_parts = [int(x) if x.isdigit() else 0 for x in re.split(r"[\.\-]", v2)[:3]]

    for v1_part, v2_part in itertools.zip_longest(v1_parts, v2_parts, fillvalue=0):
        if v1_part < v2_part:
            return -1
        elif v1_part > v2_part:
            return 1

    return 0


""" FUNCTION TO CHECK IF THE PAN-OS VERSION IS AFFECTED BY ANY CVE"""


def check_fw_version(
    fw_versions_checked: list[str], pan_os_affected_versions: list[dict], pan_os_system_info: dict[str, str]
) -> bool:  # noqa: E501
    """
    Check if a PAN-OS firewall version is affected by a CVE.

    This function determines whether the PAN-OS software version from the system
    information is affected by a given CVE by comparing it against the affected
    version ranges and applying any status changes (hotfixes/patches). It implements
    the algorithm provided on https://cveproject.github.io/cve-schema/schema/docs.

    Args:
        fw_versions_checked (list[str]): A list to track PAN-OS versions that
            have already been checked. The current version will be appended to this
            list if it falls back to the default status check.
        pan_os_affected_versions (list[dict]): A list of dictionaries containing affected
            version information for PAN-OS. Each dictionary should contain:
            - 'versions' (list): List of version entries with status information
            - 'defaultStatus' (str): Default status if version doesn't match any entry
        pan_os_system_info (dict[str, str]): Dictionary containing system information
            with 'sw_version' key containing the PAN-OS software version to check.

    Returns:
        bool: True if the PAN-OS version is affected by the CVE, False otherwise.

    Note:
        - Version entries can contain exact matches or range specifications using
          'lessThan'/'lessThanOrEqual' fields
        - Status changes (hotfixes) are applied in chronological order, with hotfix
          versions (containing '-') requiring exact matches
        - If no matching version entry is found, the defaultStatus is used
        - An empty string is used as fallback if 'sw_version' is not found in system info
    """
    v = pan_os_system_info.get("sw_version", "")
    fw_versions_checked.append(v)
    product_versions = pan_os_affected_versions[0].get("versions", []) if len(pan_os_affected_versions) > 0 else []
    default_status = (
        pan_os_affected_versions[0].get("defaultStatus", "unknown") if len(pan_os_affected_versions) > 0 else "unknown"
    )

    for entry in product_versions:
        if "lessThan" not in entry and "lessThanOrEqual" not in entry and v == entry["version"]:
            return entry["status"] == "affected"

        if ("lessThan" in entry and version_compare(entry["version"], v) <= 0 and version_compare(v, entry["lessThan"]) < 0) or (
            "lessThanOrEqual" in entry
            and version_compare(entry["version"], v) <= 0
            and version_compare(v, entry["lessThanOrEqual"]) <= 0
        ):
            status = entry["status"]
            for change in entry.get("changes", []):
                if "-" in change["at"]:  # Check if change['at'] is a hotfix
                    if change["at"] == v:  # Exact match for hotfix
                        status = change["status"]
                        break
                elif version_compare(change["at"], v) <= 0:
                    status = change["status"]
            return status == "affected"

    return default_status == "affected"


def check_gp_version(gp_version_checked: list[str], gp_affected_versions: list[dict], pan_os_system_info: dict[str, str]) -> bool:  # noqa: E501
    """
    Check if a GlobalProtect version is affected by a CVE.

    This function determines whether the GlobalProtect client package version from the
    system information is affected by a given CVE by comparing it against the affected
    version ranges and applying any status changes (hotfixes/patches). It implements
    the algorithm provided on https://cveproject.github.io/cve-schema/schema/docs.

    Args:
        gp_version_checked (list[str]): A list to track GlobalProtect versions that
            have already been checked. The current version will be appended to this
            list if it falls back to the default status check.
        gp_affected_versions (list[dict]): A list of dictionaries containing affected
            version information for GlobalProtect. Each dictionary should contain:
            - 'versions' (list): List of version entries with status information
            - 'defaultStatus' (str): Default status if version doesn't match any entry
        pan_os_system_info (dict[str, str]): Dictionary containing system information
            with 'global_protect_client_package_version' key containing the version
            to check.

    Returns:
        bool: True if the GlobalProtect version is affected by the CVE, False otherwise.

    Note:
        - Version entries can contain exact matches or range specifications using
          'lessThan'/'lessThanOrEqual' fields
        - Status changes (hotfixes) are applied in chronological order, with hotfix
          versions (containing '-') requiring exact matches
        - If no matching version entry is found, the defaultStatus is used
    """
    v = pan_os_system_info.get("global_protect_client_package_version", "")
    gp_version_checked.append(v)
    product_versions = gp_affected_versions[0].get("versions", []) if gp_affected_versions else []
    default_status = gp_affected_versions[0].get("defaultStatus", "unknown") if gp_affected_versions else "unknown"

    for entry in product_versions:
        if "lessThan" not in entry and "lessThanOrEqual" not in entry and v == entry["version"]:
            return entry["status"] == "affected"

        if ("lessThan" in entry and version_compare(entry["version"], v) <= 0 and version_compare(v, entry["lessThan"]) < 0) or (
            "lessThanOrEqual" in entry
            and version_compare(entry["version"], v) <= 0
            and version_compare(v, entry["lessThanOrEqual"]) <= 0
        ):
            status = entry["status"]
            for change in entry.get("changes", []):
                if "-" in change["at"]:  # Check if change['at'] is a hotfix
                    if change["at"] == v:  # Exact match for hotfix
                        status = change["status"]
                        break
                elif version_compare(change["at"], v) <= 0:
                    status = change["status"]
            return status == "affected"

    return default_status == "affected"


def main():
    args = demisto.args()
    pan_os_system_info_list = argToList(args.get("pan_os_system_info_list"))
    cve_json_list = argToList(args.get("cve_json"))

    try:
        for cve_json in cve_json_list:
            fw_versions_checked: list[str] = []
            gp_version_checked: list[str] = []
            result = {
                "CVE_ID": cve_json.get("cve_id", ""),
                "Severity": cve_json.get("cvethreatseverity") or cve_json.get("cvss_severity"),
                "Result": [],
            }
            affected_versions_in_cve = cve_json.get("affected_list", [])
            pan_os_affected_versions = [item for item in affected_versions_in_cve if item.get("product") == "PAN-OS"]
            gp_affected_versions = [item for item in affected_versions_in_cve if item.get("product") == "GlobalProtect App"]

            if len(pan_os_affected_versions) == 0 and len(gp_affected_versions) == 0:
                # return_results("CVE is not applicable to PAN-OS or GlobalProtect")
                for firewall in pan_os_system_info_list:
                    result["Result"].append(
                        {
                            "Hostname": firewall.get("hostname", ""),
                            "IPAddress": firewall.get("ip_address", ""),
                            "SWVersion": firewall.get("sw_version", ""),
                            "IsFirewallVersionAffected": False,
                            "GlobalProtectVersion": firewall.get("global_protect_client_package_version", ""),
                            "IsGlobalProtectVersionAffected": False,
                        }
                    )

            else:
                for firewall in pan_os_system_info_list:
                    """ USE fw_versions_checked TO SEE IF THIS VERSION WAS ALREADY CHECKED """
                    if firewall.get("sw_version") in fw_versions_checked:
                        IsFirewallVersionAffected = [
                            r.get("IsFirewallVersionAffected")
                            for r in result["Result"]
                            if r.get("SWVersion") == firewall.get("sw_version")
                        ][0]
                    else:
                        IsFirewallVersionAffected = check_fw_version(fw_versions_checked, pan_os_affected_versions, firewall)

                    """ USE gp_version_checked TO SEE IF THIS VERSION WAS ALREADY CHECKED """
                    if (
                        firewall.get("global_protect_client_package_version", "") in gp_version_checked
                        and len(result["Result"]) != 0
                    ):
                        IsGlobalProtectVersionAffected = [
                            r.get("IsGlobalProtectVersionAffected")
                            for r in result["Result"]
                            if r.get("GlobalProtectVersion") == firewall.get("global_protect_client_package_version", "")
                        ][0]
                    else:
                        IsGlobalProtectVersionAffected = check_gp_version(gp_version_checked, gp_affected_versions, firewall)

                    result["Result"].append(
                        {
                            "Hostname": firewall.get("hostname", ""),
                            "IPAddress": firewall.get("ip_address", ""),
                            "SWVersion": firewall.get("sw_version", ""),
                            "IsFirewallVersionAffected": IsFirewallVersionAffected,
                            "GlobalProtectVersion": firewall.get("global_protect_client_package_version", ""),
                            "IsGlobalProtectVersionAffected": IsGlobalProtectVersionAffected,
                        }
                    )

            context_output = result
            str_result = [{k: str(v) if isinstance(v, bool) else v for k, v in item.items()} for item in result["Result"]]
            md = tableToMarkdown(
                result["CVE_ID"],
                str_result,
                headers=[
                    "Hostname",
                    "IPAddress",
                    "SWVersion",
                    "IsFirewallVersionAffected",
                    "GlobalProtectVersion",
                    "IsGlobalProtectVersionAffected",
                ],
                is_auto_json_transform=True,
            )
            command_results = CommandResults(
                outputs_prefix="CVE_Check",
                outputs_key_field="",
                outputs=context_output,
                raw_response=context_output,
                readable_output=md,
            )

            return_results(command_results)

    except Exception as ex:
        # print the traceback
        demisto.error(traceback.format_exc())
        return_error(f"Failed to execute CheckFirewallVersionForCVEs_feed. Error: {str(ex)}")


""" ENTRY POINT """

if __name__ in ("__main__", "__builtin__", "builtins"):
    main()

README

CheckFirewallAndGPForCVEs

This script checks if PAN-OS firewall and GlobalProtect versions are affected by specific CVEs (Common Vulnerabilities and Exposures).

Description

The script analyzes firewall system information against CVE data to determine if the current PAN-OS software version or GlobalProtect client package version is vulnerable to known security issues. It implements the CVE schema algorithm for version comparison and status determination.

Inputs

Argument Name Description Required
pan_os_system_info_list System info of firewalls (array). Output of pan-os-platform-get-system-info command Required
cve_json List of CVE with detailed json (array). Output of PAN_OS_Security_Advisories_Enrichment script Required

pan_os_system_info_list Format

Each firewall entry should contain:

  • hostname: Firewall hostname
  • ip_address: Firewall IP address
  • sw_version: PAN-OS software version
  • global_protect_client_package_version: GlobalProtect client version (optional)

cve_json Format

Each CVE entry should contain:

  • cve_id: CVE identifier
  • cvethreatseverity or cvss_severity: CVE severity level
  • affected_list: List of affected products with version information

Outputs

Path Type Description
CVE_Check.CVE_ID String The CVE identifier
CVE_Check.Result.Hostname String Firewall hostname
CVE_Check.Result.IPAddress String Firewall IP address
CVE_Check.Result.SWVersion String PAN-OS software version
CVE_Check.Result.IsFirewallVersionAffected Boolean Whether PAN-OS version is affected by the CVE
CVE_Check.Result.GlobalProtectVersion String GlobalProtect client version
CVE_Check.Result.IsGlobalProtectVersionAffected Boolean Whether GlobalProtect version is affected by the CVE
CVE_Check.Severity String CVE severity level

Context Example

{
    "CVE_Check":{
        "CVE_ID": "CVE-2072-1234", 
        "Result": [
            {
                "Hostname": "fw-affected",
                "IPAddress": "1.1.1.1", 
                "SWVersion": "10.2.3",
                "IsFirewallVersionAffected": true, 
                "GlobalProtectVersion": "6.0.1",                                    
                "IsGlobalProtectVersionAffected": true                                    
            }, 
            {
                "Hostname": "fw-patched",
                "IPAddress": "2.2.2.2", 
                "SWVersion": "10.2.3-h2",
                "IsFirewallVersionAffected": false, 
                "GlobalProtectVersion": "6.0.3",                                    
                "IsGlobalProtectVersionAffected": false 
            }], 
        "Severity": "MEDIUM"
    }
}

Human Readable Output

CVE-2072-1234

Hostname IPAddress SWVersion IsFirewallVersionAffected GlobalProtectVersion IsGlobalProtectVersionAffected
fw-affected 1.1.1.1 10.2.3 True 6.0.1 True
fw-patched 2.2.2.2 10.2.3-2 False 6.0.3 False

Notes

  • The script implements the CVE schema algorithm from https://cveproject.github.io/cve-schema/schema/docs.
  • Hotfix versions require exact matches for status changes.
  • If a CVE doesn’t apply to the PAN-OS software version or the GlobalProtect client package version, the output will specify the affected status on these 2 versions. For any other installed package versions, please inspect the CVE details.