Skip to content

Commit

Permalink
Merge pull request #4 from valkiriaaquatica/event-source-plugin
Browse files Browse the repository at this point in the history
add event source for tenable
  • Loading branch information
valkiriaaquatica authored Aug 24, 2024
2 parents dd5c54d + 4cbcfb2 commit 46caad4
Show file tree
Hide file tree
Showing 3 changed files with 236 additions and 0 deletions.
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,30 @@ compose:
hash_id: id | md5
```
### Event Driven - Ansible Rulebooks
Fetch data from an event source like Tenable Public API can be made using the plugin eventstenable
that can be found in /plugins/event_source/eventstenable.py
An easy example is:
```yaml
---
- name: Retrieve critical vullnerabilities from Tenable API every 30 minutes
hosts: localhost
sources:
- valkiriaaquatica.tenable.eventstenable:
endpoint: "workbenches/vulnerabilities?filter.0.filter=severity&filter.0.quality=eq&filter.0.value=Critical"
data_key: "vulnerabilities"
interval: 30

rules:
- name: Run ansible hello default EDA playbook if critical vulnerbaility comes from 12345 plugin
condition: event.tenable.plugin_id == "12345"
action:
run_playbook:
name: ansible.eda.hello
```
## Contributing
There are many ways in which you can participate in the project, for example:
Expand Down
15 changes: 15 additions & 0 deletions extensions/eda/rulebooks/event_tenable.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
- name: Retrieve critical vullnerabilities from Tenable API every 30 minutes
hosts: localhost
sources:
- valkiriaaquatica.tenable.eventstenable:
endpoint: "workbenches/vulnerabilities?filter.0.filter=severity&filter.0.quality=eq&filter.0.value=Critical"
data_key: "vulnerabilities"
interval: 30

rules:
- name: Run ansible hello default EDA playbook if critical vulnerbaility comes from 12345 plugin
condition: event.tenable.plugin_id == "12345"
action:
run_playbook:
name: ansible.eda.hello
197 changes: 197 additions & 0 deletions plugins/event_source/eventstenable.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
"""
eventstenable.py
An ansible-rulebook event source scrapper plugin module for fetching data from public Tenable API using its API.
Arguments:
---------
endpoint: The API endpoint to query, relative to the base Tenable API URL.
This should be a valid endpoint within the Tenable API, e.g.,
"workbenches/vulnerabilities". Required.
data_key: The key in the JSON response that contains the list of items to be
processed. Default is "data", place "" to none key to filter.
Easy to look ip up, just go to the Tenable
API official docs and for example for: https://developer.tenable.com/reference/io-filters-assets-list
data_key is filters
tenable_access_key: The access key for authenticating with the Tenable API.
Can be provided directly or set as an environment
variable TENABLE_ACCESS_KEY. Required.
tenable_secret_key: The secret key for authenticating with the Tenable API.
Can be provided directly or set as an environment
variable TENABLE_SECRET_KEY. Required.
interval: The interval in minutes at which the API should be queried.
Default is 5 minutes.
Example:
-------
# gets all vulnerabilities querying every 10 minutes
- valkiriaaquatica.tenable.eventstenable:
endpoint: "workbenches/vulnerabilities"
data_key: "vulnerabilities"
tenable_access_key: "{{ TENABLE_ACCESS_KEY }}"
tenable_secret_key: "{{ TENABLE_SECRET_KEY }}"
interval: 10
# gets all critical vulnerabilites every 5 minutes
- valkiriaaquatica.tenable.eventstenable:
endpoint: "workbenches/vulnerabilities?filter.0.filter=severity&filter.0.quality=eq&filter.0.value=Critical"
data_key: "vulnerabilities"
tenable_access_key: "{{ TENABLE_ACCESS_KEY }}"
tenable_secret_key: "{{ TENABLE_SECRET_KEY }}"
interval: 0.5
# gets all vulnerabilites from an asset every 30 seconds using enviroment vars
- valkiriaaquatica.tenable.eventstenable:
endpoint: "workbenches/assets/0004544c-0a6d-45ee-9e5f-c1fbc436092b/vulnerabilities"
data_key: "vulnerabilities"
interval: 0.5
# gets plugins in nested json every day using enviroment vars
- valkiriaaquatica.tenable.eventstenable:
endpoint: "plugins/plugin"
data_key: "data.plugin_details"
interval: 1440
# gets all agent groups querying every 30 minuted using enviroment vars
- valkiriaaquatica.tenable.eventstenable:
endpoint: "scanners/null/agent-groups"
data_key: "groups"
interval: 30
"""

import asyncio
import json
import os
from typing import Any
from typing import Dict
from urllib.error import HTTPError
from urllib.error import URLError

from ansible.module_utils.urls import Request


class AuthenticationError(Exception):
pass


class BadRequestError(Exception):
pass


class TenableAPIError(Exception):
pass


class UnexpectedAPIResponse(Exception):
pass


def get_tenable_credentials(access_key=None, secret_key=None):
access_key = access_key or os.getenv("TENABLE_ACCESS_KEY")
secret_key = secret_key or os.getenv("TENABLE_SECRET_KEY")
if not access_key or not secret_key:
raise ValueError("Access key and secret key are required for Tenable API.")

return access_key, secret_key


class TenableAPI:
def __init__(self, access_key=None, secret_key=None):
self.access_key, self.secret_key = get_tenable_credentials(access_key, secret_key)
self.base_url = "https://cloud.tenable.com"
self.headers = {
"Accept": "application/json",
"X-ApiKeys": f"accessKey={self.access_key};secretKey={self.secret_key}",
}
self.client = Request(validate_certs=True, timeout=30)

def request(self, method: str, endpoint: str, data: dict = None) -> dict:
url = f"{self.base_url}/{endpoint}"

if data:
data = json.dumps(data).encode("utf-8")
if method in ["PATCH", "POST", "PUT"]:
self.headers["Content-Type"] = "application/json"
else:
self.headers.pop("Content-Type", None)

try:
response = self.client.open(method=method, url=url, headers=self.headers, data=data)
response_body = response.read()
if response_body:
return json.loads(response_body.decode("utf-8"))
return {}
except HTTPError as e:
error_data = e.read().decode("utf-8")
if e.code == 400:
raise BadRequestError(error_data, e.code)
elif e.code == 401:
raise AuthenticationError("Authentication failure. Please check your API keys.", e.code)
else:
raise UnexpectedAPIResponse(e.code, error_data)
except URLError as e:
raise TenableAPIError(f"URL error occurred: {str(e)}", None)
except TimeoutError as e:
raise TenableAPIError(f"Request timed out: {str(e)}", None)


def get_nested_value(d, keys):
"""Recursively get a value from a nested dictionary just when nested is in json response"""
for key in keys:
if isinstance(d, dict) and key in d:
d = d[key]
else:
return None
return d


async def main(queue: asyncio.Queue, args: Dict[str, Any]):
valid_keys = {"endpoint", "data_key", "tenable_access_key", "tenable_secret_key", "interval"}

for key in args:
if key not in valid_keys:
raise ValueError(f"Invalid argument '{key}' provided.")

endpoint = args.get("endpoint")
data_key = args.get("data_key", "data")
access_key = args.get("tenable_access_key")
secret_key = args.get("tenable_secret_key")
interval_minutes = args.get("interval", 5)
interval_seconds = interval_minutes * 60

if not endpoint:
raise ValueError("Endpoint must be provided, It cannot be empty.")

tenable_api = TenableAPI(access_key=access_key, secret_key=secret_key)

while True:
try:
response = tenable_api.request(method="GET", endpoint=endpoint)
keys = data_key.split(".")
data_to_process = get_nested_value(response, keys)

if data_to_process:
for item in data_to_process:
await queue.put({"tenable": item})
else:
await queue.put({"tenable": response})

await asyncio.sleep(interval_seconds)

except asyncio.CancelledError:
break
except Exception as e:
print(f"Error in Tenable plugin: {e}")


if __name__ == "__main__":
"""MockQueue if running directly."""

class MockQueue(asyncio.Queue[Any]):
"""A fake queue."""

async def put(self: "MockQueue", event: dict) -> None:
"""Print the event."""
print(event) # noqa: T201

asyncio.run(main(MockQueue(), {}))

0 comments on commit 46caad4

Please sign in to comment.