CreateSigmaRuleIndicator

python · Sigma

Details

IDCreateSigmaRuleIndicator
Languagepython
From Version6.10.0
Docker Imagedemisto/pysigma:1.0.0.10133006

README

Script Data


Name Description
Script Type python3

Inputs


Argument Name Description
sigma_rule_str A string containing the Sigma rule in its original format.
entry_id The entry ID of the file that contains the Sigma rule. The script will read the Sigma rule from this file if provided.

Outputs


There are no outputs for this script.

from unittest.mock import patch, MagicMock
import io
import zipfile

import CreateSigmaRuleIndicator
import demistomock as demisto  # noqa: F401
import pytest
from CommonServerPython import *  # noqa: F401
from CreateSigmaRuleIndicator import (
    create_indicator_relationships,
    get_mitre_technique_name,
    parse_and_create_indicator,
    parse_detection_field,
    parse_tags,
    create_relationship,
    main,
    tim_create_indicators,
    extract_rules_from_zip,
)
from sigma.rule import SigmaRule, SigmaRuleTag


def load_file(path: str) -> dict[str, Any]:
    with open(path) as f:
        return json.load(f)


def test_create_relationship():
    indicator = "Sigma Rule"
    entity_b = "Command and Scripting Interpreter"
    entity_b_type = "Attack Pattern"
    relation_type = "detects"
    result = EntityRelationship(
        entity_a="Sigma Rule",
        entity_a_type="Sigma Rule Indicator",
        name="detects",
        reverse_name="detected-by",
        entity_b="Command and Scripting Interpreter",
        entity_b_type="Attack Pattern",
    )
    assert create_relationship(indicator, entity_b, entity_b_type, relation_type).to_context() == result.to_context()


@pytest.mark.parametrize(
    "input, expected_result",
    [
        pytest.param(
            [SigmaRuleTag(namespace="attack", name="t1059", source=None)],
            (
                [{"value": "Command and Scripting Interpreter", "type": "Attack Pattern"}],
                ["T1059 - Command and Scripting Interpreter"],
                "CLEAR",
            ),
            id="Tag Creation - MITRE technique",
        ),
        pytest.param(
            [SigmaRuleTag(namespace="attack", name="resource-development"), SigmaRuleTag(namespace="tlp", name="RED")],
            ([], ["Resource Development"], "RED"),
            id="Tag Creation - MITRE tactic",
        ),
        pytest.param(
            [SigmaRuleTag(namespace="cve", name="2024-3400")],
            ([{"value": "CVE-2024-3400", "type": "CVE"}], ["CVE-2024-3400"], "CLEAR"),
            id="Tag Creation - CVEs",
        ),
    ],
)
@patch.object(CreateSigmaRuleIndicator, "get_mitre_technique_name")
def test_parse_tags(mock_get_mitre_technique_name, input, expected_result):
    mock_get_mitre_technique_name.return_value = "Command and Scripting Interpreter"
    assert parse_tags(input) == expected_result


@patch.object(CreateSigmaRuleIndicator, "execute_command")
def test_get_mitre_technique_name(mock_execute_command):
    mock_execute_command.return_value = True, {"value": "Command and Scripting Interpreter"}
    mitre_id = "T1059"
    indicator_type = "Attack Pattern"
    get_mitre_technique_name(mitre_id, indicator_type)
    mock_execute_command.assert_called_with(
        command="SearchIndicator", args={"query": f'type:"Attack Pattern" and {mitre_id}'}, fail_on_error=False
    )


@patch.object(CreateSigmaRuleIndicator, "create_relationship")
@patch.object(CreateSigmaRuleIndicator, "return_results")
def test_create_indicator_relationships(mock_return_results, mock_create_relationship):
    mock_create_relationship.return_value("relationship")
    indicator = "Sigma Rule Test"
    product = "Windows"
    relationships = [{"value": "Some technique", "type": "Attack Pattern"}, {"value": "CVE-2024-111", "type": "CVE"}]
    create_indicator_relationships(indicator, product, relationships)
    assert mock_create_relationship.call_count == 3


def test_parse_detection_field():
    with open("test_data/sigma_rule.yml") as f:
        sigma_rule = SigmaRule.from_yaml(f.read())

    result = [{"selection": "selection", "key": "displaymessage", "modifiers": "", "values": "(1)Max sign in attempts exceeded"}]
    assert parse_detection_field(sigma_rule) == result


def test_parse_and_create_indicator():
    with open("test_data/sigma_rule.yml") as f:
        rule = f.read()
    expected_indicator = load_file("test_data/expected_indicator.json")
    result = parse_and_create_indicator(SigmaRule.from_yaml(rule), raw_rule=rule)
    assert result["indicator"] == expected_indicator["indicator"]


@patch.object(demisto, "args")
@patch.object(CreateSigmaRuleIndicator, "return_results")
@patch.object(CreateSigmaRuleIndicator, "execute_command")
def test_main(mock_executeCommand, mock_return_results, mock_args):
    with open("test_data/sigma_rule.yml") as f:
        rule = f.read()

    mock_args.return_value = {"sigma_rule_str": rule, "entry_id": "", "create_indicators": "True"}
    main()
    mock_return_results.assert_called_once()
    args, kwargs = mock_return_results.call_args
    assert args[0].readable_output == "1 Sigma Rule(s) Created.\n1 Relationship(s) Created."


@patch("CreateSigmaRuleIndicator.execute_command")
@patch("CreateSigmaRuleIndicator.create_indicator_relationships")
@patch("CreateSigmaRuleIndicator.demisto.debug")
@patch("CreateSigmaRuleIndicator.time.time")
def test_tim_create_indicators(mock_time, mock_debug, mock_create_relationships, mock_execute_command):
    sample_indicators = [
        {
            "indicator": {
                "value": "Test Sigma Rule 1",
                "type": "Sigma Rule",
                "product": "windows",
            },
            "relationships": [
                {"value": "T1562 - Impair Defenses", "type": "Attack Pattern"},
                {"value": "CVE-2021-44228", "type": "CVE"},
            ],
        },
        {
            "indicator": {
                "value": "Test Sigma Rule 2",
                "type": "Sigma Rule",
                "product": "linux",
            },
            "relationships": [{"value": "S0601 - Cobalt Strike", "type": "Tool"}],
        },
    ]

    # Setup mocks
    mock_time.side_effect = [100, 105]  # Start and end times

    # Mock relationships that would be created
    mock_create_relationships.side_effect = [
        [
            EntityRelationship(
                entity_a="Test Sigma Rule 1",
                entity_a_type="Sigma Rule Indicator",
                name="related-to",
                entity_b="Windows",
                entity_b_type="Software",
            ),
            EntityRelationship(
                entity_a="Test Sigma Rule 1",
                entity_a_type="Sigma Rule Indicator",
                name="detects",
                entity_b="T1562 - Impair Defenses",
                entity_b_type="Attack Pattern",
            ),
            EntityRelationship(
                entity_a="Test Sigma Rule 1",
                entity_a_type="Sigma Rule Indicator",
                name="detects",
                entity_b="CVE-2021-44228",
                entity_b_type="CVE",
            ),
        ],
        [
            EntityRelationship(
                entity_a="Test Sigma Rule 2",
                entity_a_type="Sigma Rule Indicator",
                name="related-to",
                entity_b="Linux",
                entity_b_type="Software",
            ),
            EntityRelationship(
                entity_a="Test Sigma Rule 2",
                entity_a_type="Sigma Rule Indicator",
                name="detects",
                entity_b="S0601 - Cobalt Strike",
                entity_b_type="Tool",
            ),
        ],
    ]

    # Execute the function
    result = tim_create_indicators(sample_indicators)

    # Verify results
    assert mock_execute_command.call_count == 2
    mock_execute_command.assert_any_call("createNewIndicator", sample_indicators[0]["indicator"])
    mock_execute_command.assert_any_call("createNewIndicator", sample_indicators[1]["indicator"])

    assert mock_create_relationships.call_count == 2
    mock_create_relationships.assert_any_call("Test Sigma Rule 1", "windows", sample_indicators[0]["relationships"])
    mock_create_relationships.assert_any_call("Test Sigma Rule 2", "linux", sample_indicators[1]["relationships"])

    mock_debug.assert_called_once_with("2 indicators created. in 5 seconds")

    # Verify returned CommandResults
    assert isinstance(result, CommandResults)
    assert result.readable_output == "2 Sigma Rule(s) Created.\n5 Relationship(s) Created."
    assert len(result.relationships) == 5  # Total relationships from both indicators


def create_zip_mock(file_contents, file_names):
    """Helper to create a mock zipfile"""
    zip_buffer = io.BytesIO()
    with zipfile.ZipFile(zip_buffer, "w") as zip_file:
        for name, content in zip(file_names, file_contents):
            zip_file.writestr(name, content)
    return zip_buffer.getvalue()


@pytest.fixture
def valid_sigma_rule():
    return """
title: Test Rule
id: 12345678-1234-1234-1234-123456789012
status: test
description: Test rule for unit testing
author: Test Author
date: 2023/01/01
logsource:
    category: process_creation
    product: windows
detection:
    selection:
        CommandLine|contains: suspicious.exe
    condition: selection
level: high
tags:
    - attack.t1055
    - attack.s0001
    - cve.2023.1234
"""


@pytest.fixture
def invalid_sigma_rule():
    return """
title: Invalid Rule
status: test
# Missing required fields like id, detection, etc.
"""


@patch("zipfile.ZipFile")
def test_extract_rules_from_zip_successful(mock_zipfile, valid_sigma_rule):
    """Test extracting valid rules from a zip file"""

    # Setup mock zipfile with valid rules
    mock_zip_instance = MagicMock()
    mock_zipfile.return_value.__enter__.return_value = mock_zip_instance
    mock_zip_instance.namelist.return_value = ["rule1.yml", "rule2.yml", "__pycache__/ignored.yml", ".hidden.yml"]

    # Mock file content reading
    mock_file = MagicMock()
    mock_file.read.return_value = valid_sigma_rule.encode("utf-8")
    mock_zip_instance.open.return_value.__enter__.return_value = mock_file

    # Mock parse_and_create_indicator to return a predetermined value
    expected_indicator = {"indicator": {"value": "Test Rule"}, "relationships": []}
    with patch("CreateSigmaRuleIndicator.parse_and_create_indicator", return_value=expected_indicator):
        result = extract_rules_from_zip("test.zip")

    # Assertions
    assert len(result) == 2  # Should extract 2 valid files
    assert result[0] == expected_indicator
    assert result[1] == expected_indicator

    # Verify correct files were processed
    mock_zip_instance.open.assert_any_call("rule1.yml")
    mock_zip_instance.open.assert_any_call("rule2.yml")
    assert mock_zip_instance.open.call_count == 2


@patch("zipfile.ZipFile")
def test_extract_rules_from_zip_with_errors(mock_zipfile, valid_sigma_rule, invalid_sigma_rule, capfd):
    """Test handling errors during rule extraction"""

    # Setup mock zipfile with mixed valid and invalid rules
    mock_zip_instance = MagicMock()
    mock_zipfile.return_value.__enter__.return_value = mock_zip_instance
    mock_zip_instance.namelist.return_value = ["valid.yml", "invalid.yml"]

    # Setup mock file reading to return different content based on filename
    def mock_open_file(filename):
        from unittest.mock import MagicMock

        mock_file = MagicMock()
        if filename == "valid.yml":
            mock_file.read.return_value = valid_sigma_rule.encode("utf-8")
        else:
            mock_file.read.return_value = invalid_sigma_rule.encode("utf-8")
        return mock_file

    mock_zip_instance.open.side_effect = lambda filename: MagicMock(__enter__=lambda x: mock_open_file(filename))

    # Mock SigmaRule.from_yaml to raise an exception for the invalid rule
    original_from_yaml = SigmaRule.from_yaml

    def mock_from_yaml(yaml_str):
        if "Invalid Rule" in yaml_str:
            raise Exception("Invalid rule format")
        return original_from_yaml(yaml_str)

    # Mock parse_and_create_indicator for the valid rule
    expected_indicator = {"indicator": {"value": "Test Rule"}, "relationships": []}

    with (
        patch("sigma.rule.SigmaRule.from_yaml", side_effect=mock_from_yaml),
        patch("CreateSigmaRuleIndicator.parse_and_create_indicator", return_value=expected_indicator),
    ):
        result = extract_rules_from_zip("test.zip")

    # Assertions
    out, err = capfd.readouterr()
    assert len(result) == 1  # Only the valid rule should be processed
    assert result[0] == expected_indicator
    assert out == 'SGM: Error parsing Sigma rule from file "invalid.yml": Invalid rule format\n'


@patch("zipfile.ZipFile")
def test_extract_rules_from_zip_empty(mock_zipfile):
    """Test extracting rules from a zip file with no valid rules"""
    # Setup mock zipfile with no valid rules
    mock_zip_instance = MagicMock()
    mock_zipfile.return_value.__enter__.return_value = mock_zip_instance
    mock_zip_instance.namelist.return_value = ["__pycache__/ignored.yml", ".hidden.yml", "not_a_rule.txt"]

    result = extract_rules_from_zip("test.zip")

    # Assertions
    assert len(result) == 0  # No valid rules should be found
    assert not mock_zip_instance.open.called  # No files should be opened