From 10cc5dbb9939436f50a9b7483e047499c6eccfd2 Mon Sep 17 00:00:00 2001 From: Daniel Federschmidt Date: Tue, 6 Aug 2024 16:41:18 +0200 Subject: [PATCH 01/24] feat: first take at a release pipeline --- .github/tests/pyproject.toml | 42 +++ .github/tests/requirements-dev.txt | 141 +++++++ .github/tests/requirements.txt | 122 ++++++ .github/tests/robot/DynamicTestCases.py | 71 ++++ .github/tests/robot/PlaybookScanner.robot | 374 +++++++++++++++++++ .github/tests/robot/PlaybookScannerHelper.py | 30 ++ .github/workflows/main.yml | 34 ++ .gitignore | 1 + 8 files changed, 815 insertions(+) create mode 100644 .github/tests/pyproject.toml create mode 100644 .github/tests/requirements-dev.txt create mode 100644 .github/tests/requirements.txt create mode 100644 .github/tests/robot/DynamicTestCases.py create mode 100644 .github/tests/robot/PlaybookScanner.robot create mode 100644 .github/tests/robot/PlaybookScannerHelper.py create mode 100644 .github/workflows/main.yml diff --git a/.github/tests/pyproject.toml b/.github/tests/pyproject.toml new file mode 100644 index 0000000..85a331e --- /dev/null +++ b/.github/tests/pyproject.toml @@ -0,0 +1,42 @@ +[build-system] +requires = ["setuptools", "setuptools-scm"] +build-backend = "setuptools.build_meta" + +[project] +name = "sadlc-testing" +description = "Testing scripts for SOAR playbooks" +authors = [{ name = "Eric Li", email = "ericli@splunk.com" }] +requires-python = ">=3.10" +readme = "README.md" +dependencies = [ + "black", + "isort", + "robotframework", + "robotframework-tidy", + "paramiko", + "mistletoe", + "beautifulsoup4", + "lxml" +] + +dynamic = ["version", ] + +[project.urls] +repository = "https://cd.splunkdev.com/sgs-soar/sadlc-testing" + +[project.optional-dependencies] +dev = [ + "pip-tools" +] + +[tool.setuptools] +py-modules = [] + +[tool.black] +line-length = "120" +extend-exclude = "repos|(.*/)?test/data/|robot/data/" + +[tool.isort] +line_length = "120" +profile = "black" +extend_skip_glob = ["repos/*", "test/data/*", "*/test/data/*", "robot/data/*"] diff --git a/.github/tests/requirements-dev.txt b/.github/tests/requirements-dev.txt new file mode 100644 index 0000000..fb28496 --- /dev/null +++ b/.github/tests/requirements-dev.txt @@ -0,0 +1,141 @@ +# +# This file is autogenerated by pip-compile with Python 3.11 +# by the following command: +# +# pip-compile --extra=dev --no-emit-index-url --output-file=requirements-dev.txt +# +annotated-types==0.7.0 + # via pydantic +anyio==4.4.0 + # via httpx +bcrypt==4.2.0 + # via paramiko +beautifulsoup4==4.12.3 + # via sadlc-testing (pyproject.toml) +black==24.4.2 + # via + # sadlc-testing (pyproject.toml) + # splunk-soar-sdk +build==1.2.1 + # via pip-tools +certifi==2024.7.4 + # via + # httpcore + # httpx + # requests +cffi==1.16.0 + # via + # cryptography + # pynacl +charset-normalizer==3.3.2 + # via requests +click==8.1.7 + # via + # black + # pip-tools + # rich-click + # robotframework-tidy +colorama==0.4.6 + # via robotframework-tidy +cryptography==43.0.0 + # via paramiko +h11==0.14.0 + # via httpcore +httpcore==1.0.5 + # via httpx +httpx==0.27.0 + # via splunk-soar-sdk +hvac==2.3.0 + # via splunk-soar-sdk +idna==3.7 + # via + # anyio + # httpx + # requests +isort==5.13.2 + # via + # sadlc-testing (pyproject.toml) + # splunk-soar-sdk +jinja2==3.1.4 + # via robotframework-tidy +lxml==5.2.2 + # via sadlc-testing (pyproject.toml) +markdown-it-py==3.0.0 + # via rich +markupsafe==2.1.5 + # via jinja2 +mdurl==0.1.2 + # via markdown-it-py +mistletoe==1.4.0 + # via sadlc-testing (pyproject.toml) +mypy-extensions==1.0.0 + # via black +packaging==24.1 + # via + # black + # build +paramiko==3.4.0 + # via sadlc-testing (pyproject.toml) +pathspec==0.12.1 + # via + # black + # robotframework-tidy +pip-tools==7.4.1 + # via sadlc-testing (pyproject.toml) +platformdirs==4.2.2 + # via black +pycparser==2.22 + # via cffi +pydantic==2.8.2 + # via splunk-soar-sdk +pydantic-core==2.20.1 + # via pydantic +pygments==2.18.0 + # via + # rich + # splunk-soar-sdk +pynacl==1.5.0 + # via paramiko +pyproject-hooks==1.1.0 + # via + # build + # pip-tools +pytz==2024.1 + # via splunk-soar-sdk +requests==2.32.3 + # via hvac +rich==13.7.1 + # via rich-click +rich-click==1.7.3 + # via robotframework-tidy +robotframework==7.0.1 + # via + # robotframework-tidy + # sadlc-testing (pyproject.toml) +robotframework-tidy==4.13.0 + # via sadlc-testing (pyproject.toml) +sniffio==1.3.1 + # via + # anyio + # httpx +soupsieve==2.5 + # via beautifulsoup4 +splunk-soar-sdk @ git+ssh://git@cd.splunkdev.com/sgs-soar/splunk-soar-sdk.git@open-source + # via sadlc-testing (pyproject.toml) +toml==0.10.2 + # via splunk-soar-sdk +tomli==2.0.1 + # via robotframework-tidy +typing-extensions==4.12.2 + # via + # pydantic + # pydantic-core + # rich-click +urllib3==2.2.2 + # via requests +wheel==0.43.0 + # via pip-tools + +# The following packages are considered to be unsafe in a requirements file: +# pip +# setuptools diff --git a/.github/tests/requirements.txt b/.github/tests/requirements.txt new file mode 100644 index 0000000..6460e03 --- /dev/null +++ b/.github/tests/requirements.txt @@ -0,0 +1,122 @@ +# +# This file is autogenerated by pip-compile with Python 3.11 +# by the following command: +# +# pip-compile --no-emit-index-url +# +annotated-types==0.7.0 + # via pydantic +anyio==4.4.0 + # via httpx +bcrypt==4.2.0 + # via paramiko +beautifulsoup4==4.12.3 + # via sadlc-testing (pyproject.toml) +black==24.4.2 + # via + # sadlc-testing (pyproject.toml) + # splunk-soar-sdk +certifi==2024.7.4 + # via + # httpcore + # httpx + # requests +cffi==1.16.0 + # via + # cryptography + # pynacl +charset-normalizer==3.3.2 + # via requests +click==8.1.7 + # via + # black + # rich-click + # robotframework-tidy +colorama==0.4.6 + # via robotframework-tidy +cryptography==43.0.0 + # via paramiko +h11==0.14.0 + # via httpcore +httpcore==1.0.5 + # via httpx +httpx==0.27.0 + # via splunk-soar-sdk +hvac==2.3.0 + # via splunk-soar-sdk +idna==3.7 + # via + # anyio + # httpx + # requests +isort==5.13.2 + # via + # sadlc-testing (pyproject.toml) + # splunk-soar-sdk +jinja2==3.1.4 + # via robotframework-tidy +lxml==5.2.2 + # via sadlc-testing (pyproject.toml) +markdown-it-py==3.0.0 + # via rich +markupsafe==2.1.5 + # via jinja2 +mdurl==0.1.2 + # via markdown-it-py +mistletoe==1.4.0 + # via sadlc-testing (pyproject.toml) +mypy-extensions==1.0.0 + # via black +packaging==24.1 + # via black +paramiko==3.4.0 + # via sadlc-testing (pyproject.toml) +pathspec==0.12.1 + # via + # black + # robotframework-tidy +platformdirs==4.2.2 + # via black +pycparser==2.22 + # via cffi +pydantic==2.8.2 + # via splunk-soar-sdk +pydantic-core==2.20.1 + # via pydantic +pygments==2.18.0 + # via + # rich + # splunk-soar-sdk +pynacl==1.5.0 + # via paramiko +pytz==2024.1 + # via splunk-soar-sdk +requests==2.32.3 + # via hvac +rich==13.7.1 + # via rich-click +rich-click==1.7.3 + # via robotframework-tidy +robotframework==7.0.1 + # via + # robotframework-tidy + # sadlc-testing (pyproject.toml) +robotframework-tidy==4.13.0 + # via sadlc-testing (pyproject.toml) +sniffio==1.3.1 + # via + # anyio + # httpx +soupsieve==2.5 + # via beautifulsoup4 +toml==0.10.2 + # via splunk-soar-sdk +tomli==2.0.1 + # via robotframework-tidy +typing-extensions==4.12.2 + # via + # pydantic + # pydantic-core + # rich-click +urllib3==2.2.2 + # via requests diff --git a/.github/tests/robot/DynamicTestCases.py b/.github/tests/robot/DynamicTestCases.py new file mode 100644 index 0000000..57cb117 --- /dev/null +++ b/.github/tests/robot/DynamicTestCases.py @@ -0,0 +1,71 @@ +"""Supports dynamically adding a test case during robot framework execution. + +Adapted from https://stackoverflow.com/a/77484465 . +""" + +# While this import does not seem necessary, it was useful in the python console. +from robot.running.model import Keyword, TestCase, TestSuite + + +class DynamicTestCases(object): + ROBOT_LISTENER_API_VERSION = 3 + ROBOT_LIBRARY_SCOPE = "TEST SUITE" + + def __init__(self): + self.ROBOT_LIBRARY_LISTENER = self + self._current_suite = None + + def _start_suite(self, suite, result): + # Don't change the name of this method. + # save current suite so that we can modify it later + self._current_suite = suite + + def dynamic_test_cases_create(self, name, *tags): + """Adds a test case to the current suite. + + Args: + name: is the test case name + tags: is a list of tags to add to the test case + + Returns: The test case that was added + """ + test_case = self._current_suite.tests.create(name=name, tags=tags) + return test_case + + def dynamic_test_cases_set_body(self, test_case: TestCase, keyword_name: str, *args) -> Keyword: + """Sets the body keyword of the given test case. + + Args: + test_case: The test case to add the keyword to. + keyword_name: The name of the keyword to add. + args: The arguments to pass to the keyword. Currently only support + positional arguments. + """ + keyword = test_case.body.create_keyword(name=keyword_name, args=args) + return keyword + + def dynamic_test_cases_set_setup(self, test_case: TestCase, keyword_name: str, *args) -> Keyword: + """Sets the setup keyword of the given test case. + + Args: + test_case: The test case to add the keyword to. + keyword_name: The name of the keyword to add. + args: The arguments to pass to the keyword. Currently only support + positional arguments. + """ + keyword = test_case.body.create_keyword(name=keyword_name, args=args, type="setup") + test_case.setup = keyword + return keyword + + def dynamic_test_cases_set_teardown(self, test_case: TestCase, keyword_name: str, *args) -> Keyword: + """Sets the teardown keyword of the given test case. + + Args: + test_case: The test case to add the keyword to. + keyword_name: The name of the keyword to add. + args: The arguments to pass to the keyword. Currently only support + positional arguments. + """ + keyword = test_case.body.create_keyword(name=keyword_name, args=args, type="teardown") + test_case.teardown = keyword + return keyword diff --git a/.github/tests/robot/PlaybookScanner.robot b/.github/tests/robot/PlaybookScanner.robot new file mode 100644 index 0000000..a545756 --- /dev/null +++ b/.github/tests/robot/PlaybookScanner.robot @@ -0,0 +1,374 @@ +*** Settings *** +Documentation Scan playbook to make sure it follows MR review standard. +... +... https://docs.google.com/document/d/1cowcKOZxcc7U90eP5Zy1j5HtLhLQCg9_ALtSOTC_CYE/edit + +Library PlaybookScannerHelper.py +Library DynamicTestCases.py + + +*** Variables *** +${playbook} %{PLAYBOOK=} +${EXAMPLE_PLAYBOOK}= repos/community2/Jira_Related_Tickets_Search + +@{block_types}= Create List +... start +... end +... action +... playbook +... code +... utility +... filter +... decision +... format +... prompt + + +*** Test Cases *** +Test Playbook + # Can we get generate this list automatically? Asked on https://stackoverflow.com/q/78794730 + ${tests}= Create List + ... Scan Playbook Name + ... Scan Playbook Category + ... Scan Playbook Description + ... Scan Playbook Notes + ... Scan Playbook Block Count + ... Scan Playbook Custom List Ref + ... Scan Block Names + ... Scan Block Notes + ... Scan Custom Code + ... Scan Start End Block + ... Scan Decision Filter Block + ... Scan Action Block + ... Scan Utility Playbook Block + ... Scan Automation Playbook Label + ... Scan Automation Playbook Paths + ... Scan Input Playbook Start Block + ... Scan Input Playbook Tags + ... Scan Global Custom Code + ... Scan Unbounded Custom Code + ... Scan Code Formatting + + IF ${{not $playbook}} + ${playbook}= Set Variable ${EXAMPLE_PLAYBOOK} + END + + Log ${playbook} + ${pb}= Helper Parse Playbook ${playbook} + + FOR ${i} IN @{tests} + ${test_case}= Dynamic Test Cases Create Test ${i} + Dynamic Test Cases Set Body ${test_case} ${i} ${pb} + END + + +*** Keywords *** +Get Playbook Blocks By Type + [Documentation] Returns desired types of blocks in the playbook. + [Arguments] ${pb} @{types} + + # Input validation. + Should Be Equal ${{len($types)}} ${{len(set($types))}} "types" list contains duplicate values + FOR ${type} IN @{types} + Should Contain ${block_types} ${type} + END + + # Compute answer. + @{ans}= Create List + FOR ${block} IN @{{$pb.get_playbook_blocks()}} + IF ${{$block.block_type in $types}} + @{ans}= Create List @{ans} ${block} + END + END + RETURN ${ans} + +Scan Playbook Name + [Documentation] Playbook name is A-Z in Title case with underscores between words. (e.g. + ... MS_Graph_Search_and_Purge) + [Arguments] ${pb} + Log ${pb.name} + + ${regex_word}= Set Variable [0-9A-Z][0-9A-Za-z]* + ${regex_name}= Set Variable ^${regex_word}(?:_${regex_word})*$ + Should Match Regexp ${pb.name} ${regex_name} + +Scan Playbook Category + [Documentation] Category in Title case with spaces between words (e.g. Identifier Reputation Analysis) + [Arguments] ${pb} + Log ${pb.category} + + ${regex_word}= Set Variable [0-9A-Z][0-9A-Za-z]* + ${regex_name}= Set Variable ^${regex_word}(?: ${regex_word})*$ + Should Match Regexp ${pb.category} ${regex_name} + +Scan Playbook Description + [Documentation] Description is free of grammatical errors and describe what the playbook does. + [Arguments] ${pb} + Log ${pb.description} + + Should Not Be Empty ${pb.description} + Skip Please check playbook description manually. + +Scan Playbook Notes + [Documentation] Notes list any setup required on the third-party API as well as intended areas for customization. + [Arguments] ${pb} + Log ${pb.notes} + + Should Not Be Empty ${pb.notes} + Skip Please check playbook notes manually. + +Scan Playbook Block Count + [Documentation] Playbook block count not greater than 20 (not including Start and End blocks). + [Arguments] ${pb} + + ${block_count}= Set Variable ${{len($pb.get_playbook_blocks()) - 2}} + IF ${{$block_count > 20}} + Fail Block count ${block_count} is greater than 20 + END + +Scan Playbook Custom List Ref + [Documentation] If referencing a custom list, Notes document what the expected values are in that custom list. + [Arguments] ${pb} + + Skip Please check custom list references manually. + +Scan Block Names + [Documentation] All blocks have a custom name no more than 4 words, all lowercase, and separated by space (e.g. + ... close workbook task) + [Arguments] ${pb} + + ${fail_count}= Set Variable ${0} + + ${regex_word}= Set Variable [0-9a-z]+ + ${regex_name}= Set Variable ${regex_word}( ${regex_word}){0,3} + + FOR ${block} IN @{{$pb.get_playbook_blocks()}} + IF ${{$block.block_type in ("start", "end")}} + Log Start and end blocks do not need to be named. + ELSE IF ${{$block.block_custom_name is None}} + Log Block ${{repr($block.block_name)}} is not named ERROR + ${fail_count}= Set Variable ${{$fail_count + 1}} + ELSE + ${block_name}= Set Variable ${{$block.block_custom_name}} + ${passed}= Run Keyword And Return Status Should Match Regexp ${block_name} ^${regex_name}$ + Log ${block_name} + IF ${{not $passed}} + Log Block name not following standard: ${block_name} ERROR + ${fail_count}= Set Variable ${{$fail_count + 1}} + END + END + END + + IF ${{$fail_count > 0}} + Fail ${fail_count} errors found in block names. + END + +Scan Block Notes + [Documentation] All blocks that support a Notes Tooltip have it filled out. Must be grammatically correct and + ... describes the intended purpose of that block. + [Arguments] ${pb} + + @{notes_supported}= Create List + ... action + ... code + ... utility + ... filter + ... decision + ... format + ... prompt + @{notes_not_supported}= Create List + ... start + ... end + ... playbook + + ${fail_count}= Set Variable ${0} + + FOR ${block} IN @{{$pb.get_playbook_blocks()}} + IF ${{$block.block_type in $notes_supported}} + IF ${{not $block.notes}} + Log Block ${{repr($block.block_name)}} does not have notes ERROR + ${fail_count}= Set Variable ${{$fail_count + 1}} + END + ELSE IF ${{$block.block_type in $notes_not_supported}} + Log Notes not supported for block ${{repr($block.block_name)}} + ELSE + Fail Unknown block type: ${block.block_type} + END + END + + IF ${{$fail_count > 0}} + Fail ${fail_count} errors found in block notes. + END + + Skip Please check notes content manually. + +Scan Custom Code + [Documentation] Where custom code is used, block notes indicate presence of custom code (e.g. "This block uses + ... custom code") + ... + ... No block is disabled by custom code + ... + ... Custom code is documented with notes + ... + ... Debug statements are removed or commented out + [Arguments] ${pb} + + @{custom_code_blocks}= Create List + + FOR ${block} IN @{{$pb.get_playbook_blocks()}} + IF ${block.user_code_exists} + @{custom_code_blocks}= Create List @{custom_code_blocks} ${block.block_name} + END + END + + Skip If ${{len($custom_code_blocks) > 0}} Please check custom code manually: ${custom_code_blocks}. + +Scan Start End Block + [Documentation] No custom code of any kind in Start and End blocks + [Arguments] ${pb} + + ${blocks}= Get Playbook Blocks By Type ${pb} start end + @{failed_blocks}= Create List + + FOR ${block} IN @{blocks} + IF ${block.user_code_exists} + Log Block ${{repr($block.block_name)}} contains custom code ERROR + @{failed_blocks}= Create List @{failed_blocks} ${block.block_name} + END + END + + Should Be Empty ${failed_blocks} + +Scan Decision Filter Block + [Documentation] All condition paths have a custom label + [Arguments] ${pb} + + ${blocks}= Get Playbook Blocks By Type ${pb} decision filter + @{failed_blocks}= Create List + + FOR ${block} IN @{blocks} + FOR ${condition} IN @{block.info["data"]["conditions"]} + IF ${{$condition.get("customName") is None}} + Log Not all conditions in block ${{repr($block.block_name)}} are labeled ERROR + @{failed_blocks}= Create List @{failed_blocks} ${block.block_name} + BREAK + END + END + END + + Should Be Empty ${failed_blocks} + +Scan Action Block + [Documentation] Use apps available on Splunkbase + ... + ... Use asset names that are the app name, all lowercase separated by underscores (e.g. Azure AD Graph becomes + ... azure_ad_graph) + [Arguments] ${pb} + + ${blocks}= Get Playbook Blocks By Type ${pb} action + @{failed_blocks}= Create List + + FOR ${block} IN @{blocks} + ${app_name}= Set Variable ${block.app_info["app_name"]} + ${asset_name}= Set Variable ${block.app_info["asset_name"]} + ${expected_asset_name}= Set Variable ${app_name.lower().replace(" ", "_")} + IF ${{$expected_asset_name != $asset_name}} + Log Incorrect asset name in block ${{repr($block.block_name)}} ERROR + @{failed_blocks}= Create List @{failed_blocks} ${block.block_name} + END + END + + Should Be Empty ${failed_blocks} + Skip If ${{len($blocks) > 0}} Please check whether apps are from Splunkbase manually + +Scan Utility Playbook Block + [Documentation] Block is using local version + [Arguments] ${pb} + + ${blocks}= Get Playbook Blocks By Type ${pb} playbook + @{failed_blocks}= Create List + + FOR ${block} IN @{blocks} + ${playbook_name}= Set Variable ${block.playbook_info["playbook_name"]} + ${repo_name}= Set Variable ${block.playbook_info["playbook_repo_name"]} + IF ${{$repo_name != "community"}} + Log Subplaybook is not using community version in block ${{repr($block.block_name)}} ERROR + @{failed_blocks}= Create List @{failed_blocks} ${block.block_name} + END + END + + Should Be Empty ${failed_blocks} + +Scan Automation Playbook Label + [Documentation] Automation Playbooks: Label is set to '\*' + [Arguments] ${pb} + + IF ${{$pb.coa["playbook_type"] == "automation"}} + Log ${pb.labels} + Should Be Equal ${pb.labels} ${{["*"]}} + END + +Scan Automation Playbook Paths + [Documentation] Automation Playbooks: No more than 3 concurrent branching paths. + [Arguments] ${pb} + + IF ${{$pb.coa["playbook_type"] == "automation"}} + Skip Please check number of branching paths manually. + END + +Scan Input Playbook Start Block + [Documentation] Input Playbooks: Start blocks use ocsf variable names and a minimum of one data type per variable + ... name (e.g. device (type: host name)) + ... + ... Start blocks use a specific data type if playbooks is expecting it (e.g. user (type: user name, aws iam user + ... name)) + [Arguments] ${pb} + + IF ${{$pb.coa["playbook_type"] == "data"}} + Skip Please check start block manually. + END + +Scan Input Playbook Tags + [Documentation] Input Playbooks: Has at least one category tag (e.g. reputation) + ... + ... Playbook has a tag for each vendor app used (e.g. crowdstrike, virustotal, etc.) + ... + ... Playbook has a tag for each input type (e.g. host name, user) + ... + ... If applicable, Playbook has a tag for each D3FEND technique (e.g. D3-DA) + [Arguments] ${pb} + + IF ${{$pb.coa["playbook_type"] == "data"}} + Log ${pb.tags} + Should Not Be Empty ${pb.tags} According to the requirement there should be at least 1 tag. + Skip Please check playbook tags manually. + END + +Scan Global Custom Code + [Documentation] Make sure there is no global custom code. + [Arguments] ${pb} + + Should Be Equal ${{$pb.get_global_code()}} ${None} + +Scan Unbounded Custom Code + [Documentation] Make sure custom code are inside custom code region. + [Arguments] ${pb} + + Should Be Equal ${{$pb.coa["data"].get("customCode")}} ${None} + FOR ${block} IN @{{$pb.get_playbook_blocks()}} + Should Be Equal ${{$block.info.get("customCode")}} ${None} + END + +Scan Code Formatting + [Documentation] Make sure custom code are formatted. + [Arguments] ${pb} + + ${pb_copy}= Copy Playbook ${pb} + Log ${pb_copy.format_python_code()} + + ${old_code}= Set Variable ${pb.get_python_code()} + ${new_code}= Set Variable ${pb_copy.get_python_code()} + + # Currently do not enforce code formatting. + ${passed}= Run Keyword And Return Status Should Be Equal ${old_code} ${new_code} + Skip If ${{not $passed}} Custom code is not formatted diff --git a/.github/tests/robot/PlaybookScannerHelper.py b/.github/tests/robot/PlaybookScannerHelper.py new file mode 100644 index 0000000..cc57aee --- /dev/null +++ b/.github/tests/robot/PlaybookScannerHelper.py @@ -0,0 +1,30 @@ +from splunk_soar_sdk import ParsedPlaybook + +from robot.libraries.BuiltIn import BuiltIn + + +class PlaybookScannerHelper: + ROBOT_LIBRARY_SCOPE = "GLOBAL" + + def helper_parse_playbook(self, name_prefix: str) -> ParsedPlaybook: + """Prepares a playbook for testing. + + Args: + name_prefix: Path to playbook files, without extension. + + Returns: + Parsed Playbook object. + """ + pb = ParsedPlaybook.from_text(name_prefix) + return pb + + def copy_playbook(self, pb: ParsedPlaybook) -> ParsedPlaybook: + """Copies a playbook. + + Args: + pb: Parsed playbook object to be copied. + + Returns: + Copied parsed Playbook object. + """ + return ParsedPlaybook.from_b64(pb.to_b64()) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..7ac633e --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,34 @@ +name: Playbook Validation + +on: [push, pull_request] + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.x' # Specify the Python version you need + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install robotframework + + - name: Run Robot Framework tests + env: + PLAYBOOK: AD_LDAP_Account_Locking.json + run: | + mkdir -p results + robot -d results -L DEBUG:INFO .github/tests/robot/PlaybookScanner.robot + + - name: Archive test results + uses: actions/upload-artifact@v3 + with: + name: robot-results + path: results \ No newline at end of file diff --git a/.gitignore b/.gitignore index e43b0f9..b2685d6 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ .DS_Store +.github/tests/venv \ No newline at end of file From 58aa99ec20a09f4d4dce122b01d5d25792998b54 Mon Sep 17 00:00:00 2001 From: Daniel Federschmidt Date: Tue, 1 Oct 2024 16:31:16 +0200 Subject: [PATCH 02/24] fix: add dependency --- .github/tests/robot/PlaybookScannerHelper.py | 2 +- .../tests/robot/soar_robot_utils/__init__.py | 5 + .../robot/soar_robot_utils/playbook_parser.py | 342 ++++++++++++++++++ .github/tests/robot/soar_robot_utils/utils.py | 203 +++++++++++ 4 files changed, 551 insertions(+), 1 deletion(-) create mode 100644 .github/tests/robot/soar_robot_utils/__init__.py create mode 100644 .github/tests/robot/soar_robot_utils/playbook_parser.py create mode 100644 .github/tests/robot/soar_robot_utils/utils.py diff --git a/.github/tests/robot/PlaybookScannerHelper.py b/.github/tests/robot/PlaybookScannerHelper.py index cc57aee..f5c2247 100644 --- a/.github/tests/robot/PlaybookScannerHelper.py +++ b/.github/tests/robot/PlaybookScannerHelper.py @@ -1,4 +1,4 @@ -from splunk_soar_sdk import ParsedPlaybook +from soar_robot_utils import ParsedPlaybook from robot.libraries.BuiltIn import BuiltIn diff --git a/.github/tests/robot/soar_robot_utils/__init__.py b/.github/tests/robot/soar_robot_utils/__init__.py new file mode 100644 index 0000000..7b7adb5 --- /dev/null +++ b/.github/tests/robot/soar_robot_utils/__init__.py @@ -0,0 +1,5 @@ +from .playbook_parser import ParsedPlaybook + +__all__ = [ + "ParsedPlaybook", +] \ No newline at end of file diff --git a/.github/tests/robot/soar_robot_utils/playbook_parser.py b/.github/tests/robot/soar_robot_utils/playbook_parser.py new file mode 100644 index 0000000..417d5ec --- /dev/null +++ b/.github/tests/robot/soar_robot_utils/playbook_parser.py @@ -0,0 +1,342 @@ +import json +import logging +import re +from dataclasses import dataclass +from typing import Optional, Union + +from .utils import ParsedPlaybookOrCustomFunction, black_isort_format_str + +logger = logging.getLogger(__name__) + +# String that denotes start of a new playbook function. +# Old playbooks may not have "@phantom.playbook_block()" decorator, such as "tvm-main/[TVM] SLA Notification.py". +_FUNC_HEADER_REGEX = r"(?:\n@phantom\.playbook_block\(\))?\ndef " + +# Playbook comments that denote start and end of global / local custom code. +_GLOBAL_SEP = "#" * 80 +_GLOBAL_START = f"\n{_GLOBAL_SEP}\n## Global Custom Code Start\n{_GLOBAL_SEP}\n" +_GLOBAL_END = f"{_GLOBAL_SEP}\n## Global Custom Code End\n{_GLOBAL_SEP}\n" +_LOCAL_SEP = " " + "#" * 80 +_LOCAL_START = f"\n{_LOCAL_SEP}\n ## Custom Code Start\n{_LOCAL_SEP}\n" +_LOCAL_END = f"{_LOCAL_SEP}\n ## Custom Code End\n{_LOCAL_SEP}\n" + + +@dataclass +class _GlobalCode: + """Parsed global code at start of playbook.""" + + # String before custom code. + pre_code: str + # Custom code content, None for no custom code. + custom_code: Optional[str] + # String after custom code. Empty string if no custom code. + post_code: str + + def __str__(self): + ans = self.pre_code + if self.custom_code is not None: + ans += _GLOBAL_START + self.custom_code + _GLOBAL_END + ans += self.post_code + return ans + + @classmethod + def parse(cls, code: str): + if _GLOBAL_START in code: + regexp = f"(?s)(?P
.*){re.escape(_GLOBAL_START)}(?P.*){re.escape(_GLOBAL_END)}(?P.*)"
+            matched = re.fullmatch(regexp, code)
+            if not matched:
+                raise ValueError("Unable to parse global code", code)
+            return cls(matched.group("pre"), matched.group("custom"), matched.group("post"))
+        else:
+            return cls(code, None, "")
+
+
+@dataclass
+class _FunctionCode:
+    """Parsed code for a playbook function."""
+
+    # Function header (should match _FUNC_HEADER_REGEX).
+    header: str
+    # Function name.
+    name: str
+    # String after function name but before custom code.
+    pre_code: str
+    # Custom code content, None for no custom code.
+    custom_code: Optional[str]
+    # String after custom code. Empty string if no custom code.
+    post_code: str
+
+    def __str__(self):
+        ans = self.header + self.name + self.pre_code
+        if self.custom_code is not None:
+            ans += _LOCAL_START + self.custom_code + _LOCAL_END
+        ans += self.post_code
+        return ans
+
+    @classmethod
+    def parse(cls, code: str):
+        regexp = f"(?s)(?P
{_FUNC_HEADER_REGEX})(?P\w+)(?P.+)" + matched = re.fullmatch(regexp, code) + if not matched: + raise ValueError("Unable to parse function code (header)", code) + header = matched.group("header") + name = matched.group("name") + rest = matched.group("rest") + if _LOCAL_START in rest: + regexp = f"(?s)(?P
.*){re.escape(_LOCAL_START)}(?P.*){re.escape(_LOCAL_END)}(?P.*)"
+            matched = re.fullmatch(regexp, rest)
+            if not matched:
+                raise ValueError("Unable to parse function code (custom code)", code)
+            return cls(header, name, matched.group("pre"), matched.group("custom"), matched.group("post"))
+        else:
+            return cls(header, name, rest, None, "")
+
+
+def _add_newline(s: str):
+    """Adds newline to string s if it does not already end with a newline.
+
+    This is the behavior of SOAR when dealing with global custom code.
+    """
+
+    if s.endswith("\n"):
+        return s
+    else:
+        return s + "\n"
+
+
+def _json_to_python_function_name(function_name: str):
+    """Converts JSON data.functionName to Python function name."""
+
+    return re.sub(r"\W", "_", function_name.lower())
+
+
+@dataclass
+class PlaybookBlock:
+    block_id: int
+    block_name: str
+    block_type: str
+    action_type: Optional[str]
+    block_custom_name: Optional[str]
+    notes: Optional[str]
+    description: Optional[str]
+    notes: Optional[str]
+    app_info: Optional[dict]
+    playbook_info: Optional[dict]
+    user_code_exists: bool
+    custom_function: Optional[dict]
+    info: dict
+
+
+class ParsedPlaybook(ParsedPlaybookOrCustomFunction):
+    """Module for parsing SOAR Playbooks.
+    Args:
+        soar_json_code: String of the playbook JSON code
+        soar_python_code: string of the python code
+    Methods:
+        get_playbook_blocks: Returns a list of dictionaries of all the blocks in the playbook
+        get_global_code: returns any custom Global Code in the python playbook
+        set_global_code: sets both the Json and Python Global Code Sections
+        insert_global_code: sets the json and python playbook custom code section
+        get_python_code: output python code
+        get_json_code: output json code
+    """
+
+    def __init__(self, soar_name: str, soar_json_code: str, soar_python_code: str):
+        assert type(soar_json_code) == str
+        assert type(soar_python_code) == str
+        self._check_JSON(soar_json_code)
+        super().__init__(soar_name, soar_json_code, soar_python_code)
+        self.coa = self.soar_json_code["coa"]
+        self.soar_json_code["coa"] = None
+        # self.soar_python_code is text (set by super.__init__()).
+        # self._parsed_python_code is a list of _GlobalCode and _FunctionCode that parses out each function.
+        # Exactly one of soar_python_code and _parsed_python_code is None.
+        self._parsed_python_code = None
+
+    @property
+    def python_version(self):
+        return self.coa["python_version"]
+
+    @property
+    def description(self):
+        return self.coa["data"]["description"]
+
+    @property
+    def notes(self):
+        return self.coa["data"]["notes"]
+
+    @property
+    def category(self):
+        return self.soar_json_code["category"]
+
+    @property
+    def tags(self):
+        return self.soar_json_code["tags"]
+
+    @property
+    def labels(self):
+        return self.soar_json_code["labels"]
+
+    @property
+    def playbook_type(self):
+        return self.coa["playbook_type"]
+
+    @property
+    def playbook_version(self):
+        return self.coa["version"]
+
+    @property
+    def schema(self):
+        return self.coa["schema"]
+
+    def _check_JSON(self, soar_json_code: str) -> None:
+        # Check for Valid JSON and raise error if it isnt
+        try:
+            json.loads(soar_json_code)
+        except ValueError as error:
+            raise Exception("Playbook Provided is not a valid SOAR plabook")
+
+        # Check for Valid Playbook Json
+        # essentally we are looking for a specific Key/Value Pair
+        # if they dont exist its not a valid SOAR playbook and raise an exception
+        try:
+            json.loads(soar_json_code)["coa"]["data"]["nodes"]
+        except KeyError:
+            raise Exception("Playbook Provided is not a valid SOAR plabook")
+
+    def get_playbook_blocks(self) -> list:
+        """Returns a list of dictionaries of all the blocks in the playbook
+        Args:
+            None
+
+        Returns:
+            Returns a list of dictionaries of all the blocks in the playbook
+        """
+        playbook_blocks_output = []
+        for block_ID, block_info in self.coa["data"]["nodes"].items():
+            # Determine if this is an app block
+            if "connector" in block_info["data"] and "connectorConfigs" in block_info["data"]:
+                app_name = block_info["data"]["connector"]
+                try:
+                    connector_config = block_info["data"]["connectorConfigs"][0]
+                except IndexError:
+                    connector_config = None
+                app_info = {"app_name": app_name, "asset_name": connector_config}
+            else:
+                app_info = None
+
+            # Determine if this is a playbook block
+            if "playbookName" in block_info["data"] and "playbookRepoName" in block_info["data"]:
+                playbook_name = block_info["data"]["playbookName"]
+                playbook_repo_name = block_info["data"]["playbookRepoName"]
+                playbook_info = {"playbook_name": playbook_name, "playbook_repo_name": playbook_repo_name}
+            else:
+                playbook_info = None
+
+            # Determine if there is Custom Code
+            if "userCode" in block_info:
+                user_code = True
+            else:
+                user_code = False
+
+            block_list_item = PlaybookBlock(
+                block_id=int(block_ID),
+                block_name=block_info["data"]["functionName"],
+                block_type=block_info["type"],
+                action_type=block_info["data"].get("actionType", None),
+                block_custom_name=block_info["data"].get("advanced", {}).get("customName", None),
+                notes=block_info["data"].get("advanced", {}).get("note", None),
+                description=block_info["data"].get("advanced", {}).get("description", None),
+                app_info=app_info,
+                playbook_info=playbook_info,
+                user_code_exists=user_code,
+                custom_function=block_info["data"].get("customFunction", None),
+                info=block_info,
+            )
+
+            playbook_blocks_output.append(block_list_item)
+
+        return playbook_blocks_output
+
+    def get_global_code(self) -> str:
+        """This returns any custom Global Code in the python playbook"""
+        return self.coa["data"].get("globalCustomCode")
+
+    def get_json_code(self) -> str:
+        output_code = self.soar_json_code.copy()
+        output_code["coa"] = self.coa
+        return json.dumps(output_code, indent=4)
+
+    def get_python_code(self) -> str:
+        if self.soar_python_code is not None:
+            assert self._parsed_python_code is None
+        else:
+            assert self._parsed_python_code is not None
+            self.soar_python_code = "".join(map(str, self._parsed_python_code))
+            self._parsed_python_code = None
+        return self.soar_python_code
+
+    def get_parsed_python_code(self) -> list[Union[_FunctionCode, _GlobalCode]]:
+        """Parses Python code as necessary and returns the parsed result."""
+
+        if self._parsed_python_code is not None:
+            assert self.soar_python_code is None
+        else:
+            assert self.soar_python_code is not None
+            # Global custom code may define functions, so we look for end of
+            # global code comment and only split the code after it.
+            splitted = self.soar_python_code.split(_GLOBAL_END, 1)
+            assert len(splitted) in range(1, 3), "Global code end comment appears more than once"
+            # Split the functions using _FUNC_HEADER_REGEX.
+            fragments = re.split(f"(?<=\n)(?={_FUNC_HEADER_REGEX})", splitted[-1])
+            self._parsed_python_code = [
+                _GlobalCode.parse(_GLOBAL_END.join([*splitted[:-1], fragments[0]])),
+                *map(_FunctionCode.parse, fragments[1:]),
+            ]
+            self.soar_python_code = None
+        return self._parsed_python_code
+
+    def format_python_code(self) -> None:
+        """Formats Python custon code in a playbook using isort and black."""
+
+        if self.coa["data"].get("customCode") is not None:
+            raise ValueError("Full playbook is editted.")
+
+        # Dictionary from function name to note dict object.
+        fname2node = {}
+        for i in self.coa["data"]["nodes"].values():
+            func_name = i["data"]["functionName"]
+            assert _json_to_python_function_name(func_name) not in fname2node
+            fname2node[_json_to_python_function_name(func_name)] = i
+
+        for function_code in self.get_parsed_python_code():
+            if function_code.custom_code is None:
+                continue
+            if type(function_code) == _FunctionCode:
+                prefix = "if True:\n    print(1)\n"
+                suffix = "    print(2)\n"
+            else:
+                prefix = "print(1)\n"
+                suffix = "print(2)\n"
+            old_code = function_code.custom_code
+            new_code = black_isort_format_str(old_code, prefix=prefix, suffix=suffix)
+            if old_code == new_code:
+                continue
+
+            # Update Python code.
+            function_code.custom_code = new_code
+
+            # Update JSON code.
+            if type(function_code) == _GlobalCode:
+                assert self.coa["data"].get("globalCustomCode") is not None
+                assert _add_newline(self.coa["data"]["globalCustomCode"]) == old_code
+                self.coa["data"]["globalCustomCode"] = new_code
+            else:
+                assert type(function_code) == _FunctionCode
+                json_node = fname2node[function_code.name]
+                if json_node.get("customCode") is not None:
+                    block_name = json_node["data"].get("advanced", {}).get("customName", None)
+                    raise ValueError(f"Non-custom code of the function is editted for block {repr(block_name)}.")
+                assert json_node.get("userCode") is not None
+                assert json_node["userCode"] == old_code
+                json_node["userCode"] = new_code
diff --git a/.github/tests/robot/soar_robot_utils/utils.py b/.github/tests/robot/soar_robot_utils/utils.py
new file mode 100644
index 0000000..4b0be37
--- /dev/null
+++ b/.github/tests/robot/soar_robot_utils/utils.py
@@ -0,0 +1,203 @@
+"""Defines utilities for multiple purposes
+
+Like tgz files and formatting code.
+
+"""
+
+import base64
+import json
+import os
+import tarfile
+from io import BytesIO
+from tempfile import NamedTemporaryFile
+
+import black
+import isort
+
+_BLACK_MODE = black.Mode(line_length=120)
+_ISORT_CONFIG = isort.Config(line_length=120, profile="black")
+
+
+class ParsedPlaybookOrCustomFunction:
+    """Common parent class for ParsedPlaybook and ParsedCustomFunction.
+
+    Attributes:
+        soar_name: Name of PB or CF.
+        soar_json_code: JSON object.
+        soar_python_code: Python code.
+    """
+
+    def __init__(self, soar_name: str, soar_json_code: str, soar_python_code: str):
+        self.soar_name = soar_name
+        self.soar_json_code = json.loads(soar_json_code)
+        self.soar_python_code = soar_python_code
+
+    @property
+    def name(self) -> str:
+        """Returns playbook / CF name."""
+        return self.soar_name
+
+    @name.setter
+    def name(self, value: str):
+        """Modifies playbook / CF name."""
+        self.soar_name = value
+
+    def get_json_code(self) -> str:
+        """Returns JSON code of the custom function."""
+        return json.dumps(self.soar_json_code, indent=4)
+
+    def get_python_code(self) -> str:
+        """Returns Python code of the custom function."""
+        return self.soar_python_code
+
+    @classmethod
+    def from_text(cls, name_prefix: str):
+        """Initializes from decompressed .py and .json files.
+
+        The playbook / CF name is inferred from name_prefix.
+
+        Args:
+            name_prefix: Source file path name without extension.
+        """
+
+        name = os.path.basename(name_prefix)
+        json_name = name_prefix + ".json"
+        py_name = name_prefix + ".py"
+        with open(json_name) as fj, open(py_name) as fp:
+            return cls(name, fj.read(), fp.read())
+
+    def to_text(self, name_prefix: str) -> None:
+        """Saves playbook / CF to decompressed .py and .json files.
+
+        Args:
+            name_prefix: Source file path name without extension.
+        """
+
+        json_name = name_prefix + ".json"
+        py_name = name_prefix + ".py"
+        with open(json_name, "w") as fj, open(py_name, "w") as fp:
+            fj.write(self.get_json_code())
+            fp.write(self.get_python_code())
+
+    @classmethod
+    def from_tgz(cls, tgz_name: str):
+        """Initializes from a compressed tgz file.
+
+        The playbook / CF name is inferred from the content of tgz file.
+
+        Args:
+            tgz_name: Compressed file name (must end with ".tgz").
+        """
+
+        with tarfile.open(tgz_name, "r:gz") as tar:
+            py_names = list(filter(lambda x: os.path.splitext(x)[1] == ".py", tar.getnames()))
+            if len(py_names) != 1:
+                raise ValueError("Playbook tgz file contains more than 1 .py files.")
+            py_name = py_names[0]
+            name_prefix = os.path.splitext(py_name)[0]
+            json_name = name_prefix + ".json"
+            if os.path.sep in name_prefix:
+                raise ValueError("tgz file contains directory structure.")
+            with tar.extractfile(json_name) as fj, tar.extractfile(py_name) as fp:
+                return cls(name_prefix, fj.read().decode(), fp.read().decode())
+
+    def to_tgz(self, tgz_name: str) -> None:
+        """Saves playbook / CF to a compressed tgz file.
+
+        Args:
+            tgz_name: Compressed file name (must end with ".tgz").
+        """
+
+        with tarfile.open(tgz_name, "w:gz") as tar:
+            json_name = self.name + ".json"
+            py_name = self.name + ".py"
+            for name, content in [(json_name, self.get_json_code()), (py_name, self.get_python_code())]:
+                # https://stackoverflow.com/a/740839
+                info = tarfile.TarInfo(name=name)
+                f = BytesIO()
+                info.size = f.write(content.encode())
+                f.seek(0)
+                tar.addfile(info, f)
+
+    @classmethod
+    def from_b64(cls, data: str):
+        """Initializes from base64 encoding of compressed tgz file.
+
+        The playbook / CF name is inferred from the content of tgz file.
+
+        Args:
+            data: Encoded file content.
+        """
+
+        with NamedTemporaryFile(suffix=".tgz") as tmpfile:
+            with open(tmpfile.name, "wb") as f:
+                f.write(base64.b64decode(data.encode()))
+            return cls.from_tgz(tmpfile.name)
+
+    def to_b64(self) -> str:
+        """Saves playbook / CF to a compressed tgz file and encode using base64.
+
+        Returns:
+            Encoded file content.
+        """
+
+        with NamedTemporaryFile(suffix=".tgz") as tmpfile:
+            self.to_tgz(tmpfile.name)
+            with open(tmpfile.name, "rb") as f:
+                return base64.b64encode(f.read()).decode()
+
+
+def black_format_str(code: str, prefix: str, suffix: str) -> str:
+    """Formats a code snippet using Black.
+
+    Args:
+        code: Code snippet string to be formatted.
+        prefix: Placeholder code to temporarily add before code.
+        suffix: Placeholder code to temporarily add after code.
+
+    Returns:
+        Formatted code snippet string.
+    """
+    # Empty global code block is "\n\n\n", but black will reduce it to "\n\n".
+    # We hardcode a rule to reduce "\n{3,}" to "\n\n\n".
+    if set(code) == {"\n"} and len(code) >= 3:
+        return "\n\n\n"
+
+    # Add new line if necessary to avoid invalid syntax after adding suffix.
+    if not code.endswith("\n"):
+        code += "\n"
+
+    formatted = black.format_str(prefix + code + suffix, mode=_BLACK_MODE)
+
+    assert formatted.startswith(prefix)
+    assert formatted.endswith(suffix)
+    return formatted[len(prefix) : len(formatted) - len(suffix)]
+
+
+def isort_format_str(code: str, prefix: str, suffix: str) -> str:
+    """Formats a code snippet using isort.
+
+    Args:
+        code: Code snippet string to be formatted.
+        prefix: Placeholder code to temporarily add before code.
+        suffix: Placeholder code to temporarily add after code.
+
+    Returns:
+        Formatted code snippet string.
+    """
+    # Add new line if necessary to avoid invalid syntax after adding suffix.
+    if not code.endswith("\n"):
+        suffix = "\n" + suffix
+
+    formatted = isort.api.sort_code_string(prefix + code + suffix, config=_ISORT_CONFIG)
+
+    assert formatted.startswith(prefix)
+    assert formatted.endswith(suffix)
+    return formatted[len(prefix) : len(formatted) - len(suffix)]
+
+
+def black_isort_format_str(code: str, prefix: str, suffix: str) -> str:
+    """Calls black_format_str and isort_format_str."""
+
+    tmp = black_format_str(code, prefix, suffix)
+    return isort_format_str(tmp, prefix, suffix)

From 58b80d3793da31e4bea9e9bdfc1b3165bf349868 Mon Sep 17 00:00:00 2001
From: Daniel Federschmidt 
Date: Tue, 1 Oct 2024 16:33:41 +0200
Subject: [PATCH 03/24] fix: actually install deps

---
 .github/tests/requirements.txt | 16 ++++++++--------
 .github/workflows/main.yml     |  1 +
 2 files changed, 9 insertions(+), 8 deletions(-)

diff --git a/.github/tests/requirements.txt b/.github/tests/requirements.txt
index 6460e03..f36a6e3 100644
--- a/.github/tests/requirements.txt
+++ b/.github/tests/requirements.txt
@@ -15,7 +15,7 @@ beautifulsoup4==4.12.3
 black==24.4.2
     # via
     #   sadlc-testing (pyproject.toml)
-    #   splunk-soar-sdk
+    #   
 certifi==2024.7.4
     # via
     #   httpcore
@@ -41,9 +41,9 @@ h11==0.14.0
 httpcore==1.0.5
     # via httpx
 httpx==0.27.0
-    # via splunk-soar-sdk
+    # via 
 hvac==2.3.0
-    # via splunk-soar-sdk
+    # via 
 idna==3.7
     # via
     #   anyio
@@ -52,7 +52,7 @@ idna==3.7
 isort==5.13.2
     # via
     #   sadlc-testing (pyproject.toml)
-    #   splunk-soar-sdk
+    #   
 jinja2==3.1.4
     # via robotframework-tidy
 lxml==5.2.2
@@ -80,17 +80,17 @@ platformdirs==4.2.2
 pycparser==2.22
     # via cffi
 pydantic==2.8.2
-    # via splunk-soar-sdk
+    # via 
 pydantic-core==2.20.1
     # via pydantic
 pygments==2.18.0
     # via
     #   rich
-    #   splunk-soar-sdk
+    #   
 pynacl==1.5.0
     # via paramiko
 pytz==2024.1
-    # via splunk-soar-sdk
+    # via 
 requests==2.32.3
     # via hvac
 rich==13.7.1
@@ -110,7 +110,7 @@ sniffio==1.3.1
 soupsieve==2.5
     # via beautifulsoup4
 toml==0.10.2
-    # via splunk-soar-sdk
+    # via 
 tomli==2.0.1
     # via robotframework-tidy
 typing-extensions==4.12.2
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 7ac633e..b9d7788 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -19,6 +19,7 @@ jobs:
       run: |
         python -m pip install --upgrade pip
         pip install robotframework
+        pip install -r .github/tests/requirements.txt
 
     - name: Run Robot Framework tests
       env:

From e9e5c8aae8b48f5b6567bef031a0d91c7afd71b5 Mon Sep 17 00:00:00 2001
From: Daniel Federschmidt 
Date: Tue, 1 Oct 2024 16:36:40 +0200
Subject: [PATCH 04/24] fix: try different path

---
 .github/workflows/main.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index b9d7788..82d5be4 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -23,7 +23,7 @@ jobs:
 
     - name: Run Robot Framework tests
       env:
-        PLAYBOOK: AD_LDAP_Account_Locking.json
+        PLAYBOOK: ${GITHUB_WORKSPACE}/AD_LDAP_Account_Locking.json
       run: |
         mkdir -p results
         robot -d results  -L DEBUG:INFO .github/tests/robot/PlaybookScanner.robot

From a63865ac4da05533e425e6088afbaa7a98dc0cf3 Mon Sep 17 00:00:00 2001
From: Daniel Federschmidt 
Date: Tue, 1 Oct 2024 16:38:11 +0200
Subject: [PATCH 05/24] fix: only nam,e

---
 .github/workflows/main.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 82d5be4..83d9607 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -23,7 +23,7 @@ jobs:
 
     - name: Run Robot Framework tests
       env:
-        PLAYBOOK: ${GITHUB_WORKSPACE}/AD_LDAP_Account_Locking.json
+        PLAYBOOK: AD_LDAP_Account_Locking
       run: |
         mkdir -p results
         robot -d results  -L DEBUG:INFO .github/tests/robot/PlaybookScanner.robot

From 992d60d49f6671e2541f02756d4cdd8ae7b8b2ff Mon Sep 17 00:00:00 2001
From: Daniel Federschmidt 
Date: Tue, 1 Oct 2024 18:04:17 +0200
Subject: [PATCH 06/24] fix: test

---
 .github/tests/find_changed_playbooks.py   | 43 +++++++++++++++++++++++
 .github/tests/run_on_changed_playbooks.py | 41 +++++++++++++++++++++
 .github/workflows/main.yml                |  4 +--
 3 files changed, 85 insertions(+), 3 deletions(-)
 create mode 100644 .github/tests/find_changed_playbooks.py
 create mode 100644 .github/tests/run_on_changed_playbooks.py

diff --git a/.github/tests/find_changed_playbooks.py b/.github/tests/find_changed_playbooks.py
new file mode 100644
index 0000000..b10adf9
--- /dev/null
+++ b/.github/tests/find_changed_playbooks.py
@@ -0,0 +1,43 @@
+import os
+import subprocess
+import argparse
+
+def get_changed_files_without_extension(base_branch):
+    # Run the git diff command to get the changed files compared to the base branch
+    result = subprocess.run(
+        ['git', 'diff', '--name-only', base_branch],
+        stdout=subprocess.PIPE,
+        text=True
+    )
+    
+    files = result.stdout.splitlines()
+    root_files = [
+        file for file in files
+        if os.path.dirname(file) == '' and (file.endswith('.json') or file.endswith('.py'))
+    ]
+
+    files_without_extension = [
+        os.path.splitext(file)[0] for file in root_files if os.path.exists(file)
+    ]
+    
+    return list(set(files_without_extension))
+
+def main():
+    # Create the argument parser
+    parser = argparse.ArgumentParser(description="Get changed JSON or Python files in the root directory without extensions compared to a base branch")
+    
+    # Add an argument for the base branch
+    parser.add_argument('base_branch', type=str, help='The base branch to compare against')
+    
+    # Parse the arguments
+    args = parser.parse_args()
+    
+    # Get changed files compared to the provided base branch
+    changed_files = get_changed_files_without_extension(args.base_branch)
+    
+    # Output the files without extensions
+    for file in changed_files:
+        print(file)
+
+if __name__ == "__main__":
+    main()
\ No newline at end of file
diff --git a/.github/tests/run_on_changed_playbooks.py b/.github/tests/run_on_changed_playbooks.py
new file mode 100644
index 0000000..c14c04a
--- /dev/null
+++ b/.github/tests/run_on_changed_playbooks.py
@@ -0,0 +1,41 @@
+import argparse
+import robot
+from find_changed_playbooks import get_changed_files_without_extension
+
+def run_robot_tests(robot_file: str, playbook: str):
+
+    result = robot.run(
+        robot_file,
+        outputdir='results',
+        loglevel='DEBUG:INFO',
+        variable=[f"PLAYBOOK:{playbook}"]
+    )
+
+    if result == 0:
+        print("Tests passed successfully!")
+    else:
+        print("Tests failed.")
+
+
+
+def main():
+    # Create the argument parser
+    parser = argparse.ArgumentParser(description="Get changed JSON or Python files in the root directory without extensions compared to a base branch")
+    
+    # Add an argument for the base branch
+    parser.add_argument('base_branch', type=str, help='The base branch to compare against')
+    parser.add_argument('robot_path', type=str, help='Path of the robot test suite')
+ 
+    # Parse the arguments
+    args = parser.parse_args()
+    
+    # Get changed files compared to the provided base branch
+    changed_files = get_changed_files_without_extension(args.base_branch)
+    
+    # Output the files without extensions
+    for playbook in changed_files:
+        run_robot_tests(args.robot_path, playbook)
+
+
+if __name__ == "__main__":
+    main()
\ No newline at end of file
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 83d9607..c7a8b38 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -22,11 +22,9 @@ jobs:
         pip install -r .github/tests/requirements.txt
 
     - name: Run Robot Framework tests
-      env:
-        PLAYBOOK: AD_LDAP_Account_Locking
       run: |
         mkdir -p results
-        robot -d results  -L DEBUG:INFO .github/tests/robot/PlaybookScanner.robot
+        python .github/tests/run_on_changed_playbooks.py 6.3 .github/tests/robot/PlaybookScanner.robot
 
     - name: Archive test results
       uses: actions/upload-artifact@v3

From 1a80f184f137856f7ad09b5799f73680b4254ede Mon Sep 17 00:00:00 2001
From: Daniel Federschmidt 
Date: Tue, 1 Oct 2024 18:07:18 +0200
Subject: [PATCH 07/24] fix: test

---
 .github/workflows/main.yml | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index c7a8b38..219cf4c 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -1,6 +1,6 @@
 name: Playbook Validation
 
-on: [push, pull_request]
+on: [pull_request]
 
 jobs:
   test:
@@ -9,6 +9,8 @@ jobs:
     steps:
     - name: Checkout repository
       uses: actions/checkout@v3
+      with:
+        fetch-depth: 0
 
     - name: Set up Python
       uses: actions/setup-python@v4
@@ -24,7 +26,7 @@ jobs:
     - name: Run Robot Framework tests
       run: |
         mkdir -p results
-        python .github/tests/run_on_changed_playbooks.py 6.3 .github/tests/robot/PlaybookScanner.robot
+        python .github/tests/run_on_changed_playbooks.py ${{ github.event.pull_request.base.ref }} .github/tests/robot/PlaybookScanner.robot
 
     - name: Archive test results
       uses: actions/upload-artifact@v3

From 093eba9d49553c6442280f007c3ff6d538e57e03 Mon Sep 17 00:00:00 2001
From: Daniel Federschmidt 
Date: Tue, 1 Oct 2024 18:08:32 +0200
Subject: [PATCH 08/24] fix: fix

---
 .github/workflows/main.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 219cf4c..77d838e 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -1,6 +1,6 @@
 name: Playbook Validation
 
-on: [pull_request]
+on: [push, pull_request]
 
 jobs:
   test:

From a4387cb124cf0def7f98efcecc1fc3c13932f2b3 Mon Sep 17 00:00:00 2001
From: Daniel Federschmidt 
Date: Tue, 1 Oct 2024 18:09:20 +0200
Subject: [PATCH 09/24] fix: fix

---
 .github/workflows/main.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 77d838e..e16b350 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -26,7 +26,7 @@ jobs:
     - name: Run Robot Framework tests
       run: |
         mkdir -p results
-        python .github/tests/run_on_changed_playbooks.py ${{ github.event.pull_request.base.ref }} .github/tests/robot/PlaybookScanner.robot
+        python .github/tests/run_on_changed_playbooks.py 6.3 .github/tests/robot/PlaybookScanner.robot
 
     - name: Archive test results
       uses: actions/upload-artifact@v3

From 6887bbcbd5f4bd76e64fa10ef0754856e08d1150 Mon Sep 17 00:00:00 2001
From: Daniel Federschmidt 
Date: Tue, 1 Oct 2024 18:13:11 +0200
Subject: [PATCH 10/24] fix: fix

---
 .../find_changed_playbooks.cpython-39.pyc        | Bin 0 -> 1484 bytes
 .github/tests/run_on_changed_playbooks.py        |   4 ++--
 .github/workflows/main.yml                       |   2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)
 create mode 100644 .github/tests/__pycache__/find_changed_playbooks.cpython-39.pyc

diff --git a/.github/tests/__pycache__/find_changed_playbooks.cpython-39.pyc b/.github/tests/__pycache__/find_changed_playbooks.cpython-39.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..17bf13e577539769cccfc1689d165cd139ab805e
GIT binary patch
literal 1484
zcma)6ON$#v5boEECfZ%c#(`jyLzCo?0k0H{u^}u=LNt4|WeZ_%HP}r~HMG;8cwsYz&69nCh;s>Uw-trS#ySOJMze|L@V$fRI0s
zxw#lHU&Cj=0-}hbB^j)DI-o7j223%LIto39jNdHGmo`c5eje4N!4Uh*Yf&tA{QXRD3vtC3c)XZfU1u79F`(PiCe
z(}&#h>9Cj8lYWz@m8&P&YzX&Fna+lF{lfNpYn8s!*4h53s8o`H=2)v_rv$P2?onA-
z2Zows^lU>QA!YRQxAP}s?RJkaI-6j;fLsE&>B<&$)jKNdEG_LZ9JY4ekLVX$MCDCH
z4OScQT%&uBY*3^I`z02p1DlC}_)xzBC)P){DGMx~*+Uryz9t{y={AtN@L8;8NaK$0
z+Uc+{b*8QL+)OJk&VD%i&Ipi$3~iM~rJ+QagXdPeIPiPJ)aqnt(kjb+XtbS{&Ic_H
zgRZG-m#ohpzXzIk!t$T}4>+;+?0q1F3EHKCvVX#`OQ9fd!=Rx0G=m4k5&Te0y175u?=aN3;B0Yvz2ZN&tF
z!qDwL5W@EPZ65J1kEn@(_fe9dMv}zgx}C82

literal 0
HcmV?d00001

diff --git a/.github/tests/run_on_changed_playbooks.py b/.github/tests/run_on_changed_playbooks.py
index c14c04a..c09b297 100644
--- a/.github/tests/run_on_changed_playbooks.py
+++ b/.github/tests/run_on_changed_playbooks.py
@@ -23,8 +23,8 @@ def main():
     parser = argparse.ArgumentParser(description="Get changed JSON or Python files in the root directory without extensions compared to a base branch")
     
     # Add an argument for the base branch
-    parser.add_argument('base_branch', type=str, help='The base branch to compare against')
-    parser.add_argument('robot_path', type=str, help='Path of the robot test suite')
+    parser.add_argument('--base_branch', type=str, help='The base branch to compare against')
+    parser.add_argument('--robot-path', type=str, help='Path of the robot test suite')
  
     # Parse the arguments
     args = parser.parse_args()
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index e16b350..27322be 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -26,7 +26,7 @@ jobs:
     - name: Run Robot Framework tests
       run: |
         mkdir -p results
-        python .github/tests/run_on_changed_playbooks.py 6.3 .github/tests/robot/PlaybookScanner.robot
+        python .github/tests/run_on_changed_playbooks.py --base-branch=6.3 --robot-path=.github/tests/robot/PlaybookScanner.robot
 
     - name: Archive test results
       uses: actions/upload-artifact@v3

From 894c36c928f7559008699b660c1a19a0d71b3881 Mon Sep 17 00:00:00 2001
From: Daniel Federschmidt 
Date: Tue, 1 Oct 2024 18:13:54 +0200
Subject: [PATCH 11/24] fix: fix

---
 .github/tests/run_on_changed_playbooks.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/tests/run_on_changed_playbooks.py b/.github/tests/run_on_changed_playbooks.py
index c09b297..be57116 100644
--- a/.github/tests/run_on_changed_playbooks.py
+++ b/.github/tests/run_on_changed_playbooks.py
@@ -23,7 +23,7 @@ def main():
     parser = argparse.ArgumentParser(description="Get changed JSON or Python files in the root directory without extensions compared to a base branch")
     
     # Add an argument for the base branch
-    parser.add_argument('--base_branch', type=str, help='The base branch to compare against')
+    parser.add_argument('--base-branch', type=str, help='The base branch to compare against')
     parser.add_argument('--robot-path', type=str, help='Path of the robot test suite')
  
     # Parse the arguments

From 0cd9671d40def3787e9eb93bb0407b446dd34feb Mon Sep 17 00:00:00 2001
From: Daniel Federschmidt 
Date: Tue, 1 Oct 2024 18:15:51 +0200
Subject: [PATCH 12/24] fix: fetch base

---
 .github/workflows/main.yml | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 27322be..2a435a1 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -11,6 +11,9 @@ jobs:
       uses: actions/checkout@v3
       with:
         fetch-depth: 0
+    - name: Fetch all branches
+      run: |
+        git fetch --all
 
     - name: Set up Python
       uses: actions/setup-python@v4

From 7ca4d7d7f1238694d258d5d2fcddc5b34d00716b Mon Sep 17 00:00:00 2001
From: Daniel Federschmidt 
Date: Tue, 1 Oct 2024 18:19:05 +0200
Subject: [PATCH 13/24] fix: fix

---
 .github/tests/find_changed_playbooks.py   | 43 -----------------------
 .github/tests/run_on_changed_playbooks.py | 26 ++++++++++++++
 2 files changed, 26 insertions(+), 43 deletions(-)
 delete mode 100644 .github/tests/find_changed_playbooks.py

diff --git a/.github/tests/find_changed_playbooks.py b/.github/tests/find_changed_playbooks.py
deleted file mode 100644
index b10adf9..0000000
--- a/.github/tests/find_changed_playbooks.py
+++ /dev/null
@@ -1,43 +0,0 @@
-import os
-import subprocess
-import argparse
-
-def get_changed_files_without_extension(base_branch):
-    # Run the git diff command to get the changed files compared to the base branch
-    result = subprocess.run(
-        ['git', 'diff', '--name-only', base_branch],
-        stdout=subprocess.PIPE,
-        text=True
-    )
-    
-    files = result.stdout.splitlines()
-    root_files = [
-        file for file in files
-        if os.path.dirname(file) == '' and (file.endswith('.json') or file.endswith('.py'))
-    ]
-
-    files_without_extension = [
-        os.path.splitext(file)[0] for file in root_files if os.path.exists(file)
-    ]
-    
-    return list(set(files_without_extension))
-
-def main():
-    # Create the argument parser
-    parser = argparse.ArgumentParser(description="Get changed JSON or Python files in the root directory without extensions compared to a base branch")
-    
-    # Add an argument for the base branch
-    parser.add_argument('base_branch', type=str, help='The base branch to compare against')
-    
-    # Parse the arguments
-    args = parser.parse_args()
-    
-    # Get changed files compared to the provided base branch
-    changed_files = get_changed_files_without_extension(args.base_branch)
-    
-    # Output the files without extensions
-    for file in changed_files:
-        print(file)
-
-if __name__ == "__main__":
-    main()
\ No newline at end of file
diff --git a/.github/tests/run_on_changed_playbooks.py b/.github/tests/run_on_changed_playbooks.py
index be57116..bd0e79f 100644
--- a/.github/tests/run_on_changed_playbooks.py
+++ b/.github/tests/run_on_changed_playbooks.py
@@ -1,7 +1,33 @@
 import argparse
 import robot
+import os
+import subprocess
 from find_changed_playbooks import get_changed_files_without_extension
 
+def get_changed_files_without_extension(base_branch):
+    # Run the git diff command to get the changed files compared to the base branch
+    result = subprocess.run(
+        ['git', 'diff', '--name-only', base_branch],
+        stdout=subprocess.PIPE,
+        text=True
+    )
+    
+    files = result.stdout.splitlines()
+
+    # Only consider files in the root directory and with .json or .py extensions
+    root_files = [
+        file for file in files
+        if '/' not in file and (file.endswith('.json') or file.endswith('.py'))  # No subdirectories allowed
+    ]
+
+    # Remove extensions and only return the files that still exist in the working directory
+    files_without_extension = [
+        os.path.splitext(file)[0] for file in root_files if os.path.exists(file)
+    ]
+    
+    # Return unique file names without extensions
+    return list(set(files_without_extension))
+
 def run_robot_tests(robot_file: str, playbook: str):
 
     result = robot.run(

From 84413281d0c4b8b0b10a208bc81a0ee5393428ec Mon Sep 17 00:00:00 2001
From: Daniel Federschmidt 
Date: Tue, 1 Oct 2024 18:20:02 +0200
Subject: [PATCH 14/24] fix: fix

---
 .github/tests/run_on_changed_playbooks.py | 1 -
 1 file changed, 1 deletion(-)

diff --git a/.github/tests/run_on_changed_playbooks.py b/.github/tests/run_on_changed_playbooks.py
index bd0e79f..d4ee98d 100644
--- a/.github/tests/run_on_changed_playbooks.py
+++ b/.github/tests/run_on_changed_playbooks.py
@@ -2,7 +2,6 @@
 import robot
 import os
 import subprocess
-from find_changed_playbooks import get_changed_files_without_extension
 
 def get_changed_files_without_extension(base_branch):
     # Run the git diff command to get the changed files compared to the base branch

From 13b8fb655cddb14155d7fe8478141785a9295c02 Mon Sep 17 00:00:00 2001
From: Daniel Federschmidt 
Date: Wed, 2 Oct 2024 12:32:52 +0200
Subject: [PATCH 15/24] fix: fix

---
 .../DynamicTestCases.cpython-39.pyc           | Bin 0 -> 3152 bytes
 .../PlaybookScannerHelper.cpython-39.pyc      | Bin 0 -> 1278 bytes
 .../__pycache__/__init__.cpython-39.pyc       | Bin 0 -> 282 bytes
 .../playbook_parser.cpython-39.pyc            | Bin 0 -> 10524 bytes
 .../__pycache__/utils.cpython-39.pyc          | Bin 0 -> 7398 bytes
 .github/tests/run_on_changed_playbooks.py     |  22 +++++++++---------
 6 files changed, 11 insertions(+), 11 deletions(-)
 create mode 100644 .github/tests/robot/__pycache__/DynamicTestCases.cpython-39.pyc
 create mode 100644 .github/tests/robot/__pycache__/PlaybookScannerHelper.cpython-39.pyc
 create mode 100644 .github/tests/robot/soar_robot_utils/__pycache__/__init__.cpython-39.pyc
 create mode 100644 .github/tests/robot/soar_robot_utils/__pycache__/playbook_parser.cpython-39.pyc
 create mode 100644 .github/tests/robot/soar_robot_utils/__pycache__/utils.cpython-39.pyc

diff --git a/.github/tests/robot/__pycache__/DynamicTestCases.cpython-39.pyc b/.github/tests/robot/__pycache__/DynamicTestCases.cpython-39.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..aae50632585e2f6f7479f65d5720199b1391068b
GIT binary patch
literal 3152
zcmeHJPj4GV6yJaT8z-b`DWolQ1YC@Y7ok8=R3XyTsv=NQ*)9^kjMh8j*vZapM`wjR2EoSTVAdj@cjAyKmFff
z_7{HS9|u0Zg;zcR!i=uLOlAe<$m&`nyK9e}u45Wc4Q4av#9$6D{AzcLtk5><#py5Y
zIEq4{Bw>@WHyZk05KM^2*l@g0JfgT%#P=j8EEc#V!d|FIUw9*a9EwB2f98IyhT*tT
zt!^@m@Y-T#*Jf7NN&V~=Sb-H!4DhtX$}pCA
znOAs~RerNsl`Wk(-KE@0jV;4UEw{45-hq{6whD2&k*q#R!;_s(zOupewAyL5JEZ-5
zr_=P!+zD}R!MhEwTm>?NABE%qg~PUuQ**a&CAV99kM}zC`<=(Ft=5k)wmZ$;W-D2t
zek=qZD=N|dx|ukV2mMn+EP<}S2kvvpg>+e;GZ_8BXvmcNgdg!BjJR<3hiVY_bg-j=
zH&)@upY&io3cN`#3=gH-NLgIWuXJ@%-HRSJqDfMwbT}R=N>|Vk47*_#&FT6j-i*)3
zuw)O;7g0bA&&)ImZ9|mcvu=tC4z|F}q@bNmih|2HP}(0ZL*Xku_M6sM@fn
zA6!06j?>p%$O#WRfbUay1KM3E
zI5XMeDi&jq?d11?+f(G&1AUBfwlD>7bI^9;pmvBkhCV511SCaYa1Vgy=uC(kuq80Y
zNd@;QR)CB7SZC>tpbHL3Q!Vd-V2YK>nmPUSQhKfwghRcHMr(&@F`)XqfCu#R9I^_)
zwn*eH_?VfP$eA^>bsC+SgK6{{a1q-asvCx|7qSVNtJYD5wf*4{AD;#HjR{>idMV=b
zOo6^ky$TzP$Ril?9;M>wTXu>wDNNdVNe_XgQw+0s1VuqZBl4sk(XqLBgKVc&11fkp
z1|rkie(oTIM4=pF#r6WS(0FFDZt3hN4%$V`VAPV8G_!PW>9mmI1hXYD-C`X`QkrjH
zgv#C5WL$DZQT4|lI0wtBIel;)uwhyKuPwRiS*lbhLI
z+LX1o_ntMyGP;C~oWQn0*gz6nDj~4F5FaA>2+1c%uu01iY+nS{ChT%_H&`|u)3M4<
zxlpdH)z+5v?ry!5lqhAPPign2?#UV=9*?2zZh#v+NNu5Mf&YC;G3*_1Uuof-Ic;6H
TOyx(Z``=(Ys*9{syk-9j3YS;*

literal 0
HcmV?d00001

diff --git a/.github/tests/robot/__pycache__/PlaybookScannerHelper.cpython-39.pyc b/.github/tests/robot/__pycache__/PlaybookScannerHelper.cpython-39.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..871524390bded071a57baf27b7428d8d614b4ebe
GIT binary patch
literal 1278
zcmZux&u`N(6t
z5W_J_u!pgECoxGZ&%$UQF^kz3h}mC>XS3Qd>eYY2dDO%C=E#?dvr+8N10g;@q%oL9
zu|7ycbD}y{SdSsfk%t-b2*aMmZC+#Kk`ySmSLZd@*I-{~jSJgruqGh3^6e$aap
zz-Qk_l4ri;%JH3SQIj(f($QS$C_U-7+sSle#aU4dg^FV$r8m}J#s!r
zdMakx;isCXDiUcW<+z}HBWo$Jvn<{b!FwL+{tCHAq^Z$sO{7SuhP*v1w-t{kvJUZ_
zWWhHiO&k19+_#EL<+2H9@C>I(#I*a8f8wzKDcuv`HVgWpNZf4dr&=W8Jb?I83FY?7
zBd(D{xl#l|yGv$kw)C@kzCA4*q9CXOUlww<8=CW+SvgWI)
z-(lL|1*SPTYa!+K@nyVGr|93JAjDe=H|M^$yZY&MB6Pk3alH*qE3Q$N2VvYeC@&1vgE>Mb*Z^!bZ6PZ+RCe%vYjrLr4{^-a5O#1X?#_?fg-%
znKvmdI+o^bN)y3mF{d=|Q2KG^$BQp_#>2tz4Lv*AHaWO3kaAu5fJrJC&b!M^ZBiH
Qay05Le-ZO3mu2zw4dlvDn*aa+

literal 0
HcmV?d00001

diff --git a/.github/tests/robot/soar_robot_utils/__pycache__/playbook_parser.cpython-39.pyc b/.github/tests/robot/soar_robot_utils/__pycache__/playbook_parser.cpython-39.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..4fdf85b0efdb97de6be04e55fa345832c660efb3
GIT binary patch
literal 10524
zcmb_iU2GdycAh&k9Fil7qGVgPz_tP0J`@F-08&vDbx{=iP!vV64}I%f`#ybZpYqrTU2FE)xu+p^Fut(P0+b+_RPBNRImVP#h|CZ*G;%3@wj0xOqA`P7GpJSJkZK*^*kqQr8wW%CJwsOI)MGIChyaJz%Zmsl?Y`6sRiDh}oKcCfh|
zMfa|$iyLtgtzX&bG^u_%yW9?%_xx5Isl;E2)OwKkaiV(lg?G-}>74Jb1sznsv)IiD
z_{;66dGF4`oqGM9I~^IWc)jbVzUE&l+}?5b)G6=1cC;L{Jw0UaVkATFW)i5x?H%2x
z_;N?Oy;~lBU-Otw_2yko_IblO^1Lz{OOrNh^k*Y+9#8x}g3ur)ZkV^h*bOVR!-6!=
z8le-~a|YrT;*@8fHDp0LkBvq#EQPi#f@w;!{MZJQm8A=&m`Eply;!tJy#>AL-l|@|
zCM{f$crmS=7p-{tW-g|st_poBPbZqp7&=kzMsdQ?Ch<}JGZC@yT*MQ=B*1iZqX!k
z^77IpRY8TPW@^V_d!=Coo%pGtW{~&QqtmzJP{pU^N+|JeuC2FZa{5yEAZ$n7P@P_F
zC2JeYi_K{LbjH5T&1K~0vyM+M>MEy`FizsrDq4<`({U6ipQHXp(rU+p>VB6bNG*0Z
z)1ptZ;rpjBjF^E-IKmY(czOp$HefNU_f(`dEpC$*F8>h+ICyvwJBTDkUpz3>kEBS%
zj+q#{0zhMO+~Te9;%%=BHTTzP01X!mi-Alf8?(V%xfj
zE$S0cmMAU_@Je~W~rv5!qxCmH!X7QQ1aI6-~g2ucqHFMG^VZ$vm{Q4ima2wUL@u>yv4&#IqPjm&oW)ECC^Rr2G!JXguoyHJ0U9=P0T@YP1T>r+G}I&rfTaj<
z)VC-Y0!YRG@Y=-zez;jGhYwYw+QWU}Eur1Jj3*Od5?&VILwi;v7h!b>Rrknm5g8>s
zEWiJZ2;|lviT%Q2DK$r=^tHs2Rwkv^jxFt7v1LO_p_Cw2;27j}%Sj4-2N>|zvIsdx
zisinLCm9A1btl7s$Dr;GFu?9>dJ`=FwuF+y^e2G>1;zoY;Q$M&lx8fLTIvn3
zK=19j-aI1%?eMuf4`1hm0#2S=e7)X#Z5#=5-q4J*plQ^HTb0_>RvpGNDUt&eOJQpgA*o{_a)r-B>iq}d!8HFg05-(^g2=}HJcBJo$&?xqjwa{A**MbKvzyy?uo7XPf@E)#(9Z!YC7}7SJ#`~cFbL47l$Dh=Ql1!od=;xV(;qBYfB#K(~CLHB?thWc-J?9@SOn&dL$T&J@d{-
zsqpI-^Bm5Kjpek^jvj_eok3^glR56+jiZjAM1Gg4`FSTi@Ox+xvxZ=)bBH(b?D^4d
zpT`sb8G_-N5JF>=0+?UG+sxi{E;wNU`XkV0Bu-~To{#fk5HF6zb$*fCmN;L>%ZQWS
z>5k-0AYMdX1!v797PFd8<~GK=WFe#_HWd7pOkEwLh0Ei~W)~V~z$S>#iD_LaQ$o3J
z0c;t@>4by^p;~O-q@|$S^;?~lD4opJIi0L+Ku2SZ)ej%Fpw^{R8LfP+Q=8f?)yrxD
z%c$xUkp7Tbtxlreq~seE&}yf)Y&DbAhRM_(+03EnzJp5l@x&y6E|d@Y6EY0{lvY&l
znf;_Qo_?_dSl{AkO5&}~K7lxa>QmirMf?AEwFuIV5#+p0}
z9BDIWU@|h|0jdbPa<8ms-a-e<_E0&jr6=4CVoy|8_Mw+??#wx
zs7~w!UK@N#h)KLfwz_Hodyx*>ZH=lVIKbG>o`DXAN7OZ{GM}IER8G0G>19WzHUhP&
zoYT>x&c`(%7mt%eM(n{9TI24o0x98lz?)L^N%BHBp08;O_E80-OUIMp55@2n
zoapf3X!eee4PY;={f9vYVm=HBK%%pM2@sFtUR^<%x!N}3{
zg59ehQFoUq*td5o{i~7Q503TD=i~nUKD+v*W{`xdk=n$9Gb=oA&ZKPp%}B4~+n&>S
zk&f*oSdG>1g6eaZIY7bRjdXcnY@*cqMY?m^!Ey*s!oD3YqqColba-g2L*4#Gx*Vz_
zEVldZDJcX0Fw*m}v7U37X07N&22jjIYF~CrYXjO@)qffp$4mQ-Bdhx&Lvi4f3)h2v
zN1|5H@y|y(A76gmeyMIUmy$1tj?g_;IAJj{IOEI%M0!>V9N&-ms0t+&m+U
zEmIi1gRqpvw)j+hWPA*?-79(3%@u0V>=TvPej
z=$LP53%an>d-Hnc8~3iO=s`<{lFTHwsk{fUh@{q8u=?bIXTW3WPodxfK8#XKOq
zH;!H-r%GL@RkYZ%%jAG1I=sXyP*qp8+lJws7HUWA8kdYG;*z3U+n5D455@yO)0h=|;o7K^aES;@_Uf@0)7I1c(TTaJDt|LnWnm?E2
zVLYQ)0<~fmFC$*+7yBjYqJ5d$m*qs?!r1;zR<>N^Om4Y}1=kaf@~l-A2eT!oIK(`5
z3bH2QY|*7iO}C6)6YrVl<{o%%?m^CV`sKbmpmQjzIr~{#v&{HbwXsA4oWP1N^ja46VYv7?qxfyjj(D5SGs12epzgJo~
z7|5joY1h^rDQFEBzEx;-9IxcKwL~QY6uS|2Bg#R$A^1{*bS7`$E7V~kX^HH^i;%Zl
zSJFyu5>j(@i-zZFTl5y$rKLbV2s+IWmaEoKxnG-3TUHkuRh@k$^NZFi>O5$u3lzLd
z0ZELyL;)Z0tYf@K$@eMvfP$+Od>;W^QmnfbT1mJbr|t*1RC~no&c>r6&r@na#Z4X9
zzVMl#JA#46>|mB`I>)}&dmEYI{lTS1W_Xj?tLUO(WaVK&(9$a&RfICtrGUoQC=X2O
z#+0_o$#C&$d}DZ%9$++nZvv+drTzXE2dq9g=$-={+Md+0)esKRlBPZ3bcZOYA|0nB7
z19wfEVHerAMNEa0iZ)R_q~JM9RXRoE9x>hu7-z0jj$P3%j#b?=J<^299O3DmM1-bC
zILx;hP-5MM$stW?eMU||dx;14CAEX!&1r$dItlLK_#nXrRd-X5zJ&^DL2j&f
zaigr6Y(g^?8=T{tKS=`}*j;6skpl__wiEYZ1
z94SS!1oN6(bWs8|23LRB+imlK5nsOwRfQCr#Mv=rAt~aj!1)4K4ic_Av@ny>PC2o5
z+*P^;MD8Er-2O|LqY|g(pFmmK6{{kdNGf;B$TOv&@@*3}Xe}ws*RtF@$D$bL4goSQ
zy?}hn%`lDwwaGSqE>-Mm(RWI?0oZ6Ki%W_&`Ab@f-eTrWU>f9c#%1j~n9?JNLo07R
zQt-aw;{z`O68Fjr@0|Ufi@S*LeI@E&{t`bvGwPGPr@Xtci@WVsqVE~4b_jh+1)bGU
z-wJ#Oz4C>dzN-5%B|oKreC(NG)hIEAB$SQviV9X~K=C-!&k`SZd19kh_F>`>y5KM@
z<`AnY=FKDK$zPi0%>OtxxaHO70ogNj_Am`yk&{F*)BHJl&`d*$7h##Sz&^(kQ!_wo
zR5^oUwPTTT_Q@Mt@I;VOCLO3mBrNb$;J#VJH`r4F$k2Ibjr(uIZ&Aopx&_=**>@fE
zVW{5ywWQbwn7cWrqp!u_h#R?1-C#E+e{jPR!yn)*^1;%`6b?F4
zmU?qna9KX|mcgb7Wh()DE~+(fg;EH5v$)2&r8YvX!FA#3OcuUXcYBkh$m!(UNS22s
zSroa@oE+TVue2VePQ0;#x9Sn$n1Y0;e+Sy)o1<>J%oCa`P-N(2gJLHws|#K#7S(+;
zuUAJ-#g~zhIm=dlLC>%bijrkRS*gG?R>f)HLSdm#51`Ia2<4?DOv?cV9D(6gGwFJ{hWJ7JXGHJi
zF(i1lulljEI|}tW`qk$ZdFbe~9s1NI*;M2s(@rc_HOYCS0t%KXXi^YTutEWuhpA24
zoZ6)H?@{mv6l_yK3WDAS-T~g9KSdrs7SOK3X4r1$?m}L%kEz803a%j_K#l1#uO#fr
z*@`J#>sLLssp@yDcC}C~&mUwOm^!N=#dfs13V%PtcVmKlb?mJK^O)!%b>L>9Vhaf#
zcv|4kb`$|7vr$?GRDTF6u#^|L36VsC&Vt6^Q=^-guk*($d~h`KF9<02ZKCvT3V1o_
zq|jSH*N>Wo>3WBUJ#tL&!qA$erL+1YlXGAlBYp!xeos_0Yo=@4;G!9D28URx+HpJ!
K)k@XDyY|10asR3S

literal 0
HcmV?d00001

diff --git a/.github/tests/robot/soar_robot_utils/__pycache__/utils.cpython-39.pyc b/.github/tests/robot/soar_robot_utils/__pycache__/utils.cpython-39.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..ad6a59f7d66db1d68170be976a3a88298261a5d1
GIT binary patch
literal 7398
zcmd^D&2JmW6`$E%E$_1m^xdqR
z`fk-N+|4l8w(B-0Su4!93-yAo8)30t)N-YI$uG8ML{8XuczxC{wdPRIi^3hFUVfX2
zqA1;FqO@hy5BZh!FJ|sqVph!Ew(5tc`pV)E`sSzlD&jEu9uvkjR-NxJU+|kj$5&1-
z4#FS~P-{lgY4^f7=!U-2?a6MWd{rvF8?5_IyxMn~K?o_YBS^CC#c|MCbsCZIYo*eC
z>;zk`@+5zLBlgwhE2z#ddu?A__uJh_dUE3uhTp#hF+7?H@oqQtJFD?pl3NMA#(LF|
zw6FDiS4Q-v2F;lLS!kTYrT&P*XV`58tu0>9
z`}tM@eFgLt$9<_xNz7nOaX2E~&5}QZb{W!#L`Baqi}qnLf7cL?i6fAo(-MzE;;1;L
zCCXajI3!MpCm?Z1OPqwnDe)~WQ5nADw0IKF4~xUlgp-`U>Ph8`tD(2C5=HA*|f
zo^(6V(xOAUa-ybl^}GWZEM2Ni3$;|#aY@&U&O6txEIZLk%WuScgt{B?+F0zWZVd53
zFRPkKas0xhtgGW`lZtL!F7X95WDVayj|HS7`OQ>?>|4S;l0SHjmE*CZ0W~6*=cYZJ^WFfF1=|q?SQrP
z_ViD_ucehUTC%eMl{mQ<&LLxV>D)O(Q#nnX)qv&LmP4|y<#UgG1Z?^&ljg1#4U4VSrZ
zUkFh8#T8Uy)&hiDnzru{J(~UMXoob~3HW3j@tW^6v~@bop?TKs(^RB^pd(vPV;5t7M*KRPd0r0PPmPDO+R&Rg14ex?D)50
zzoXz(BiPGU*l6V-fTHbNy&LN(gdlA?yP
zmdkfHv`&a9DNhi{HOIU$(PD+p-A}6<*FsWP1W2@8N|LkLC+BX
zvZ|dDt={fusP|bao}=PS3{b19Gjo1`W?`i>BkO?imQ#xymG>@)20sNAI8VblSe#M+q*bA-%H`|&zb>Y{_Nc0`pF@24E~LL2VY
z@sVQ?TRBhFApcZ#LR5WAF0S_Xc|8+|xJGCrzdv*qFk-<^Fk!4~m%eW
zeE;m#VVpQls-TWe>;)a=v?GaJ3qhrGR{IZ~a@C|>IhktzxskdiV?-bkibrz8QL*eb
z5C;0SV#h{%BRtkXS66@ST79OU!@!|p^S&JMPamI?_&~m6a_0)_)!GF
zL>d)bR6BlfI6RsMj)B%A_c;dQ4oV9&cl;KW33qINhQNYCKKwaTwtG^``Fmbfg@Op_`Y%=MS98@ldb}q(N%;m-
zkopwy0Yms%<}W`Y!(il;|DlzRA@NHz#*kRsqyQ4p38|Dbq$j9pc)SF#5HySw8aw%2
zKm!TNSmo0#4S;$U0Q-P+EBkZ^kI%med+1yxwM?g{2s`O+XT13^iJY7tIr#m1^-AJe
zc>zUY$cbX#qFzlPnls*}=cLv2{gPaad<_LWwWfp1)NziAJ<23&$GBhPQL4)PEZha+
z%tx?sjH}Mm>5I;t+@$2X?MU=O-*uB&*Zs8Tg~KO#*A-C%-6mYDq$7i*fMZG=2E`j`
z{$ndYhGU(?%*l*CsB8PkxrC&n2>BuvgjK0O{~fBmN5$lubd6$YNhvTLY$)1`0=EkD
zIfGX$3s)I`)DKnWP(EInM_JbLbgreD)lPG2Ndb831_`92A8{BRwUTPoXDE;ZF$Bka
zjjtQ>G+2yU#(GYkrc4(Q#>vPoS1%2~b>C;30CLNs!k0N5QTpLA68@?@ng@4dfLW0-Jk+TV{zDjH5;wh9^DrH69AL6+8u?03-lqUH}M)
zvfwydMhomXh7?ZohmeB%gyd^ONC9wYT^d3Pa08fZ4JmsHQs%*s3REO1|0&uN%~HBrjuq=ubF;NhNdcmUx^
zKeOb8p>p=3Q~R>Ludg95AaF{0Bf^C};5QL7?EQqJ>BM_y`U(>$S|T%Nxui(5dPM#h
zH%X36pZ}(56h*~}5v0K{4m3b6IxHdc(1t1-sMRf;b^DSs%*0;O>X1H3kPncQ6;0Ny
s-35-9q^CmOgT0xp~7pEKjg$s
Date: Wed, 2 Oct 2024 12:39:12 +0200
Subject: [PATCH 16/24] feat: test

---
 .github/tests/run_on_changed_playbooks.py |  2 +-
 .github/workflows/main.yml                | 10 ++++++++--
 2 files changed, 9 insertions(+), 3 deletions(-)

diff --git a/.github/tests/run_on_changed_playbooks.py b/.github/tests/run_on_changed_playbooks.py
index 84aac24..50d1ae2 100644
--- a/.github/tests/run_on_changed_playbooks.py
+++ b/.github/tests/run_on_changed_playbooks.py
@@ -63,4 +63,4 @@ def main(args):
     # Parse the arguments
     args = parser.parse_args()
 
-    run_robot_tests(args.robot_path, "Active_Directory_Disable_Account_Dispatch")
\ No newline at end of file
+    main(args=args)
\ No newline at end of file
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 2a435a1..db1ebdb 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -1,6 +1,6 @@
 name: Playbook Validation
 
-on: [push, pull_request]
+on: [pull_request]
 
 jobs:
   test:
@@ -15,6 +15,7 @@ jobs:
       run: |
         git fetch --all
 
+
     - name: Set up Python
       uses: actions/setup-python@v4
       with:
@@ -26,10 +27,15 @@ jobs:
         pip install robotframework
         pip install -r .github/tests/requirements.txt
 
+    - name: Dump GitHub context
+      env:
+        GITHUB_CONTEXT: ${{ toJson(github) }}
+      run: echo "$GITHUB_CONTEXT"
+
     - name: Run Robot Framework tests
       run: |
         mkdir -p results
-        python .github/tests/run_on_changed_playbooks.py --base-branch=6.3 --robot-path=.github/tests/robot/PlaybookScanner.robot
+        python .github/tests/run_on_changed_playbooks.py --base-branch=${{github.event.pull_request.base.ref}} --robot-path=.github/tests/robot/PlaybookScanner.robot
 
     - name: Archive test results
       uses: actions/upload-artifact@v3

From 1729c1f16bfd457966af98c60c271ad42f462c74 Mon Sep 17 00:00:00 2001
From: Daniel Federschmidt 
Date: Wed, 2 Oct 2024 12:42:47 +0200
Subject: [PATCH 17/24] fix: add comparator

---
 .github/tests/run_on_changed_playbooks.py | 5 +++--
 .github/workflows/main.yml                | 2 +-
 2 files changed, 4 insertions(+), 3 deletions(-)

diff --git a/.github/tests/run_on_changed_playbooks.py b/.github/tests/run_on_changed_playbooks.py
index 50d1ae2..930a362 100644
--- a/.github/tests/run_on_changed_playbooks.py
+++ b/.github/tests/run_on_changed_playbooks.py
@@ -3,10 +3,10 @@
 import os
 import subprocess
 
-def get_changed_files_without_extension(base_branch):
+def get_changed_files_without_extension(base_branch, current_branch):
     # Run the git diff command to get the changed files compared to the base branch
     result = subprocess.run(
-        ['git', 'diff', '--name-only', base_branch],
+        ['git', 'diff', '--name-only', base_branch, current_branch],
         stdout=subprocess.PIPE,
         text=True
     )
@@ -58,6 +58,7 @@ def main(args):
     
     # Add an argument for the base branch
     parser.add_argument('--base-branch', type=str, help='The base branch to compare against')
+    parser.add_argument('--current-branch', type=str, help='The current branch to compare against')
     parser.add_argument('--robot-path', type=str, help='Path of the robot test suite')
  
     # Parse the arguments
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index db1ebdb..48b96e0 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -35,7 +35,7 @@ jobs:
     - name: Run Robot Framework tests
       run: |
         mkdir -p results
-        python .github/tests/run_on_changed_playbooks.py --base-branch=${{github.event.pull_request.base.ref}} --robot-path=.github/tests/robot/PlaybookScanner.robot
+        python .github/tests/run_on_changed_playbooks.py --current-branch=${{github.event.pull_request.head.ref}} --base-branch=${{github.event.pull_request.base.ref}} --robot-path=.github/tests/robot/PlaybookScanner.robot
 
     - name: Archive test results
       uses: actions/upload-artifact@v3

From 5d1fd216de9e1574b330db9b319a879c0f56ae05 Mon Sep 17 00:00:00 2001
From: Daniel Federschmidt 
Date: Wed, 2 Oct 2024 12:43:40 +0200
Subject: [PATCH 18/24] fix: fix

---
 .github/tests/run_on_changed_playbooks.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/tests/run_on_changed_playbooks.py b/.github/tests/run_on_changed_playbooks.py
index 930a362..ce86858 100644
--- a/.github/tests/run_on_changed_playbooks.py
+++ b/.github/tests/run_on_changed_playbooks.py
@@ -45,7 +45,7 @@ def run_robot_tests(robot_file: str, playbook: str):
 def main(args):
     
     # Get changed files compared to the provided base branch
-    changed_files = get_changed_files_without_extension(args.base_branch)
+    changed_files = get_changed_files_without_extension(args.base_branch, args.current_branch)
     
     # Output the files without extensions
     for playbook in changed_files:

From c7f5e2d64edc12dbfdbafede38c7e462b0b89dd3 Mon Sep 17 00:00:00 2001
From: Daniel Federschmidt 
Date: Wed, 2 Oct 2024 12:46:04 +0200
Subject: [PATCH 19/24] fix: maybe this works

---
 .github/workflows/main.yml | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 48b96e0..4a2a159 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -8,9 +8,10 @@ jobs:
 
     steps:
     - name: Checkout repository
-      uses: actions/checkout@v3
+      uses: actions/checkout@v4
       with:
         fetch-depth: 0
+
     - name: Fetch all branches
       run: |
         git fetch --all

From 9e934126e82216421252070a7d61a7a861af77d5 Mon Sep 17 00:00:00 2001
From: Daniel Federschmidt 
Date: Wed, 2 Oct 2024 12:48:24 +0200
Subject: [PATCH 20/24] fix: fix

---
 .github/workflows/main.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 4a2a159..4a56eb1 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -36,7 +36,7 @@ jobs:
     - name: Run Robot Framework tests
       run: |
         mkdir -p results
-        python .github/tests/run_on_changed_playbooks.py --current-branch=${{github.event.pull_request.head.ref}} --base-branch=${{github.event.pull_request.base.ref}} --robot-path=.github/tests/robot/PlaybookScanner.robot
+        python .github/tests/run_on_changed_playbooks.py --current-branch=origin/${{github.event.pull_request.head.ref}} --base-branch=origin/${{github.event.pull_request.base.ref}} --robot-path=.github/tests/robot/PlaybookScanner.robot
 
     - name: Archive test results
       uses: actions/upload-artifact@v3

From a7ed1e38b5f87cb4b28b36446299f84204ea9e0a Mon Sep 17 00:00:00 2001
From: Daniel Federschmidt 
Date: Wed, 2 Oct 2024 12:49:58 +0200
Subject: [PATCH 21/24] fix: print out changed files

---
 .github/tests/run_on_changed_playbooks.py | 1 +
 1 file changed, 1 insertion(+)

diff --git a/.github/tests/run_on_changed_playbooks.py b/.github/tests/run_on_changed_playbooks.py
index ce86858..90ac80c 100644
--- a/.github/tests/run_on_changed_playbooks.py
+++ b/.github/tests/run_on_changed_playbooks.py
@@ -47,6 +47,7 @@ def main(args):
     # Get changed files compared to the provided base branch
     changed_files = get_changed_files_without_extension(args.base_branch, args.current_branch)
     
+    print(changed_files)
     # Output the files without extensions
     for playbook in changed_files:
         run_robot_tests(args.robot_path, playbook)

From c46e6c7d6c61006d4d43650379bad23a70c41efb Mon Sep 17 00:00:00 2001
From: Daniel Federschmidt 
Date: Wed, 2 Oct 2024 13:05:44 +0200
Subject: [PATCH 22/24] fix: rename

---
 .github/workflows/main.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 4a56eb1..4902308 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -3,7 +3,7 @@ name: Playbook Validation
 on: [pull_request]
 
 jobs:
-  test:
+  validate_playbook:
     runs-on: ubuntu-latest
 
     steps:

From dd0eef41c3ce38b64eead4f2a301e77a13f1c9e7 Mon Sep 17 00:00:00 2001
From: Daniel Federschmidt 
Date: Wed, 2 Oct 2024 13:13:50 +0200
Subject: [PATCH 23/24] fix: use proper diffing

---
 .github/tests/run_on_changed_playbooks.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/tests/run_on_changed_playbooks.py b/.github/tests/run_on_changed_playbooks.py
index 90ac80c..6528e23 100644
--- a/.github/tests/run_on_changed_playbooks.py
+++ b/.github/tests/run_on_changed_playbooks.py
@@ -6,7 +6,7 @@
 def get_changed_files_without_extension(base_branch, current_branch):
     # Run the git diff command to get the changed files compared to the base branch
     result = subprocess.run(
-        ['git', 'diff', '--name-only', base_branch, current_branch],
+        ['git', 'diff', '--name-only', f"{base_branch}...{current_branch}"],
         stdout=subprocess.PIPE,
         text=True
     )

From 7dcd1a930fe3c628aa90d76c8f05a8a69675d9ca Mon Sep 17 00:00:00 2001
From: Kelby Shelton 
Date: Fri, 4 Oct 2024 12:33:22 -0500
Subject: [PATCH 24/24] Delete find_changed_playbooks.cpython-39.pyc

---
 .../find_changed_playbooks.cpython-39.pyc        | Bin 1484 -> 0 bytes
 1 file changed, 0 insertions(+), 0 deletions(-)
 delete mode 100644 .github/tests/__pycache__/find_changed_playbooks.cpython-39.pyc

diff --git a/.github/tests/__pycache__/find_changed_playbooks.cpython-39.pyc b/.github/tests/__pycache__/find_changed_playbooks.cpython-39.pyc
deleted file mode 100644
index 17bf13e577539769cccfc1689d165cd139ab805e..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 1484
zcma)6ON$#v5boEECfZ%c#(`jyLzCo?0k0H{u^}u=LNt4|WeZ_%HP}r~HMG;8cwsYz&69nCh;s>Uw-trS#ySOJMze|L@V$fRI0s
zxw#lHU&Cj=0-}hbB^j)DI-o7j223%LIto39jNdHGmo`c5eje4N!4Uh*Yf&tA{QXRD3vtC3c)XZfU1u79F`(PiCe
z(}&#h>9Cj8lYWz@m8&P&YzX&Fna+lF{lfNpYn8s!*4h53s8o`H=2)v_rv$P2?onA-
z2Zows^lU>QA!YRQxAP}s?RJkaI-6j;fLsE&>B<&$)jKNdEG_LZ9JY4ekLVX$MCDCH
z4OScQT%&uBY*3^I`z02p1DlC}_)xzBC)P){DGMx~*+Uryz9t{y={AtN@L8;8NaK$0
z+Uc+{b*8QL+)OJk&VD%i&Ipi$3~iM~rJ+QagXdPeIPiPJ)aqnt(kjb+XtbS{&Ic_H
zgRZG-m#ohpzXzIk!t$T}4>+;+?0q1F3EHKCvVX#`OQ9fd!=Rx0G=m4k5&Te0y175u?=aN3;B0Yvz2ZN&tF
z!qDwL5W@EPZ65J1kEn@(_fe9dMv}zgx}C82