Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Support for Panorama/PAN-OS Master Key Rotation #37290

Open
wants to merge 16 commits into
base: master
Choose a base branch
from
Open
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
118 changes: 117 additions & 1 deletion Packs/PAN-OS/Integrations/Panorama/Panorama.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@

''' IMPORTS '''
import uuid
from typing import Tuple, Callable, ValuesView, Iterator, TYPE_CHECKING
from typing import Tuple, Callable, ValuesView, Iterator, Literal, TYPE_CHECKING
from urllib.parse import urlparse

if TYPE_CHECKING:
Expand Down Expand Up @@ -14511,6 +14511,116 @@ def pan_os_list_profile_exception_command(args: dict) -> CommandResults:
)


def build_master_key_create_or_update_cmd(args: dict, action: Literal['create', 'update']) -> str:
"""Builds the XML command for creating or updating the default master key on Panorama / PAN-OS.

Args:
args (dict): The command arguments.
action ('create'| 'update'): Whether to create a new master key or update an existing one.

Returns:
str: XML string of the master key create or update command.
"""
master_key_args = [
add_argument(arg=args.get('lifetime_in_hours'), field_name='lifetime', member=False),
add_argument(arg=args.get('reminder_in_hours'), field_name='reminder', member=False),
]
if action == 'create':
master_key_args.append(
add_argument(arg=args.get('master_key'), field_name='new-master-key', member=False),
)
else:
master_key_args.extend(
[
add_argument(arg=args.get('new_master_key'), field_name='new-master-key', member=False),
add_argument(arg=args.get('current_master_key'), field_name='current-master-key', member=False),
]
)

master_key_args.append(add_argument_yes_no(arg='no', field_name='on-hsm'))
master_key_element = add_argument(arg=''.join(master_key_args), field_name='master-key', member=False)

return add_argument(arg=master_key_element, field_name='request', member=False)


def create_or_update_master_key(args: dict, action: Literal['create', 'update']) -> CommandResults:
"""Builds an XML command and sends a request to create or update the master key based on the given action.

Args:
args (dict): The command arguments.
action ('create'| 'update'): Whether to create a new master key or update an existing one.

Returns:
CommandResults: Contains readable output and raw response.
"""
master_key_cmd = build_master_key_create_or_update_cmd(args, action=action)
raw_response: dict = http_request(URL, 'GET', params={'type': 'op', 'key': API_KEY, 'cmd': master_key_cmd})
response_result = dict_safe_get(raw_response, ('response', 'result')) # human readable message

# Creating or updating the encryption master key by definition invalidates the current API key, refer to the integration docs.
demisto.info(f'The master key of {URL} has been {action}d. The current API key has been invalidated.')

return CommandResults(
readable_output=f'{response_result}. The current API key has been invalidated. Generate a new API key and ensure the integration instance is updated accordingly.',
raw_response=raw_response,
)


def pan_os_create_master_key_command(args: dict) -> CommandResults:
"""Creates a new default master key on Panorama / PAN-OS.

Args:
args (dict): The command arguments.

Returns:
CommandResults: Contains readable output and raw response.
"""
return create_or_update_master_key(args, action='create')


def pan_os_update_master_key_command(args: dict) -> CommandResults:
"""Updates the default master key on Panorama / PAN-OS.

Args:
args (dict): The command arguments.

Returns:
CommandResults: Contains readable output and raw response.
"""
return create_or_update_master_key(args, action='update')


def pan_os_get_master_key_details_command() -> CommandResults:
"""Shows the details of the default master key on Panorama / PAN-OS.

Args:
args (dict): The command arguments.

Returns:
CommandResults: Contains context output, readable output, and raw response.
"""
system_element = add_argument(arg='<masterkey-properties/>', field_name='system', member=False)
show_master_key_cmd = add_argument(arg=system_element, field_name='show', member=False)

raw_response: dict = http_request(URL, 'GET', params={'type': 'op', 'key': API_KEY, 'cmd': show_master_key_cmd})
response_result = dict_safe_get(raw_response, ('response', 'result'), default_return_value={})

result_to_human_readable = {'auto-renew-mkey': 'Auto-renew master key', "on-hsm": "Stored on HSM"}
human_readable = tableToMarkdown(
'Master Key Details',
response_result,
headers=['auto-renew-mkey', 'on-hsm', 'remind-at', 'expire-at'],
headerTransform=lambda key: result_to_human_readable.get(key, ' '.join(key.split('-')).capitalize()),
)

return CommandResults(
outputs_prefix='Panorama.MasterKey',
outputs=response_result,
raw_response=raw_response,
readable_output=human_readable,
)


""" Fetch Incidents """


Expand Down Expand Up @@ -15695,6 +15805,12 @@ def main(): # pragma: no cover
return_results(pan_os_delete_profile_exception_command(args))
elif command == 'pan-os-list-profile-exception':
return_results(pan_os_list_profile_exception_command(args))
elif command == 'pan-os-create-master-key':
return_results(pan_os_create_master_key_command(args))
elif command == 'pan-os-update-master-key':
return_results(pan_os_update_master_key_command(args))
elif command == 'pan-os-get-master-key-details':
return_results(pan_os_get_master_key_details_command())
else:
raise NotImplementedError(f'Command {command} is not implemented.')
except Exception as err:
Expand Down
76 changes: 75 additions & 1 deletion Packs/PAN-OS/Integrations/Panorama/Panorama.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9726,7 +9726,81 @@ script:
- contextPath: Panorama.Spyware.Exception.packet-capture
description: The exception packet capture.
type: String
dockerimage: demisto/pan-os-python:1.0.0.108041
- name: pan-os-get-master-key-details
description: Show the details of the default master key that encrypts all the private keys and passwords in the configuration.
outputs:
- contextPath: Panorama.MasterKey.auto-renew-mkey
type: String
description: Whether the master key will be automatically renewed on expiry.
- contextPath: Panorama.MasterKey.expire-at
type: String
description: The date and time when the key is set to expire.
- contextPath: Panorama.MasterKey.hours-to-expiry
type: String
description: The number of hours remaining before the key expires.
- contextPath: Panorama.MasterKey.hours-to-reminder
type: String
description: The number of hours remaining before being notified that the key is set to expire.
- contextPath: Panorama.MasterKey.minutes-to-expiry
type: String
description: The number of minutes remaining before the key expires.
- contextPath: Panorama.MasterKey.minutes-to-reminder
type: String
description: The number of minutes remaining before being notified that the key is set to expire.
- contextPath: Panorama.MasterKey.on-hsm
type: String
description: Whether the key is stored on a Hardware Security Module (HSM).
- contextPath: Panorama.MasterKey.remind-at
type: String
description: The date and time when to be notified that the key is set to expire.
- contextPath: Panorama.MasterKey.seconds-to-expiry
type: String
description: The number of seconds remaining before the key expires.
- contextPath: Panorama.MasterKey.seconds-to-reminder
type: String
description: The number of seconds remaining before being notified that the key is set to expire.
- name: pan-os-create-master-key
description: Create a default master key that encrypts all the private keys and passwords in the configuration.
arguments:
- description: The encryption master key. Must be exactly 16 characters.
name: master_key
type: String
required: true
isArray: false
- description: The lifetime of the key in hours.
name: lifetime_in_hours
type: Number
required: true
isArray: false
- description: The time to be notified of the key's expiration in hours.
name: reminder_in_hours
type: Number
required: true
isArray: false
- name: pan-os-update-master-key
description: Update the default master key that encrypts all the private keys and passwords in the configuration.
arguments:
- description: The new encryption master key. Must be exactly 16 characters.
name: new_master_key
type: String
required: true
isArray: false
- description: The current encryption master key.
name: current_master_key
type: String
required: true
isArray: false
- description: The lifetime of the new key in hours.
name: lifetime_in_hours
type: Number
required: true
isArray: false
- description: The time to be notified of new the new key's expiration in hours.
name: reminder_in_hours
type: Number
required: true
isArray: false
dockerimage: demisto/pan-os-python:1.0.0.117206
isfetch: true
runonce: false
script: ''
Expand Down
8 changes: 6 additions & 2 deletions Packs/PAN-OS/Integrations/Panorama/Panorama_description.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
The integration uses the Panorama XML API.
To obtain an API Key, run the following REST command and copy the key:
https://[PanoramaIP]/api/?type=keygen&user=[user]&password=[password]
To obtain an API key, run the following CURL command and copy the key:
```shell
-curl -H "Content-Type: application/x-www-form-urlencoded" -X POST https://[PanoramaIP]/api/\?type\=keygen -d 'user=[user]&password=[password]'
```

***Creating or updating the encryption master key of Palo Alto Networks Firewall or Panorama invalidates the current API key and requires obtaining a new one. All subsequent commands will raise an "Invalid Credentials" error until a new API key is obtained and the integration instance is updated accordingly.***

For more information, visit the [Palo Alto Networks documentation](https://docs.paloaltonetworks.com/panorama).

Expand Down
101 changes: 101 additions & 0 deletions Packs/PAN-OS/Integrations/Panorama/Panorama_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import pytest
import requests_mock
from pytest_mock import MockerFixture
from requests_mock.mocker import Mocker as RequestsMock

import demistomock as demisto
from unittest.mock import patch, MagicMock
Expand Down Expand Up @@ -7945,3 +7946,103 @@ def test_fetch_incidents_offset(mocker: MockerFixture):
assert last_id_dict == LastIDs(Correlation=10)
assert max_fetch_dict == MaxFetch(Correlation=5)
assert offset_dict == Offset(Correlation=2)


def test_build_master_key_create_or_update_cmd():
"""
Given:
- Command arguments for updating Panorama / PAN-OS master key

When:
- Calling build_master_key_create_or_update_cmd.

Assert:
- Correct XML command string.
"""
from Panorama import build_master_key_create_or_update_cmd

# Set
args = {
'current_master_key': 'MyFakeMasterKey1',
'new_master_key': 'MyFakeMasterKey2',
'lifetime_in_hours': '2160',
'reminder_in_hours': '1992',
}
# Arrange
cmd = build_master_key_create_or_update_cmd(args, action='update')

# Assert
assert cmd == (
'<request><master-key><lifetime>2160</lifetime><reminder>1992</reminder>'
'<new-master-key>MyFakeMasterKey2</new-master-key>'
'<current-master-key>MyFakeMasterKey1</current-master-key>'
'<on-hsm>no</on-hsm></master-key></request>'
)


def test_pan_os_create_master_key_command(requests_mock: RequestsMock):
"""
Given:
- Command arguments for creating Panorama / PAN-OS master key

When:
- Calling pan_os_create_master_key_command.

Assert:
- Correct human readable output and raw response.
"""
from Panorama import pan_os_create_master_key_command, xml2json
import Panorama

# Set
args = {'master_key': 'MyFakeMasterKey1', 'lifetime_in_hours': '2160', 'reminder_in_hours': '1992'}
Panorama.URL = 'https://1.1.1.1:443/api/'

xml_root = load_xml_root_from_test_file('test_data/create_master_key.xml')
response_result = xml_root.find('result').text

xml_response_text = ElementTree.tostring(xml_root, encoding='unicode')
requests_mock.get(Panorama.URL, text=xml_response_text)

# Arrange
command_results: CommandResults = pan_os_create_master_key_command(args)

# Assert
assert command_results.readable_output == (
f'{response_result}. The current API key has been invalidated. '
'Generate a new API key and ensure the integration instance is updated accordingly.'
)
assert command_results.raw_response == json.loads(xml2json(xml_response_text))


def test_pan_os_get_master_key_details_command(mocker: MockerFixture, requests_mock: RequestsMock):
"""
When:
- Calling pan_os_get_master_key_command.

Assert:
- Correct human readable, context output, and raw response.
"""
from Panorama import pan_os_get_master_key_details_command, xml2json
import Panorama

# Set
Panorama.URL = 'https://1.1.1.1:443/api/'

xml_root = load_xml_root_from_test_file('test_data/get_master_key.xml')
xml_response_text = ElementTree.tostring(xml_root, encoding='unicode')
requests_mock.get(Panorama.URL, text=xml_response_text)

table_to_markdown = mocker.patch('Panorama.tableToMarkdown')

# Arrange
command_results: CommandResults = pan_os_get_master_key_details_command()
table_name: str = table_to_markdown.call_args[0][0]
table_data: dict = table_to_markdown.call_args[0][1]
raw_response: dict = json.loads(xml2json(xml_response_text))

# Assert
assert table_name == 'Master Key Details'
assert table_data == raw_response['response']['result']
assert command_results.outputs == raw_response['response']['result']
assert command_results.raw_response == raw_response
Loading
Loading