PAN-OS Policy Optimizer
Automate your AppID Adoption by using this integration together with your Palo Alto Networks Next-Generation Firewall or Panorama.
Network Security · PAN-OS Policy Optimizer (beta)
Details
| ID | PAN-OS Policy Optimizer |
|---|---|
| Provider | Palo Alto Networks |
| Category | Network Security |
| From Version | 6.0.0 |
| Docker Image | demisto/python3:3.12.13.10116658 |
| Supported Modules | Agentix XSIAM |
README
Automate your AppID Adoption by using this integration together with your Palo Alto Networks Next-Generation Firewall or Panorama.
This integration was integrated and tested with version 8 up to version 10.1.6 and version 10.2.0 of PAN-OS Policy Optimizer.
Moved to beta due to the lack of a formal API.
Configure PAN-OS Policy Optimizer (Beta) in Cortex
| Parameter | Required |
|---|---|
| Server URL (e.g., https://192.168.0.1:443) | True |
| Username | True |
| Password | True |
| Vsys - Firewall instances only | False |
| Device Group - Panorama instances only | False |
| PAN-OS Version (The exact version, e.g., 10.1.4, 1.1, 9) | False |
| Trust any certificate (not secure) | False |
| Use system proxy settings | False |
Commands
You can execute these commands from the CLI, as part of an automation, or in a playbook.
After you successfully execute a command, a DBot message appears in the War Room with the command details.
pan-os-po-get-stats
Gets the Policy Optimizer statistics.
Base Command
pan-os-po-get-stats
Input
| Argument Name | Description | Required |
|---|---|---|
| position | Whether to get pre-rules statistics or post-rules statistics. ‘pre’ for pre rules, ‘post’ for post-rules. Only for Panorama instances. Possible values are: pre, post. Default is pre. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| PanOS.PolicyOptimizer.Stats.no_app_specified | Number | Number of rules with no apps specified. |
| PanOS.PolicyOptimizer.Stats.unused | Number | Number of unused security policies. |
| PanOS.PolicyOptimizer.Stats.unused_apps | Number | Number of unused apps in security policies. |
| PanOS.PolicyOptimizer.Stats.unused_in_30_days | Number | Number of unused security policies in 30 days. |
| PanOS.PolicyOptimizer.Stats.unused_in_90_days | Number | Number of unused security policies in 90 days. |
Command Example
#### Context Example
```json
{
"PanOS": {
"PolicyOptimizer": {
"Stats": {
"no_app_specified": "1",
"unused": "8",
"unused_apps": "0",
"unused_in_30_days": "13",
"unused_in_90_days": "12"
}
}
}
}
Human Readable Output
Policy Optimizer Statistics
@name text no_app_specified 1 unused_apps 0 unused_in_30_days 13 unused_in_90_days 12 unused 8
pan-os-po-no-apps
Shows all security policies with no apps specified.
Base Command
pan-os-po-no-apps
Input
| Argument Name | Description | Required |
|---|---|---|
| position | Whether to get pre-rules with no apps or post-rules with no apps. ‘pre’ for pre rules, ‘post’ for post-rules. Only for Panorama instances. Possible values are: pre, post. Default is pre. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| PanOS.PolicyOptimizer.NoApps | Unknown | Contains information about the rules that have no apps specified. For example, Source and Destination. |
Command Example
#### Context Example
```json
{
"PanOS": {
"PolicyOptimizer": {
"NoApps": {
"@__recordInfo": "{\"permission\":\"readonly\",\"xpathId\":\"panorama\",\"vsysName\":\"vsys1\",\"position\":\"pre\"}",
"@loc": "Lab-Devices",
"@name": "pano_rule",
"@panorama": "true",
"@uuid": "uuid",
"action": "allow",
"application": {
"member": [
"any"
]
},
"apps-allowed-count": "0",
"apps-seen-count": "72",
"bytes": "84800223916",
"category": {
"member": [
"any"
]
},
"days-no-new-app-count": "193",
"description": "a test rule for the move function",
"destination": {
"member": [
"any"
]
},
"first-hit-timestamp": "1602403843",
"from": {
"member": [
"any"
]
},
"hip-profiles": {
"member": [
"any"
]
},
"hit-count": "32193134",
"last-app-seen-since-count": "193",
"last-hit-timestamp": "1602468975",
"last-reset-timestamp": "0",
"rule-creation-timestamp": "1575916248",
"rule-modification-timestamp": "1614045009",
"service": {
"member": [
"application-default"
]
},
"source": {
"member": [
"any"
]
},
"source-user": {
"member": [
"any"
]
},
"to": {
"member": [
"any"
]
}
}
}
}
}
Human Readable Output
Policy Optimizer No App Specified
@name @uuid action description source destination pano_rule uuid allow a test rule for the move function member: any member: any
pan-os-po-unused-apps
Gets the unused apps.
Base Command
pan-os-po-unused-apps
Input
| Argument Name | Description | Required |
|---|---|---|
| position | Whether to get pre-rules unused apps or post-rules unused apps. ‘pre’ for pre rules, ‘post’ for post-rules. Only for Panorama instances. Possible values are: pre, post. Default is pre. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| PanOS.PolicyOptimizer.UnusedApps | String | Shows all security rules with unused apps. |
pan-os-po-get-rules
Gets unused, used, or any rules.
Base Command
pan-os-po-get-rules
Input
| Argument Name | Description | Required |
|---|---|---|
| timeframe | The time frame in days to show the unused rules. Default is 30. | Optional |
| usage | Rule usage type to filter by. Can be Unused, Used, or Any. Possible values are: Unused, Used, Any. Default is Unused. | Optional |
| exclude | Whether to exclude rules reset during the last x days, where x is the value defined in the timeframe argument. It will not exclude rules by default. Possible values are: false, true. Default is false. | Optional |
| position | Whether to get pre-rules, post-rules or both. ‘pre’ for pre rules, ‘post’ for post-rules, only for panorama instances. Possible values are: pre, post, both. Default is both. | Optional |
| rule_type | Which type of rules to query. Possible values are: security, nat, qos, pbf, decryption, tunnel-inspect, application-override, authentication, dos, sdwan. Default is security. | Optional |
| limit | The maximum number of rules to return. Default is 200. | Optional |
| page_size | The amount of items to return in each paginated call. Can only be a value of up to 200. Default is 200. | Optional |
| page | A specific pagination page to get items from. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| PanOS.PolicyOptimizer.UnusedRules | String | Shows all unused security rules. |
| PanOS.PolicyOptimizer.AnyRules | String | Shows all security rules. |
| PanOS.PolicyOptimizer.UsedRules | String | Shows all used security rules. |
Command Example
!pan-os-po-get-rules usage=Any
Context Example
{
"PanOS": {
"PolicyOptimizer": {
"AnyRules": [
{
"@__recordInfo": "{\"permission\":\"readonly\",\"xpathId\":\"panorama\",\"vsysName\":\"vsys1\",\"position\":\"pre\"}",
"@loc": "Lab-Devices",
"@name": "tip rule",
"@panorama": "true",
"@uuid": "uuid",
"action": "allow",
"application": {
"member": [
"any"
]
},
"apps-allowed-count": "0",
"apps-seen-count": "0",
"bytes": "0",
"category": {
"member": [
"any"
]
},
"days-no-new-app-count": [],
"destination": {
"member": [
"any"
]
},
"first-hit-timestamp": "0",
"from": {
"member": [
"any"
]
},
"hip-profiles": {
"member": [
"any"
]
},
"hit-count": "0",
"last-app-seen-since-count": [],
"last-hit-timestamp": "0",
"last-reset-timestamp": "0",
"rule-creation-timestamp": "1575925916",
"rule-modification-timestamp": "1614045009",
"service": {
"member": [
"application-default"
]
},
"source": {
"member": [
"tip"
]
},
"source-user": {
"member": [
"any"
]
},
"to": {
"member": [
"any"
]
}
}
]
}
}
}
Human Readable Output
PolicyOptimizer AnyRules
@name @uuid action description source destination tip rule uuid allow member: tip member: any
pan-os-po-app-and-usage
Gets the app usage statistics for a specific security rule.
Base Command
pan-os-po-app-and-usage
Input
| Argument Name | Description | Required |
|---|---|---|
| rule_uuid | The UUID of the security rule. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| PanOS.PolicyOptimizer.AppsAndUsage | Unknown | Shows detailed app usage statistics for specific security rules. |
Command Example
!pan-os-po-app-and-usage rule_uuid=uuid
Human Readable Output
Rule with UUID:{uuid} does not use apps.
pan-os-get-dag
Gets a specific dynamic address group.
Base Command
pan-os-get-dag
Input
| Argument Name | Description | Required |
|---|---|---|
| dag | Dynamic address group name. | Required |
Context Output
There is no context output for this command.
Configuration parameters
server_url— Server URL (e.g., https://192.168.0.1:443) (required)credentials— Username (required)vsys— Vsys - Firewall instances onlydevice_group— Device Group - Panorama instances onlyversion— PAN-OS Version (The exact version, e.g., 10.1.4, 1.1, 9)insecure— Trust any certificate (not secure)proxy— Use system proxy settings
Commands (6)
-
pan-os-get-dagGets a specific dynamic address group.
-
pan-os-po-app-and-usageGets the app usage statistics for a specific security rule.
-
pan-os-po-get-rulesGets unused, used, or any rules.
-
pan-os-po-get-statsGets the Policy Optimizer statistics.
-
pan-os-po-no-appsShows all security policies with no apps specified.
-
pan-os-po-unused-appsGets the unused apps.
import pytest from CommonServerPython import * from PANOSPolicyOptimizer import ( Client, define_position, get_policy_optimizer_statistics_command, is_cms_selected, policy_optimizer_get_dag_command, policy_optimizer_get_rules_command, policy_optimizer_get_unused_apps_command, policy_optimizer_no_apps_command, ) BASE_URL = "https://test.com" def get_firewall_instance_client(): return Client(url=BASE_URL, username="test", password="test", vsys="test", device_group="", verify=False, tid=0, version="8") def get_panorama_instance_client(): return Client(url=BASE_URL, username="test", password="test", vsys="", device_group="test", verify=False, tid=0, version="8") def read_json_file(path): with open(path, encoding="utf-8") as f: return json.loads(f.read()) QUERYING_RULES_PARAMS = [ ( get_firewall_instance_client(), "main", ), ( get_panorama_instance_client(), "pre", ), ( get_panorama_instance_client(), "post", ), ] QUERYING_RULES_PARAMS_WITH_VERSION = [ (get_firewall_instance_client(), "pre", "9.0.0", "main"), (get_panorama_instance_client(), "pre", "10.2.0", "pre"), (get_panorama_instance_client(), "post", "9.0.0", "main"), ] QUERYING_RULES_PARAMS_WITH_VERSION_AND_FLAG = [ (get_firewall_instance_client(), "pre", "9.0.0", "main", False, False), (get_panorama_instance_client(), "pre", "10.2.0", "pre", True, True), (get_panorama_instance_client(), "post", "9.0.0", "main", True, False), ] @pytest.mark.parametrize("client, position", QUERYING_RULES_PARAMS) def test_body_request_is_valid_when_querying_rules(mocker, client, position): """ Given - a client. When - querying rules in firewall/panorama instances. Then - Verify that the body request that was sent is correct for each type of instance. """ mocker.patch.object(client, "token_generator", return_value="123") response = requests.Response() response._content = b"{}" response_mocker = mocker.patch.object(client.session, "post", return_value=response) client.session_metadata["headers"] = "test" policy_optimizer_get_rules_command( client=client, args={"timeframe": "30", "usage": "Unused", "exclude": "false", "rule_type": "security", "position": position}, ) assert response_mocker.call_args.kwargs["json"] == { "action": "PanDirect", "method": "run", "data": [ "123", "PoliciesDirect.getPoliciesByUsage", [ { "type": "security", "position": position, "vsysName": "test", "isCmsSelected": client.is_cms_selected, "isMultiVsys": False, "showGrouped": False, "usageAttributes": {"timeframe": "30", "usage": "Unused", "exclude": False, "exclude-reset-text": "90"}, "start": 0, "limit": 200, "pageContext": "rule_usage", } ], ], "type": "rpc", "tid": 1, } @pytest.mark.parametrize("client, position, version, excepted_position", QUERYING_RULES_PARAMS_WITH_VERSION) def test_body_request_is_valid_when_querying_policy_optimizer_statistics(mocker, client, position, version, excepted_position): """ Given - a client. When - querying policy optimizer statistics in firewall/panorama instances. Then - Verify that the body request that was sent is correct for each type of instance. case1: PAN-OS 9.0.0 should always return main. case2: Panorama 10.2.0 should will return pre, the given position argument. case3: Panorama 9.0.0 should always return main. """ client.version = version mocker.patch.object(client, "token_generator", return_value="123") response = requests.Response() response._content = b'{"result":{"result":{"entry":[{"@name":"test","text":"test"}]}}}' response_mocker = mocker.patch.object(client.session, "post", return_value=response) client.session_metadata["headers"] = "test" get_policy_optimizer_statistics_command(client=client, args={"position": position}) assert response_mocker.call_args.kwargs["json"] == { "action": "PanDirect", "method": "run", "data": [ "123", "PoliciesDirect.getRuleCountInRuleUsage", [{"type": "security", "position": excepted_position, "vsysName": "test"}], ], "type": "rpc", "tid": 1, } @pytest.mark.parametrize( "client, position, version, excepted_position, flag, expected_flag", QUERYING_RULES_PARAMS_WITH_VERSION_AND_FLAG ) def test_body_request_is_valid_when_querying_policy_optimizer_no_apps( mocker, client, position, version, excepted_position, flag, expected_flag ): """ Given - a client. When - querying policy optimizer no apps in firewall/panorama instances. Then - Verify that the body request that was sent is correct for each type of instance. case1: PAN-OS 9.0.0 should always return main, and the isCmsSelected flag should be False. case2: Panorama 10.2.0 should will return pre, the given position argument, and the isCmsSelected flag should be True. case3: Panorama 9.0.0 should always return main, and the isCmsSelected flag should be False due to the given vresion. """ client.version = version mocker.patch.object(client, "token_generator", return_value="123") response = requests.Response() response._content = b'{"result":{"result":{"entry":[{"@name":"test","text":"test"}]}}}' response_mocker = mocker.patch.object(client.session, "post", return_value=response) client.session_metadata["headers"] = "test" policy_optimizer_no_apps_command(client=client, args={"position": position}) assert response_mocker.call_args.kwargs["json"] == { "action": "PanDirect", "method": "run", "data": [ "123", "PoliciesDirect.getPoliciesByUsage", [ { "type": "security", "position": excepted_position, "vsysName": "test", "isCmsSelected": expected_flag, "isMultiVsys": False, "showGrouped": False, "usageAttributes": { "timeframeTag": "30", "application/member": "any", "apps-seen-count": "geq '1'", "action": "allow", }, "pageContext": "app_usage", "field": "$.bytes", "direction": "DESC", } ], ], "type": "rpc", "tid": 1, } @pytest.mark.parametrize( "client, position, version, excepted_position, flag, expected_flag", QUERYING_RULES_PARAMS_WITH_VERSION_AND_FLAG ) def test_body_request_is_valid_when_querying_policy_optimizer_unused_apps( mocker, client, position, version, excepted_position, flag, expected_flag ): """ Given - a client. When - querying policy optimizer unused_apps in firewall/panorama instances. Then - Verify that the body request that was sent is correct for each type of instance. case1: PAN-OS 9.0.0 should always return main, and the isCmsSelected flag should be False. case2: Panorama 10.2.0 should will return pre, the given position argument, and the isCmsSelected flag should be True. case3: Panorama 9.0.0 should always return main, and the isCmsSelected flag should be False due to the given vresion. """ client.version = version mocker.patch.object(client, "token_generator", return_value="123") response = requests.Response() response._content = b'{"result":{"result":{"entry":[{"@name":"test","text":"test"}]}}}' response_mocker = mocker.patch.object(client.session, "post", return_value=response) client.session_metadata["headers"] = "test" client.session_metadata["dit"] = 0 policy_optimizer_get_unused_apps_command(client=client, args={"position": position}) assert response_mocker.call_args.kwargs["json"] == { "action": "PanDirect", "method": "run", "data": [ "123", "PoliciesDirect.getPoliciesByUsage", [ { "type": "security", "position": excepted_position, "vsysName": "test", "serialNumber": "", "isCmsSelected": expected_flag, "isMultiVsys": False, "showGrouped": False, "usageAttributes": {"timeframeTag": "30", "application/member": "unused", "action": "allow"}, "pageContext": "app_usage", "field": "$.bytes", "direction": "DESC", } ], ], "type": "rpc", "tid": 2, } CLIENTS = [get_firewall_instance_client(), get_panorama_instance_client()] @pytest.mark.parametrize("client", CLIENTS) def test_querying_rules_is_valid(mocker, client): """ Given - a client instance and a valid mocked security rules response. When - querying rules in firewall/panorama instances. Then - Verify that the output for both cases returns expected responses. """ mocker.patch.object(client, "token_generator", return_value="123") mocker.patch.object(client.session, "post") mocker.patch.object(json, "loads", return_value=read_json_file(path="test_data/valid_security_rules_response.json")) client.session_metadata["headers"] = "test" client.is_cms_selected = False rules = policy_optimizer_get_rules_command(client=client, args={"timeframe": "30", "usage": "Unused", "exclude": "false"}) assert isinstance(rules.outputs, list) assert len(rules.outputs) > 0 assert "PolicyOptimizer Unused Security Rules" in rules.readable_output @pytest.mark.parametrize("client", CLIENTS) def test_querying_invalid_dynamic_address_group_response(mocker, client): """ Given - a response which indicates no dynamic address group was found. When - querying for a specific dynamic group. Then - an entry indicating that no dynamic address group was found. """ mocker.patch.object(client, "token_generator", return_value="123") mocker.patch.object(client.session, "post") mocker.patch.object(json, "loads", return_value=read_json_file(path="test_data/invalid_dynamic_group_response.json")) client.session_metadata["headers"] = "test" dag = policy_optimizer_get_dag_command(client=client, args={"dag": "dag_test_ag"}) assert dag.readable_output == "Dynamic Address Group dag_test_ag was not found." @pytest.mark.parametrize( "version , output", [ ("9.0.0", "f6f4061a1bddc1c04d8109b39f581270"), ("10.2.1", "590c9f8430c7435807df8ba9a476e3f1295d46ef210f6efae2043a4c085a569e"), ], ) def test_token_generator(mocker, version, output): """ Given: version of PAN-OS. When: running token_generator. Then: return the correct token. case 1: PAN-OS 9.0.0 should return a token generated with md5. case 2: PAN-OS 10.2.1 should return a token generated with sha256. """ client = get_firewall_instance_client() client.version = version client.session_metadata["cookie_key"] = "test" assert client.token_generator() == output def test_extract_csrf(): client = get_firewall_instance_client() assert ( client.extract_csrf('<input type="hidden" name="_csrf" value="422JE5PO1WARA1I91CB5FRS99UQ65RF31P9Y3L4T" />') == "422JE5PO1WARA1I91CB5FRS99UQ65RF31P9Y3L4T" ) # noqa @pytest.mark.parametrize("position_value, num_of_rules", [("both", 3), ("pre", 2), ("post", 1)]) def test_get_unused_rules(mocker, position_value, num_of_rules): """ Given: position of unused rules (pre, post or any) When: running pan-os-po-get-rules for unused rules Then: return rules based on their location """ def mock_policy_optimizer_get_rules(position: str, **kwargs): pre = {"result": {"result": {"entry": ["test1", "test2"]}}} post = {"result": {"result": {"entry": ["test3"]}}} if position == "pre": return pre else: return post client = get_panorama_instance_client() mocker.patch.object(client, "policy_optimizer_get_rules", side_effect=mock_policy_optimizer_get_rules) args = {"timeframe": "", "usage": "test", "exclude": "false", "position": position_value, "rule_type": "unused"} rules = policy_optimizer_get_rules_command(client, args).outputs assert len(rules) == num_of_rules @pytest.mark.parametrize( "version, position , is_panorama, res", [("8", "post", True, "main"), ("9", "post", False, "main"), ("10.3", "post", True, "post")], ) def test_define_position(mocker, version, position, is_panorama, res): """ Given: - version of PAN-OS. - position of the rule. - is_panorama flag. When: - running define_position. Then: - return the correct position. case 1: PAN-OS 8 should always return main. case 2: PAN-OS 9 should always return main. case 3: PAN-OS 10.3 should return post as its input. """ assert define_position(version=version, args={"position": position}, is_panorama=is_panorama) == res @pytest.mark.parametrize("version, is_panorama, res", [("8", True, False), ("9", False, False), ("10.3", True, True)]) def test_isCmsSelected(version, is_panorama, res): """ Given: - version of PAN-OS. - is_panorama flag. When: - running is_cms_selected. Then: - return the correct flag. case 1: PAN-OS 8 should always return False. case 2: PAN-OS 9 should always return False. case 3: PAN-OS 10.3 should return True. """ assert is_cms_selected(version=version, is_panorama=is_panorama) == res def test_policy_optimizer_get_rules_pagination(mocker): """ Given: a client instance and a valid mocked security rules response. When: Calling 'policy_optimizer_get_rules' using pagination parameters. Then: Ensure the request is sent with the correct parameters. """ client = get_firewall_instance_client() mocker.patch.object(client, "token_generator", return_value="123") kwargs = {"timeframe": "all", "usage": "Unused", "exclude": False, "position": "both", "rule_type": "security"} session_post_mock = mocker.patch.object(client, "session_post") client.policy_optimizer_get_rules(limit=50, page_size=200, **kwargs) assert session_post_mock.call_args.kwargs["json_cmd"]["data"][2][0]["start"] == 0 assert session_post_mock.call_args.kwargs["json_cmd"]["data"][2][0]["limit"] == 50 session_post_mock = mocker.patch.object( client, "session_post", side_effect=[ {"result": {"result": {"@count": 30, "@total-count": 100, "entry": []}}}, {"result": {"result": {"@count": 30, "@total-count": 100, "entry": []}}}, {"result": {"result": {"@count": 20, "@total-count": 100, "entry": []}}}, ], ) client.policy_optimizer_get_rules(limit=80, page_size=30, **kwargs) assert session_post_mock.call_count == 3 assert session_post_mock.call_args_list[0].kwargs["json_cmd"]["data"][2][0]["start"] == 0 assert session_post_mock.call_args_list[0].kwargs["json_cmd"]["data"][2][0]["limit"] == 30 assert session_post_mock.call_args_list[1].kwargs["json_cmd"]["data"][2][0]["start"] == 30 assert session_post_mock.call_args_list[1].kwargs["json_cmd"]["data"][2][0]["limit"] == 30 assert session_post_mock.call_args_list[2].kwargs["json_cmd"]["data"][2][0]["start"] == 60 assert session_post_mock.call_args_list[2].kwargs["json_cmd"]["data"][2][0]["limit"] == 20 session_post_mock = mocker.patch.object( client, "session_post", return_value={"result": {"result": {"@count": 10, "@total-count": 100, "entry": []}}} ) client.policy_optimizer_get_rules(limit=50, page_size=10, page=3, **kwargs) assert session_post_mock.call_count == 1 assert session_post_mock.call_args.kwargs["json_cmd"]["data"][2][0]["start"] == 20 assert session_post_mock.call_args.kwargs["json_cmd"]["data"][2][0]["limit"] == 10