Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 74 additions & 51 deletions Packs/PAN-OS/Integrations/Panorama/Panorama.py
Original file line number Diff line number Diff line change
Expand Up @@ -6594,60 +6594,83 @@ def panorama_check_latest_dynamic_update_command(args: dict):
target = args.get("target")
outdated_item_count = 0
outputs = {}

if not VSYS and not target:
# When the VSYS param is not set it meams that this is a panorama instance -> user must specify a target FW
raise DemistoException(
f"When running from a Panorama instance, you must specify the target argument. "
F"Set target to the serial number of the Panorama-managed firewall you want to check updates for."
)

for update_type in DynamicUpdateType:
# Call firewall API to check for the latest available update of each type
result = panorama_check_latest_dynamic_update_content(update_type, target)

if "result" in result["response"] and result["response"]["@status"] == "success":
versions = result["response"]["result"]["content-updates"]["entry"]

# Ensure versions is a list even if there's only one entry
if not isinstance(versions, list):
versions = [versions]

latest_version = {}
current_version = {}
latest_version_parts = (0, 0)

# Identify the latest available version and what is currently installed
for entry in versions:
# Find current version
if entry.get("current") == "yes" or entry.get("installing") == "yes":
current_version = entry

# Parse version parts as integers for proper comparison
version_str = entry.get("version", "")
if "-" in version_str:
major, minor = version_str.split("-")
version_parts = (int(major), int(minor))

# Check if this is the latest version
if version_parts > latest_version_parts:
latest_version_parts = version_parts
latest_version = entry

# Check if currently installed is the most recent available
is_up_to_date = current_version.get("version") == latest_version.get("version")

context_prefix = DynamicUpdateContextPrefixMap.get(update_type)

if not is_up_to_date:
outdated_item_count += 1

# Add both latest and current versions to the output
outputs[context_prefix] = {
"LatestAvailable": latest_version,
"CurrentlyInstalled": current_version,
"IsUpToDate": is_up_to_date,
}
else:
# Raise error if API call failed
raise DemistoException(
f"Failed to retrieve dynamic update information for {update_type.value}.\nAPI response:\n"
f"{result['response']['msg']}"
)

try:
result = panorama_check_latest_dynamic_update_content(update_type, target)

if "result" in result["response"] and result["response"]["@status"] == "success":
versions = result.get("response", {}).get("result", {}).get("content-updates", {}).get("entry",[])
if not versions: # firewall probably doesn't have app/threat or Antivirus or WildFire or GP installed
demisto.debug(f"No available updates (Firewall probably doesn't have any {update_type.value} installed).")

# Ensure versions is a list even if there's only one entry
if not isinstance(versions, list):
versions = [versions]

latest_version = {}
current_version = {}
latest_version_parts = (0, 0)

# Identify the latest available version and what is currently installed
for entry in versions:
# Find current version
if entry.get("current") == "yes" or entry.get("installing") == "yes":
current_version = entry

# Parse version parts as integers for proper comparison
version_str = entry.get("version", "")
if "-" in version_str:
major, minor = version_str.split("-")
version_parts = (int(major), int(minor))

# Check if this is the latest version
if version_parts > latest_version_parts:
latest_version_parts = version_parts
latest_version = entry

# Check if currently installed is the most recent available
is_up_to_date = False
if current_version and latest_version:
is_up_to_date = current_version.get("version") == latest_version.get("version")

context_prefix = DynamicUpdateContextPrefixMap.get(update_type)

if not is_up_to_date:
outdated_item_count += 1

# Add both latest and current versions to the output
outputs[context_prefix] = {
"LatestAvailable": latest_version,
"CurrentlyInstalled": current_version,
"IsUpToDate": is_up_to_date,
}
else:
# Raise error if API call failed
raise DemistoException(
f"Failed to retrieve dynamic update information for {update_type.value}.\nAPI response:\n"
f"{result['response']['msg']}"
)
except Exception as e:
if "There is no Global Protext Gateway license on the box" in str(e):
outputs["GP"] = {
"LatestAvailable": {"version": "An Error received from Panorama API: 'There is no Global Protect Gateway license on the box.'"},
"CurrentlyInstalled": {},
"IsUpToDate": False,
}
continue
else:
raise e


outputs["ContentTypesOutOfDate"] = {"Count": outdated_item_count}

# Create summary table for human-readable output
Expand Down
4 changes: 2 additions & 2 deletions Packs/PAN-OS/Integrations/Panorama/Panorama.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6868,7 +6868,7 @@ script:
- arguments:
- description: Serial number of the firewall on which to run the command. Use only for a Panorama instance.
name: target
description: Checks for the latest available dynamic update versions and returns a list of latest available / currently installed content.
description: Checks for the latest available dynamic update versions and returns a list of latest available / currently installed content. When running from a Panorama instance, the 'target' argument must be specified.
name: pan-os-check-dynamic-updates-status
outputs:
- contextPath: Panorama.DynamicUpdates.AntiVirus.CurrentlyInstalled.app-version
Expand Down Expand Up @@ -7247,7 +7247,7 @@ script:
description: Version of the dynamic update package.
type: String
- arguments:
- description: Serial number of the firewall on which to run the command. Use only for a Panorama instance.
- description: Serial number of the firewall on which to run the command. Mandatory for Panorama instances.
name: target
- description: Job ID for a running download process. Used for status polling.
name: job_id
Expand Down
146 changes: 145 additions & 1 deletion Packs/PAN-OS/Integrations/Panorama/Panorama_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -8623,7 +8623,7 @@

mock_return_results = mocker.patch("Panorama.return_results")

panorama_check_latest_dynamic_update_command({"args": {"target": "1337"}})
panorama_check_latest_dynamic_update_command({"target": "1337"})

# Prepare results for comparison
returned_commandresults: CommandResults = mock_return_results.call_args[0][0]
Expand All @@ -8637,7 +8637,151 @@

assert returned_commandresults.outputs == expected_returned_output
assert returned_commandresults.readable_output == expected_returned_readable


def test_panorama_check_latest_dynamic_update_panorama_instance(self):
"""
Given:
- A panorama instance (VSYS instance parameter is set to '').
- target argument is not specified (set to '').

When:
- Calling the panorama_check_latest_dynamic_update_command.

Then:
- Verify that an exception is raised askung the user to specify a target firewall
when running from a Panorama instance.

"""
from Panorama import panorama_check_latest_dynamic_update_command

# VSYS global variable is set to '' by default, which means we are simulating a run from a Panorama instance.

with pytest.raises(DemistoException) as e:

Check failure on line 8660 in Packs/PAN-OS/Integrations/Panorama/Panorama_test.py

View workflow job for this annotation

GitHub Actions / pre-commit / pre-commit

TestDynamicUpdateCommands.test_panorama_check_latest_dynamic_update_panorama_instance requests.exceptions.ConnectionError: HTTPSConnectionPool(host='1.1.1.1', port=443): Max retries exceeded with url: /api/ (Caused by NewConnectionError('<urllib3.connection.HTTPSConnection object at 0x7f072fc16a50>: Failed to establish a new connection: [Errno 101] Network unreachable'))
panorama_check_latest_dynamic_update_command({"target": ''})

assert 'When running from a Panorama instance, you must specify the target argument.' in str(e.value)


def test_panorama_check_latest_dynamic_update_command_empty_response(self, mocker):
"""
Tests the scenario were the response from the 'panorama_check_latest_dynamic_update_content' api call, includes no
entries - which means that the Firewall probably doesn't have any App/Threat or Antivirus, or Wildfire or GP
installed.

Given:
- a Panorama instance.
- Mock response for the 'panorama_check_latest_dynamic_update_content' api call, with no entries.

When:
- Calling the panorama_check_latest_dynamic_update_command.

Then:
- Verify that the outputs include no versions (all versions set to N/A).
- Verify that the expected debug log is created.

"""
from Panorama import panorama_check_latest_dynamic_update_command

mocker.patch("Panorama.panorama_check_latest_dynamic_update_content", return_value={"response": {
"@status": "success",
"result": {
"content-updates": {
"@last-updated-at": "2025/06/11 13:40:16 PDT",
"entry": [
]
}
}
}
})


mock_return_results = mocker.patch("Panorama.return_results")
mock_debug = mocker.patch.object(demisto, "debug")

panorama_check_latest_dynamic_update_command({"target": "1337"})

# Prepare results for comparison
returned_commandresults: CommandResults = mock_return_results.call_args[0][0]
expected_returned_output = {'Content': {'LatestAvailable': {}, 'CurrentlyInstalled': {}, 'IsUpToDate': False},
'AntiVirus': {'LatestAvailable': {}, 'CurrentlyInstalled': {}, 'IsUpToDate': False},
'WildFire': {'LatestAvailable': {}, 'CurrentlyInstalled': {}, 'IsUpToDate': False},
'GP': {'LatestAvailable': {}, 'CurrentlyInstalled': {}, 'IsUpToDate': False},
'ContentTypesOutOfDate': {'Count': 4}}
expected_returned_readable = ('### Dynamic Update Status Summary\n|Update Type|Is Up To Date|Latest Available '
'Version|Currently Installed Version|\n|---|---|---|---|\n| Content | False | N/A | N/A |'
'\n| AntiVirus | False | N/A | N/A |\n| WildFire | False | N/A | N/A |\n| GP | False | N/A '
'| N/A |\n\n\n**Total Content Types Outdated: 4**')

assert mock_debug.call_args[0][0] == ("No available updates "
"(Firewall probably doesn't have any global-protect-clientless-vpn installed).")
assert returned_commandresults.outputs == expected_returned_output
assert returned_commandresults.readable_output == expected_returned_readable


def test_panorama_check_latest_dynamic_update_command_no_gp_license(self, mocker):
"""
Tests the scenario were there is no Global Protect license on the Firewall.

Given:
- a Panorama instance.
- Mock response for the 'panorama_check_latest_dynamic_update_content' api call, with an exception regfarding the
GP missing license.

When:
- Calling the panorama_check_latest_dynamic_update_command.

Then:
- Verify that the outputs include the versions of the content, wildfire and antivirus, and the error message
egarding the gp missing license.
- Verify that the human readable output includes the versions of the versions of the content, wildfire and
antivirus, and the error message regarding the gp missing license.
"""

from Panorama import panorama_check_latest_dynamic_update_command

# Side-effect function to return the proper NGFW API response for the requested dynamic update type
def side_effect_function(update_type, target):
if update_type.name == "APP_THREAT":
return load_json("test_data/pan-os-check-latest-dynamic-update-status_apiresponse_app-threat.json")

elif update_type.name == "ANTIVIRUS":
return load_json("test_data/pan-os-check-latest-dynamic-update-status_apiresponse_antivirus.json")

elif update_type.name == "WILDFIRE":
return load_json("test_data/pan-os-check-latest-dynamic-update-status_apiresponse_wildfire.json")

elif update_type.name == "GP":
raise Exception("There is no Global Protext Gateway license on the box")

else:
return None

mock_api_call = mocker.patch("Panorama.panorama_check_latest_dynamic_update_content")
mock_api_call.side_effect = side_effect_function

mock_return_results = mocker.patch("Panorama.return_results")

panorama_check_latest_dynamic_update_command({"target": "1337"})

# Prepare results for comparison
returned_commandresults: CommandResults = mock_return_results.call_args[0][0]
expected_returned_output = load_json("test_data/pan-os-check-latest-dynamic-update-status_expected-returned-outputs-no-gp"
"-license.json")
expected_returned_readable = (
"### Dynamic Update Status Summary\n|Update Type|Is Up To Date|Latest Available "
"Version|Currently Installed Version|\n|---|---|---|---|\n| Content | True | 8987-9481 | 8987-9481 |\n| "
"AntiVirus | False | 5212-5732 | 5211-5731 |\n| WildFire | False | 986250-990242 | 986026-990018 |\n| GP | "
"False | An Error received from Panorama API: 'There is no Global Protect Gateway license on the box.' |"
" N/A |\n\n\n**Total Content Types Outdated: 2**"
)

assert returned_commandresults.outputs == expected_returned_output
assert returned_commandresults.readable_output == expected_returned_readable




@pytest.mark.parametrize(
"update_phase, job_id, api_response_payload",
[
Expand Down
3 changes: 2 additions & 1 deletion Packs/PAN-OS/Integrations/Panorama/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9894,6 +9894,7 @@ Gathers the name, expiration date, and expiration status of certificates configu

***
Checks for the latest available dynamic update versions and returns a list of latest available / currently installed content.
When running from a Panorama instance, the `target` argument must be specified.

#### Base Command

Expand All @@ -9903,7 +9904,7 @@ Checks for the latest available dynamic update versions and returns a list of la

| **Argument Name** | **Description** | **Required** |
| --- | --- | --- |
| target | Serial number of the firewall on which to run the command. Use only for a Panorama instance. | Optional |
| target | Serial number of the firewall on which to run the command. Mandatory for Panorama instances. | Optional |

#### Context Output

Expand Down
Loading
Loading