howler-sentinel-plugin 0.2.0.dev87__tar.gz → 0.2.0.dev92__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {howler_sentinel_plugin-0.2.0.dev87 → howler_sentinel_plugin-0.2.0.dev92}/PKG-INFO +1 -1
- {howler_sentinel_plugin-0.2.0.dev87 → howler_sentinel_plugin-0.2.0.dev92}/pyproject.toml +1 -1
- howler_sentinel_plugin-0.2.0.dev92/sentinel/actions/send_to_sentinel.py +103 -0
- howler_sentinel_plugin-0.2.0.dev92/sentinel/utils/tenant_utils.py +38 -0
- {howler_sentinel_plugin-0.2.0.dev87 → howler_sentinel_plugin-0.2.0.dev92}/LICENSE +0 -0
- {howler_sentinel_plugin-0.2.0.dev87 → howler_sentinel_plugin-0.2.0.dev92}/README.md +0 -0
- {howler_sentinel_plugin-0.2.0.dev87 → howler_sentinel_plugin-0.2.0.dev92}/sentinel/__init__.py +0 -0
- {howler_sentinel_plugin-0.2.0.dev87 → howler_sentinel_plugin-0.2.0.dev92}/sentinel/actions/ingestion.py +0 -0
- {howler_sentinel_plugin-0.2.0.dev87 → howler_sentinel_plugin-0.2.0.dev92}/sentinel/actions/synchronization.py +0 -0
- {howler_sentinel_plugin-0.2.0.dev87 → howler_sentinel_plugin-0.2.0.dev92}/sentinel/mapping/sentinel_incident.py +0 -0
- {howler_sentinel_plugin-0.2.0.dev87 → howler_sentinel_plugin-0.2.0.dev92}/sentinel/mapping/xdr_alert.py +0 -0
- {howler_sentinel_plugin-0.2.0.dev87 → howler_sentinel_plugin-0.2.0.dev92}/sentinel/mapping/xdr_alert_evidence.py +0 -0
- {howler_sentinel_plugin-0.2.0.dev87 → howler_sentinel_plugin-0.2.0.dev92}/sentinel/odm/hit.py +0 -0
- {howler_sentinel_plugin-0.2.0.dev87 → howler_sentinel_plugin-0.2.0.dev92}/sentinel/odm/models/sentinel.py +0 -0
- {howler_sentinel_plugin-0.2.0.dev87 → howler_sentinel_plugin-0.2.0.dev92}/sentinel/routes/__init__.py +0 -0
- {howler_sentinel_plugin-0.2.0.dev87 → howler_sentinel_plugin-0.2.0.dev92}/sentinel/routes/ingest.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "howler-sentinel-plugin"
|
|
3
|
-
version = "0.2.0.
|
|
3
|
+
version = "0.2.0.dev92"
|
|
4
4
|
description = "A howler plugin for integration with Microsoft's Sentinel API"
|
|
5
5
|
authors = [{ name = "CCCS", email = "analysis-development@cyber.gc.ca" }]
|
|
6
6
|
license = { text = "MIT" }
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
from howler.common.exceptions import HowlerRuntimeError
|
|
3
|
+
from howler.common.loader import datastore
|
|
4
|
+
from howler.common.logging import get_logger
|
|
5
|
+
from howler.odm.models.action import VALID_TRIGGERS
|
|
6
|
+
from howler.odm.models.hit import Hit
|
|
7
|
+
|
|
8
|
+
from sentinel.utils.tenant_utils import get_token
|
|
9
|
+
|
|
10
|
+
logger = get_logger(__file__)
|
|
11
|
+
|
|
12
|
+
OPERATION_ID = "send_to_sentinel"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def execute(query: str, **kwargs):
|
|
16
|
+
"""Send hit to Microsoft Sentinel.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
query (str): The query on which to apply this automation.
|
|
20
|
+
"""
|
|
21
|
+
report = []
|
|
22
|
+
ds = datastore()
|
|
23
|
+
|
|
24
|
+
hits: list[Hit] = ds.hit.search(query, as_obj=True)["items"]
|
|
25
|
+
if not hits:
|
|
26
|
+
report.append(
|
|
27
|
+
{
|
|
28
|
+
"query": query,
|
|
29
|
+
"outcome": "error",
|
|
30
|
+
"title": "No hits returned by query",
|
|
31
|
+
"message": f"No hits returned by '{query}'",
|
|
32
|
+
}
|
|
33
|
+
)
|
|
34
|
+
return report
|
|
35
|
+
|
|
36
|
+
for hit in hits:
|
|
37
|
+
try:
|
|
38
|
+
token, credentials = get_token(hit.azure.tenant_id)
|
|
39
|
+
except HowlerRuntimeError as err:
|
|
40
|
+
logger.exception("Error on token fetching")
|
|
41
|
+
report.append(
|
|
42
|
+
{
|
|
43
|
+
"query": f"howler.id:{hit.howler.id}",
|
|
44
|
+
"outcome": "error",
|
|
45
|
+
"title": "Invalid Credentials",
|
|
46
|
+
"message": err.message,
|
|
47
|
+
}
|
|
48
|
+
)
|
|
49
|
+
continue
|
|
50
|
+
|
|
51
|
+
uri = (
|
|
52
|
+
f"https://{credentials['dce']}.ingest.monitor.azure.com/dataCollectionRules/{credentials['dcr']}/"
|
|
53
|
+
+ f"streams/{credentials['table']}?api-version=2021-11-01-preview"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
payload = [
|
|
57
|
+
{
|
|
58
|
+
"TimeGenerated": hit.event.ingested.isoformat(),
|
|
59
|
+
"Title": hit.howler.analytic,
|
|
60
|
+
"RawData": {"Hit": hit.as_primitives(), "From": "Howler"},
|
|
61
|
+
}
|
|
62
|
+
]
|
|
63
|
+
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
|
|
64
|
+
|
|
65
|
+
response = requests.post(uri, headers=headers, json=payload, timeout=5.0)
|
|
66
|
+
if not response.ok:
|
|
67
|
+
report.append(
|
|
68
|
+
{
|
|
69
|
+
"query": f"howler.id:{hit.howler.id}",
|
|
70
|
+
"outcome": "error",
|
|
71
|
+
"title": "Azure Monitor API request failed",
|
|
72
|
+
"message": f"POST request to Azure Monitor failed with status code {response.status_code}.",
|
|
73
|
+
}
|
|
74
|
+
)
|
|
75
|
+
continue
|
|
76
|
+
|
|
77
|
+
report.append(
|
|
78
|
+
{
|
|
79
|
+
"query": f"howler.id:{hit.howler.id}",
|
|
80
|
+
"outcome": "success",
|
|
81
|
+
"title": "Alert updated in Sentinel",
|
|
82
|
+
"message": "Howler has successfuly propagated changes to this alert to Sentinel.",
|
|
83
|
+
}
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
return report
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def specification():
|
|
90
|
+
"Send to Sentinel action specification"
|
|
91
|
+
return {
|
|
92
|
+
"id": OPERATION_ID,
|
|
93
|
+
"title": "Send hit to Microsoft Sentinel",
|
|
94
|
+
"priority": 8,
|
|
95
|
+
"i18nKey": "Send hit to Microsoft Sentinel",
|
|
96
|
+
"description": {
|
|
97
|
+
"short": "Send hit to Microsoft Sentinel",
|
|
98
|
+
"long": execute.__doc__,
|
|
99
|
+
},
|
|
100
|
+
"roles": ["automation_basic"],
|
|
101
|
+
"steps": [{"args": {}, "options": {}, "validation": {}}],
|
|
102
|
+
"triggers": VALID_TRIGGERS,
|
|
103
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
import requests
|
|
5
|
+
from howler.common.exceptions import HowlerRuntimeError
|
|
6
|
+
from howler.common.logging import get_logger
|
|
7
|
+
from howler.config import cache
|
|
8
|
+
|
|
9
|
+
logger = get_logger(__file__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@cache.memoize(15 * 60)
|
|
13
|
+
def get_token(tenant_id: str) -> tuple[str, dict[str, str]]:
|
|
14
|
+
"""Get a borealis token based on the current howler token"""
|
|
15
|
+
# Get bearer token
|
|
16
|
+
try:
|
|
17
|
+
credentials = json.loads(os.environ["HOWLER_SENTINEL_INGEST_CREDENTIALS"])
|
|
18
|
+
except (KeyError, json.JSONDecodeError):
|
|
19
|
+
raise HowlerRuntimeError("Credential data not configured.")
|
|
20
|
+
|
|
21
|
+
token_request_url = f"https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token"
|
|
22
|
+
response = requests.post(
|
|
23
|
+
token_request_url,
|
|
24
|
+
data={
|
|
25
|
+
"grant_type": "client_credentials",
|
|
26
|
+
"client_id": credentials["client_id"],
|
|
27
|
+
"client_secret": credentials["client_secret"],
|
|
28
|
+
"scope": "https://monitor.azure.com/.default",
|
|
29
|
+
},
|
|
30
|
+
timeout=5.0,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
if not response.ok:
|
|
34
|
+
raise HowlerRuntimeError(f"Authentication to Azure Monitor API failed with status code {response.status_code}.")
|
|
35
|
+
|
|
36
|
+
token = response.json()["access_token"]
|
|
37
|
+
|
|
38
|
+
return token, credentials
|
|
File without changes
|
|
File without changes
|
{howler_sentinel_plugin-0.2.0.dev87 → howler_sentinel_plugin-0.2.0.dev92}/sentinel/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{howler_sentinel_plugin-0.2.0.dev87 → howler_sentinel_plugin-0.2.0.dev92}/sentinel/odm/hit.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{howler_sentinel_plugin-0.2.0.dev87 → howler_sentinel_plugin-0.2.0.dev92}/sentinel/routes/ingest.py
RENAMED
|
File without changes
|