Skip to content

Commit

Permalink
feat(event_driven_ansible): New plugin for event-driven ansible
Browse files Browse the repository at this point in the history
Support for receiving logs sent from PAN-OS firewalls via HTTP server profiles
  • Loading branch information
cdot65 authored May 12, 2023
1 parent 5b27721 commit c4b627d
Show file tree
Hide file tree
Showing 4 changed files with 257 additions and 0 deletions.
80 changes: 80 additions & 0 deletions docs/source/examples.rst
Original file line number Diff line number Diff line change
Expand Up @@ -278,3 +278,83 @@ configuration from a configuration file
- name: commit (blocks until finished)
panos_commit:
provider: '{{ provider }}'
Event-Driven Ansible (EDA)
===========================================

Event-Driven Ansible is a responsive automation solution that can
process events containing discrete, actionable intelligence.
The `plugins/event_source/logs.py` plugin is capable of receiving
JSON structured messages from a PAN-OS firewall, restructure the
payload as a Python dictionary, determine the appropriate response
to the event and then execute automated actions to address or remediate.

There are four components needed to implement EDA:

- rulebook: A YAML file that defines the conditions and actions to be
taken when a condition is met.
- playbook: A YAML file that defines the Ansible tasks to be executed
when a condition is met.
- inventory: A YAML file that defines the PAN-OS firewall(s) to be
executed against.
- HTTP server profile: A PAN-OS firewall configuration that defines
how the PAN-OS firewall(s) should send events to the EDA server.

rulebook.yml
------------

.. code-block:: yaml
---
- name: "Receive logs sourced from HTTP Server Profile in PAN-OS"
hosts: "localhost"
## Define how our plugin should listen for logs from the PAN-OS firewall
sources:
- paloaltonetworks.panos.logs:
host: 0.0.0.0
port: 5000
type: decryption
## Define the conditions we are looking for
rules:
- name: "Troubleshoot Decryption Failure"
condition: event.meta.log_type == "decryption"
## Define the action we should take should the condition be met
run_playbook:
name: playbook.yml
HTTP Server Profile
-------------------

The following example shows what a Decryption HTTP server profile
would look like in PAN-OS. The HTTP server profile is configured to
send logs to the EDA server.

.. code-block:: json
{
"category": "network",
"details": {
"action": "$action",
"app": "$app",
"cn": "$cn",
"dst": "$dst",
"device_name": "$device_name",
"error": "$error",
"issuer_cn": "$issuer_cn",
"root_cn": "$root_cn",
"root_status": "$root_status",
"sni": "$sni",
"src": "$src",
"srcuser": "$srcuser"
},
"receive_time": "$receive_time",
"rule": "$rule",
"rule_uuid": "$rule_uuid",
"serial": "$serial",
"sessionid": "$sessionid",
"severity": "informational",
"type": "decryption"
}
157 changes: 157 additions & 0 deletions extensions/plugins/event_source/logs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
# Copyright 2023 Palo Alto Networks, Inc
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from __future__ import absolute_import, division, print_function

__metaclass__ = type

import asyncio
import logging
from typing import Any, Dict

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__)

routes = web.RouteTableDef()


@routes.get("/")
async def status(request: web.Request):
"""Return a simple status response."""
return web.Response(status=200, text="up")


@routes.post("/{endpoint}")
async def webhook(request: web.Request):
"""
Handle webhook requests.
Process the incoming JSON payload and forward it to the event queue
if it matches the configured log type.
"""
try:
payload = await request.json()
except Exception as e:
logger.error(f"Failed to parse JSON payload: {e}")
return web.Response(status=400, text="Invalid JSON payload")

if request.app["type"] == "decryption":
log_type = payload.get("type", "log_type")
if log_type != "decryption":
log_type = "log_type"

data = process_payload(request, payload, log_type)
await request.app["queue"].put(data)

return web.Response(
status=202, text=str({"status": "received", "payload": "happy"})
)


def process_payload(request, payload, log_type):
"""
Process the payload and extract the necessary information.
: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=".")
data = {
"payload": payload,
"meta": {
"device_name": device_name,
"endpoint": request.match_info["endpoint"],
"headers": dict(request.headers),
"log_type": log_type,
},
}
except KeyError:
logger.warning("KeyError occurred while processing the payload")
data = {
"payload": payload,
"meta": {
"message": "processing failed, check key names",
"headers": dict(request.headers),
},
}
return data


async def main(queue: asyncio.Queue, args: Dict[str, Any], logger=None):
"""
Main function to run the plugin as a standalone application.
: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__)

app = web.Application()
app["queue"] = queue
app["type"] = str(args.get("type", "decryption"))

app.add_routes(routes)

runner = web.AppRunner(app)
await runner.setup()
site = web.TCPSite(
runner,
args.get("host", "localhost"),
args.get("port", 5000),
)
await site.start()

try:
await asyncio.Future()
except asyncio.CancelledError:
logger.info("Plugin Task Cancelled")
finally:
await runner.cleanup()


if __name__ == "__main__":

class MockQueue:
async def put(self, event):
print(event)

asyncio.run(main(MockQueue(), {}))
18 changes: 18 additions & 0 deletions extensions/rulebooks/decryption.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
- name: "Receive logs sourced from HTTP Server Profile in PAN-OS"
hosts: "localhost"

## Define how our plugin should listen for logs from the PAN-OS firewall
sources:
- paloaltonetworks.panos.logs:
host: 0.0.0.0
port: 5000
type: decryption

## Define the conditions we are looking for
rules:
- name: "Troubleshoot Decryption Failure"
condition: event.meta.log_type == "decryption"

## Define the action we should take should the condition be met
action:
debug:
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ python = "^3.8"
pan-python = "^0.17.0"
pan-os-python = "^1.8.0"
xmltodict = "^0.12.0"
aiohttp = "^3.8.4"
dpath = "^2.1.5"

[tool.poetry.dev-dependencies]
black = "22.3.0"
Expand Down

0 comments on commit c4b627d

Please sign in to comment.