AzureIdentifyNSGExposureRule

Identifies the first inbound Allow rule in a specified NSG that matches network exposure criteria. Returns rule details and a list of available priority numbers for inserting higher-priority rules.

python · Cloud Security Policy Management

Details

IDAzureIdentifyNSGExposureRule
Languagepython
From Version6.10.0
Docker Imagedemisto/python3:3.12.13.10116658

README

This script identifies the first inbound Allow rule in the specified NSG that matches the criteria for network exposure. It returns details about the identified rule as well as a list of available priority numbers to insert new security rules with a higher priority.

Script Data


Name Description
Script Type python3
Cortex XSOAR Version 6.10.0

Inputs


Argument Name Description
subscription_id The Azure Subscription ID where the NSG resides.
resource_group_name The Azure Resource Group Name where the NSG resides.
network_security_group_name The Azure Network Security Group (NSG) Name to analyze for exposure rules.
private_ip_addresses The destination private IP address(es) of the Virtual Machine interface.
port TCP/UDP port to be restricted.
protocol Protocol of the port to be restricted.
priority_count The number of available priority values below the matching rule to return.
integration_instance The Azure Integration Instance to use.

Outputs


Path Description Type
AzurePublicExposure.MatchingRuleName The name of the matching inbound security rule in the NSG that allows exposure. String
AzurePublicExposure.MatchingRulePriority The priority number of the matching inbound security rule. Number
AzurePublicExposure.NextAvailablePriorityValues The next available priority values to insert new security rules before the matching rule, given in descending order. Unknown
AzurePublicExposure.IntegrationInstance The Azure Integration Instance used for identification. String
import demistomock as demisto  # noqa: F401
import pytest
import json
from CommonServerPython import DemistoException
import ipaddress


def util_load_json(path):
    with open(path) as f:
        return json.loads(f.read())


def test_get_nsg_rules(mocker):
    """
    Tests that the get_nsg_rules function correctly identifies valid command responses and returns a correctly
    formatted and ordered list of NSG rules.
    """
    from AzureIdentifyNSGExposureRule import get_nsg_rules

    command_response = util_load_json("./test_data/command_response.json")

    mocker.patch.object(demisto, "executeCommand", return_value=command_response)

    sorted_rules_result, instance_to_use_result = get_nsg_rules("fake-subscription-id", "fake-resource-group", "test-nsg", "")

    # Validate that the response contains 5 rules
    assert len(sorted_rules_result) == 5

    # Validate that the correct integration instance's response was identified
    assert instance_to_use_result == "azure-correct-instance"

    # Validate that nsg rule entries are sorted in ascending order by priority
    priorities = [rule["properties"]["priority"] for rule in sorted_rules_result]
    assert priorities == sorted(priorities), f"Rules are not sorted by priority. Found order: {priorities}"


def test_find_available_priorities_success():
    """
    Test find_available_priorities function with valid inputs that should return available priorities.
    """
    from AzureIdentifyNSGExposureRule import find_available_priorities

    nsg_rules = util_load_json("./test_data/sorted_nsg_rules.json")

    # Test finding 2 available priorities before priority 105
    # Available should be 104 and 101 (counting down from 104)
    result = find_available_priorities(105, nsg_rules, 2)

    assert len(result) == 2
    assert 104 in result
    assert 101 in result
    # Ensure none of the existing priorities are returned
    existing_priorities = [102, 103, 105, 107, 117]
    for priority in result:
        assert priority not in existing_priorities


def test_find_available_priorities_single():
    """
    Test find_available_priorities function when requesting a single priority.
    """
    from AzureIdentifyNSGExposureRule import find_available_priorities

    nsg_rules = util_load_json("./test_data/sorted_nsg_rules.json")

    # Test finding 1 available priority before priority 107
    # Available should be 106
    result = find_available_priorities(107, nsg_rules, 1)

    assert len(result) == 1
    assert result[0] == 106


def test_find_available_priorities_insufficient():
    """
    Test find_available_priorities function when there aren't enough available priorities.
    """
    from AzureIdentifyNSGExposureRule import find_available_priorities

    # Create rules that occupy most priorities near 100
    nsg_rules = util_load_json("./test_data/sorted_nsg_rules.json")

    # Try to find 5 available priorities before 105 when there aren't enough
    with pytest.raises(DemistoException) as exc_info:
        find_available_priorities(105, nsg_rules, 5)

    assert "Requested 5 available priority values, but only found" in str(exc_info.value)


def test_find_available_priorities_edge_case_near_limit():
    """
    Test find_available_priorities function near the lower limit of 100.
    """
    from AzureIdentifyNSGExposureRule import find_available_priorities

    nsg_rules = util_load_json("./test_data/sorted_nsg_rules.json")

    # Find available priorities before 103 (should find 101 and 100)
    result = find_available_priorities(103, nsg_rules, 2)

    assert len(result) == 2
    assert 101 in result
    assert 100 in result


def test_process_nsg_info_success(mocker):
    """
    Test process_nsg_info function with valid arguments.
    """
    from AzureIdentifyNSGExposureRule import process_nsg_info

    nsg_rules = util_load_json("./test_data/sorted_nsg_rules.json")

    # Mock the get_nsg_rules function
    mocker.patch("AzureIdentifyNSGExposureRule.get_nsg_rules", return_value=(nsg_rules, "azure-correct-instance"))

    # Mock find_matching_rule function (assuming it exists)
    mocker.patch("AzureIdentifyNSGExposureRule.find_matching_rule", return_value=("AllowSshToAll", 102))

    args = {
        "subscription_id": "fake-subscription-id",
        "resource_group_name": "fake-resource-group",
        "network_security_group_name": "test-nsg",
        "private_ip_addresses": "1.2.3.4",
        "port": "22",
        "protocol": "TCP",
        "priority_count": "2",
        "integration_instance": "",
    }

    result = process_nsg_info(args)

    expected_result = {
        "MatchingRuleName": "AllowSshToAll",
        "MatchingRulePriority": 102,
        "NextAvailablePriorityValues": [101, 100],
        "IntegrationInstance": "azure-correct-instance",
    }

    # Validate CommandResults structure
    assert result.outputs == expected_result


def test_process_nsg_info_multiple_ips(mocker):
    """
    Test process_nsg_info function with multiple IP addresses.
    """
    from AzureIdentifyNSGExposureRule import process_nsg_info

    nsg_rules = util_load_json("./test_data/sorted_nsg_rules.json")

    # Mock the get_nsg_rules function
    mocker.patch("AzureIdentifyNSGExposureRule.get_nsg_rules", return_value=(nsg_rules, "azure-correct-instance"))

    # Mock find_matching_rule function
    mocker.patch("AzureIdentifyNSGExposureRule.find_matching_rule", return_value=("AllowRDPtoJumpBoxes", 103))

    args = {
        "subscription_id": "fake-subscription-id",
        "resource_group_name": "fake-resource-group",
        "network_security_group_name": "test-nsg",
        "private_ip_addresses": ["1.2.3.4", "1.2.3.5"],
        "port": "3389",
        "protocol": "TCP",
        "priority_count": "1",
        "integration_instance": "",
    }

    result = process_nsg_info(args)

    # Should succeed with multiple IPs
    expected_results = {
        "MatchingRuleName": "AllowRDPtoJumpBoxes",
        "MatchingRulePriority": 103,
        "NextAvailablePriorityValues": [101],
        "IntegrationInstance": "azure-correct-instance",
    }

    assert result.outputs == expected_results


def test_process_nsg_info_comma_separated_ips(mocker):
    """
    Test process_nsg_info function with comma-separated IP addresses in a string.
    """
    from AzureIdentifyNSGExposureRule import process_nsg_info

    nsg_rules = util_load_json("./test_data/sorted_nsg_rules.json")

    # Mock the get_nsg_rules function
    mocker.patch("AzureIdentifyNSGExposureRule.get_nsg_rules", return_value=(nsg_rules, "azure-correct-instance"))

    # Mock find_matching_rule function
    mocker.patch("AzureIdentifyNSGExposureRule.find_matching_rule", return_value=("AllowHTTPServices", 105))

    args = {
        "subscription_id": "fake-subscription-id",
        "resource_group_name": "fake-resource-group",
        "network_security_group_name": "test-nsg",
        "private_ip_addresses": "1.2.3.4, 1.2.3.5, 1.2.3.6",
        "port": "80",
        "protocol": "TCP",
        "priority_count": "1",
        "integration_instance": "",
    }

    result = process_nsg_info(args)

    expected_result = {
        "MatchingRuleName": "AllowHTTPServices",
        "MatchingRulePriority": 105,
        "NextAvailablePriorityValues": [104],
        "IntegrationInstance": "azure-correct-instance",
    }

    # Should succeed with comma-separated IPs
    assert result.outputs == expected_result


def test_process_nsg_info_invalid_ips():
    """
    Test process_nsg_info function with invalid IP addresses.
    """
    from AzureIdentifyNSGExposureRule import process_nsg_info

    args = {
        "subscription_id": "fake-subscription-id",
        "resource_group_name": "fake-resource-group",
        "network_security_group_name": "test-nsg",
        "private_ip_addresses": "invalid-ip",
        "port": "22",
        "protocol": "TCP",
        "priority_count": "2",
        "integration_instance": "",
    }

    with pytest.raises(ValueError) as exc_info:
        process_nsg_info(args)

    assert str(exc_info.value) == "Invalid IP address provided: invalid-ip"


def test_process_nsg_info_empty_ips():
    """
    Test process_nsg_info function with empty IP addresses.
    """
    from AzureIdentifyNSGExposureRule import process_nsg_info

    args = {
        "subscription_id": "fake-subscription-id",
        "resource_group_name": "fake-resource-group",
        "network_security_group_name": "test-nsg",
        "private_ip_addresses": "",
        "port": "22",
        "protocol": "TCP",
        "priority_count": "2",
        "integration_instance": "",
    }

    with pytest.raises(ValueError) as exc_info:
        process_nsg_info(args)

    assert "At least one valid IP address must be provided" in str(exc_info.value)


def test_port_matches_range_single_port():
    """
    Test _port_matches_range function with a single port.
    """
    from AzureIdentifyNSGExposureRule import _port_matches_range

    result = _port_matches_range(80, "80")
    assert result is True


def test_port_matches_range_multiple_ports():
    """
    Test _port_matches_range function with a multiple individual ports.
    """
    from AzureIdentifyNSGExposureRule import _port_matches_range

    result = _port_matches_range(80, "80,443")
    assert result is True


def test_port_matches_range_multiple_ports_with_range():
    """
    Test _port_matches_range function with multiple ports and ranges
    """
    from AzureIdentifyNSGExposureRule import _port_matches_range

    result = _port_matches_range(80, "22, 79-81")
    assert result is True


def test_ip_matches_prefix_in_cidr():
    """
    Test _ip_matches_prefix function when the target IP address is within a CIDR prefix
    """
    from AzureIdentifyNSGExposureRule import _ip_matches_prefix

    target_ip_obj = ipaddress.ip_address("10.0.0.5")
    result = _ip_matches_prefix(target_ip_obj, "10.0.0.0/24")
    assert result is True


def test_ip_matches_prefix_not_in_cidr():
    """
    Test _ip_matches_prefix function when the target IP address is not within a CIDR prefix
    """
    from AzureIdentifyNSGExposureRule import _ip_matches_prefix

    target_ip_obj = ipaddress.ip_address("10.0.0.5")
    result = _ip_matches_prefix(target_ip_obj, "10.0.1.0/24")
    assert result is False


def test_ip_matches_prefix_invalid_prefix():
    """
    Test _ip_matches_prefix function when the provided prefix string is invalid
    """
    from AzureIdentifyNSGExposureRule import _ip_matches_prefix

    target_ip_obj = ipaddress.ip_address("10.0.0.5")
    result = _ip_matches_prefix(target_ip_obj, "10.0.0.0/42")
    assert result is False


def test_get_nsg_rules_error(mocker):
    """Tests that get_nsg_rules raises DemistoException when the command returns an error.

    Given:
        - A mocked error response from azure-vn-security-rules-list
    When:
        - Calling get_nsg_rules
    Then:
        - A DemistoException is raised
    """
    from AzureIdentifyNSGExposureRule import get_nsg_rules

    error_result = [{"Type": 4, "Contents": "Some error occurred", "ContentsFormat": "text"}]
    mocker.patch.object(demisto, "executeCommand", return_value=error_result)

    with pytest.raises(DemistoException, match="Error retrieving security group details"):
        get_nsg_rules("fake-subscription-id", "fake-resource-group", "test-nsg", "")


def test_get_nsg_rules_no_inbound_rules(mocker):
    """Tests that get_nsg_rules raises DemistoException when no inbound rules are found.

    Given:
        - A valid response from azure-vn-security-rules-list with only outbound rules
    When:
        - Calling get_nsg_rules
    Then:
        - A DemistoException is raised indicating no inbound rules were found
    """
    from AzureIdentifyNSGExposureRule import get_nsg_rules

    outbound_only_result = [
        {
            "Type": 1,
            "Metadata": {"instance": "azure-instance"},
            "Contents": [
                {
                    "properties": {
                        "priority": 100,
                        "direction": "Outbound",
                        "protocol": "TCP",
                        "access": "Allow",
                        "destinationPortRange": "443",
                        "sourceAddressPrefix": "*",
                        "destinationAddressPrefix": "*",
                    },
                    "name": "AllowOutboundHTTPS",
                }
            ],
        }
    ]
    mocker.patch.object(demisto, "executeCommand", return_value=outbound_only_result)

    with pytest.raises(DemistoException, match="No inbound NSG rules found"):
        get_nsg_rules("fake-subscription-id", "fake-resource-group", "test-nsg", "")


def test_get_nsg_rules_empty_result(mocker):
    """Tests that get_nsg_rules raises DemistoException when the command returns an empty list.

    Given:
        - An empty list response from azure-vn-security-rules-list
    When:
        - Calling get_nsg_rules
    Then:
        - A DemistoException is raised indicating no results returned
    """
    from AzureIdentifyNSGExposureRule import get_nsg_rules

    mocker.patch.object(demisto, "executeCommand", return_value=[])

    with pytest.raises(DemistoException, match="No results returned"):
        get_nsg_rules("fake-subscription-id", "fake-resource-group", "test-nsg", "")