diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dd8b9314e..bba12a0a8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -77,6 +77,25 @@ jobs: timeout-minutes: 8 run: poetry run make ${{ matrix.sanity }} + # Tox is used to execute linters required for Event-Driven Ansible (EDA) code: + # github.com/ansible/eda-partner-testing/blob/main/README.md + # Tox should only execute over /extensions/eda/plugins. + # Tox utilises the tox.ini file found in the local directory. + # This action is taken from Ansible Partner Engineering's example: + # github.com/ansible/eda-partner-testing/blob/main/.github/workflows/tox.yml + # Tox is planned by Ansible Partner Engineering to cover other code in future. + tox: + name: Tox Checks + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Install deps + run: python -m pip install tox + - name: Move to tox conf file and run tox + run: | + cd .github/workflows + python -m tox -- ../.. + format: name: Code Format Check runs-on: ubuntu-latest @@ -105,7 +124,7 @@ jobs: release: name: release if: github.event_name == 'push' && github.ref == 'refs/heads/main' - needs: [sanity] + needs: [sanity, tox] runs-on: ubuntu-latest steps: diff --git a/.github/workflows/tox.ini b/.github/workflows/tox.ini new file mode 100644 index 000000000..345c35ef8 --- /dev/null +++ b/.github/workflows/tox.ini @@ -0,0 +1,29 @@ +# Recommended usage of this file is detailed in https://github.com/ansible/eda-partner-testing/blob/main/README.md. +# The linter paths can be changed, but may result in false passes. +# {posargs} in this case would be the path to collection root relative from the .github/workflows dir (`../..`) + +[tox] +envlist = ruff, darglint, pylint-event-source, pylint-event-filter +requires = + ruff + darglint + pylint + +[testenv:ruff] +deps = ruff +commands = ruff check --select ALL --ignore INP001 -q {posargs}/extensions/eda/plugins + + +[testenv:darglint] +deps = darglint +commands = darglint -s numpy -z full {posargs}/extensions/eda/plugins + + +# If you dont have any event_source or event_filter plugins, remove the corresponding testenv +[testenv:pylint-event-source] +deps = pylint +commands = pylint {posargs}/extensions/eda/plugins/event_source/*.py --output-format=parseable -sn --disable R0801 --disable E0401 + +; [testenv:pylint-event-filter] +; deps = pylint +; commands = pylint {posargs}/extensions/eda/plugins/event_filter/*.py --output-format=parseable -sn --disable R0801 diff --git a/extensions/eda/plugins/event_source/logs.py b/extensions/eda/plugins/event_source/logs.py index 139e52bf3..78ebfd33e 100644 --- a/extensions/eda/plugins/event_source/logs.py +++ b/extensions/eda/plugins/event_source/logs.py @@ -1,3 +1,23 @@ +"""An ansible-rulebook event source module. + +An ansible-rulebook event source module for receiving events via a webhook from +PAN-OS firewall or Panorama appliance. + +Arguments: +--------- + host: The webserver hostname to listen to. Set to 0.0.0.0 to listen on all + interfaces. Defaults to 127.0.0.1 + port: The TCP port to listen to. Defaults to 5000 + +Example: +------- + - paloaltonetworks.panos.logs: + host: 0.0.0.0 + port: 5000 + type: decryption + +""" + # Copyright 2023 Palo Alto Networks, Inc # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -12,37 +32,20 @@ # See the License for the specific language governing permissions and # limitations under the License. +# ruff: noqa: UP001, UP010 from __future__ import absolute_import, division, print_function +# pylint: disable-next=invalid-name __metaclass__ = type import asyncio import logging -from typing import Any, Dict +from json import JSONDecodeError +from typing import Any from aiohttp import web from dpath import util - -DOCUMENTATION = """ -logs.py - -An ansible-rulebook event source module for receiving events via a webhook from -PAN-OS firewall or Panorama appliance. - -Arguments: - host: The webserver hostname to listen to. Set to 0.0.0.0 to listen on all - interfaces. Defaults to 127.0.0.1 - port: The TCP port to listen to. Defaults to 5000 - -Example: - - - paloaltonetworks.panos.logs: - host: 0.0.0.0 - port: 5000 - type: decryption -""" - logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) @@ -50,23 +53,36 @@ @routes.get("/") -async def status(request: web.Request): - """Return a simple status response.""" +async def status() -> web.Response: + """Return a simple status response. + + Returns + ------- + A web.Response object with status 200 and the text "up" returned by the function. + """ return web.Response(status=200, text="up") @routes.post("/{endpoint}") -async def webhook(request: web.Request): - """ - Handle webhook requests. +async def webhook(request: web.Request) -> web.Response: + """Handle webhook requests. Process the incoming JSON payload and forward it to the event queue if it matches the configured log type. + + Parameters + ---------- + request + The incoming webhook request. + + Returns + ------- + A web.Response object with status 200 and the status. """ try: payload = await request.json() - except Exception as e: - logger.error(f"Failed to parse JSON payload: {e}") + except JSONDecodeError: + logger.exception("Failed to parse JSON payload") return web.Response(status=400, text="Invalid JSON payload") if request.app["type"] == "decryption": @@ -78,18 +94,31 @@ async def webhook(request: web.Request): await request.app["queue"].put(data) return web.Response( - status=202, text=str({"status": "received", "payload": "happy"}) + status=202, + text=str({"status": "received", "payload": "happy"}), ) -def process_payload(request, payload, log_type): - """ - Process the payload and extract the necessary information. +def process_payload( + request: web.Request, + payload: dict[str, Any], + log_type: str, +) -> dict[str, Any]: + """Process the payload and extract the necessary information. + + Parameters + ---------- + request + The incoming webhook request. + payload + The JSON payload from the request. + log_type : str + The log type to filter events. + + Returns + ------- + A dictionary containing the processed payload and metadata. - :param request: The incoming webhook request. - :param payload: The JSON payload from the request. - :param log_type: The log type to filter events. - :return: A dictionary containing the processed payload and metadata. """ try: device_name = util.get(payload, "details.device_name", separator=".") @@ -114,16 +143,21 @@ def process_payload(request, payload, log_type): return data -async def main(queue: asyncio.Queue, args: Dict[str, Any], logger=None): - """ - Main function to run the plugin as a standalone application. +async def main(queue: asyncio.Queue, args: dict[str, Any], custom_logger: None) -> None: + """Run the plugin as a standalone application. + + Parameters + ---------- + queue + The event queue to forward incoming events to. + args + A dictionary containing configuration arguments. + custom_logger + An optional custom logger. - :param queue: The event queue to forward incoming events to. - :param args: A dictionary containing configuration arguments. - :param logger: An optional custom logger. """ - if logger is None: - logger = logging.getLogger(__name__) + if custom_logger is None: + custom_logger = logging.getLogger(__name__) app = web.Application() app["queue"] = queue @@ -143,7 +177,7 @@ async def main(queue: asyncio.Queue, args: Dict[str, Any], logger=None): try: await asyncio.Future() except asyncio.CancelledError: - logger.info("Plugin Task Cancelled") + custom_logger.info("Plugin Task Cancelled") finally: await runner.cleanup() @@ -151,7 +185,22 @@ async def main(queue: asyncio.Queue, args: Dict[str, Any], logger=None): if __name__ == "__main__": class MockQueue: - async def put(self, event): - print(event) + """A mock queue for handling events asynchronously.""" + + async def put(self: "MockQueue", event: str) -> None: + """Put an event into the queue. + + Parameters + ---------- + event: str + The event to be added to the queue. + + """ + the_logger.info(event) + + async def get(self: "MockQueue") -> None: + """Get an event from the queue.""" + the_logger.info("Getting event from the queue.") - asyncio.run(main(MockQueue(), {})) + the_logger = logging.getLogger() + asyncio.run(main(MockQueue(), {}, the_logger)) diff --git a/tests/sanity/ignore-2.13.txt b/tests/sanity/ignore-2.13.txt index 09a2032e7..d9a75f079 100644 --- a/tests/sanity/ignore-2.13.txt +++ b/tests/sanity/ignore-2.13.txt @@ -536,4 +536,4 @@ plugins/modules/panos_zone_facts.py import-3.5 plugins/httpapi/panos.py import-3.5 plugins/modules/panos_dag.py no-get-exception plugins/modules/panos_dag_tags.py no-get-exception -plugins/modules/panos_sag.py no-get-exception \ No newline at end of file +plugins/modules/panos_sag.py no-get-exception