howler-sentinel-plugin 0.2.0.dev137__py3-none-any.whl → 0.2.0.dev147__py3-none-any.whl

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: howler-sentinel-plugin
3
- Version: 0.2.0.dev137
3
+ Version: 0.2.0.dev147
4
4
  Summary: A howler plugin for integration with Microsoft's Sentinel API
5
5
  License: MIT
6
6
  Author: CCCS
@@ -0,0 +1,21 @@
1
+ sentinel/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ sentinel/actions/azure_emit_hash.py,sha256=ES9u3iIkm18qcwVT7-f3r8K5d-haCnjSTC7cyOKxH7A,4000
3
+ sentinel/actions/send_to_sentinel.py,sha256=wrpE-ML19CPFU7l5aVWsZpiNvdyQImelt0hFwhtmXPw,4588
4
+ sentinel/actions/update_defender_xdr_alert.py,sha256=_AS6fL0lWgrKIfd2fgVfWXyLias1uX89bZ0RgjNsB7c,6796
5
+ sentinel/actions/update_defender_xdr_incident.py,sha256=HjvK8yTDafYePY5LoMkYat_cFUHnFWK3tzpCfAO3JNE,6854
6
+ sentinel/config.py,sha256=RLt65CZhD0VxygWngU6RjDkrS24h1H9OMLieV4iTf9A,1788
7
+ sentinel/manifest.yml,sha256=Ps5qp5PjcY6MY9U8wQFt-18VDpEySI0lrj25wMlxod4,222
8
+ sentinel/mapping/sentinel_incident.py,sha256=GkOMbFLeHgKd00H8Bc42yQylr4fvO8yq8y2bM1x7c3s,9741
9
+ sentinel/mapping/xdr_alert.py,sha256=J-o76G6gJy2R_bZO7FBgG3Ks8QToIkr5Uy7HRHzmb6w,17994
10
+ sentinel/mapping/xdr_alert_evidence.py,sha256=iMn9Wd5NB7Wi9l0Fl0vmJhugX8L6hAO9jYA9AtLLX2o,31429
11
+ sentinel/odm/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
+ sentinel/odm/hit.py,sha256=XEStYFIfiEUzcNVYo2Z1s6U7j6B-IDQOhRZR6YRUJn0,867
13
+ sentinel/odm/models/sentinel.py,sha256=XT3XdT92uoCV5vmY9dT1jmcxRyuu9vp1gE8AwZdKBIc,337
14
+ sentinel/routes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
+ sentinel/routes/ingest.py,sha256=XlQqOaZT-6_cBUuhe_DAdD81brKIgG1B4d3ZH8JJ0NA,10788
16
+ sentinel/utils/tenant_utils.py,sha256=Xfxh90x87pl0rdf8aqkwYtEOje8Hw5U5KNXLFtVxXCw,1725
17
+ howler_sentinel_plugin-0.2.0.dev147.dist-info/LICENSE,sha256=Wg2luVnxEkP2NSn11nh1US6W_nFFbICBAVTG9iG3t5M,1091
18
+ howler_sentinel_plugin-0.2.0.dev147.dist-info/METADATA,sha256=8CSPIJuPOJqXWV2q99h22R4cQnmw2lHH-6ulsD2GPhA,749
19
+ howler_sentinel_plugin-0.2.0.dev147.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
20
+ howler_sentinel_plugin-0.2.0.dev147.dist-info/entry_points.txt,sha256=4IJyMY0V49s3Wp659ngN_7U8g66-czeKxI-_dNAFP5g,60
21
+ howler_sentinel_plugin-0.2.0.dev147.dist-info/RECORD,,
@@ -35,6 +35,8 @@ def execute(query: str, **kwargs) -> list[dict[str, Any]]:
35
35
  )
36
36
  return report
37
37
 
38
+ from sentinel.config import config
39
+
38
40
  for hit in hits:
39
41
  if hit.azure and hit.azure.tenant_id:
40
42
  tenant_id = hit.azure.tenant_id
@@ -52,7 +54,7 @@ def execute(query: str, **kwargs) -> list[dict[str, Any]]:
52
54
  continue
53
55
 
54
56
  try:
55
- token, credentials = get_token(tenant_id, "https://monitor.azure.com/.default")
57
+ token = get_token(tenant_id, "https://monitor.azure.com/.default")
56
58
  except HowlerRuntimeError as err:
57
59
  logger.exception("Error on token fetching")
58
60
  report.append(
@@ -65,9 +67,24 @@ def execute(query: str, **kwargs) -> list[dict[str, Any]]:
65
67
  )
66
68
  continue
67
69
 
70
+ ingestor = next((ingestor for ingestor in config.ingestors if ingestor.tenant_id == tenant_id), None)
71
+
72
+ if not ingestor:
73
+ report.append(
74
+ {
75
+ "query": f"howler.id:{hit.howler.id}",
76
+ "outcome": "error",
77
+ "title": "Invalid Tenant ID",
78
+ "message": (
79
+ f"The tenant ID ({tenant_id}) associated with this alert has not been correctly configured."
80
+ ),
81
+ }
82
+ )
83
+ continue
84
+
68
85
  uri = (
69
- f"https://{credentials['dce']}.ingest.monitor.azure.com/dataCollectionRules/{credentials['dcr']}/"
70
- + f"streams/{credentials['table']}?api-version=2021-11-01-preview"
86
+ f"https://{ingestor.dce}.ingest.monitor.azure.com/dataCollectionRules/{ingestor.dcr}/"
87
+ + f"streams/{ingestor.table}?api-version=2021-11-01-preview"
71
88
  )
72
89
 
73
90
  payload = [
@@ -89,7 +89,7 @@ def execute(query: str, **kwargs):
89
89
  continue
90
90
 
91
91
  try:
92
- token = get_token(tenant_id, "https://graph.microsoft.com/.default")[0]
92
+ token = get_token(tenant_id, "https://graph.microsoft.com/.default")
93
93
  except HowlerRuntimeError as err:
94
94
  logger.exception("Error on token fetching")
95
95
  report.append(
sentinel/config.py ADDED
@@ -0,0 +1,70 @@
1
+ # mypy: ignore-errors
2
+ import os
3
+ from pathlib import Path
4
+ from typing import Optional
5
+
6
+ from howler.plugins.config import BasePluginConfig
7
+ from pydantic import BaseModel, ImportString
8
+ from pydantic_settings import SettingsConfigDict
9
+
10
+ APP_NAME = os.environ.get("APP_NAME", "howler")
11
+ PLUGIN_NAME = "sentinel"
12
+
13
+ root_path = Path("/etc") / APP_NAME.replace("-dev", "").replace("-stg", "")
14
+
15
+ config_locations = [
16
+ Path(__file__).parent / "manifest.yml",
17
+ root_path / "conf" / f"{PLUGIN_NAME}.yml",
18
+ Path(os.environ.get("HWL_CONF_FOLDER", root_path)) / f"{PLUGIN_NAME}.yml",
19
+ ]
20
+
21
+
22
+ class ClientCredentials(BaseModel):
23
+ "OAuth2 credentials for client_credential OAuth2 Flow"
24
+
25
+ client_id: str
26
+ client_secret: str
27
+
28
+
29
+ class Auth(BaseModel):
30
+ "Configuration for the various authentication methods, both to azure and incoming requests."
31
+
32
+ link_key: str = "abcdefghijklmnopqrstuvwxyz1234567890"
33
+
34
+ client_credentials: Optional[ClientCredentials] = None
35
+
36
+ custom_auth: Optional[ImportString] = None
37
+
38
+
39
+ class Ingestor(BaseModel):
40
+ "Defines necessary data to ingest howler alerts into a specific azure tenancy"
41
+
42
+ tenant_id: str
43
+ dce: str
44
+ dcr: str
45
+ table: str
46
+
47
+
48
+ class SentinelConfig(BasePluginConfig):
49
+ "Sentinel Plugin Configuration Model"
50
+
51
+ auth: Auth = Auth()
52
+
53
+ ingestors: list[Ingestor] = []
54
+
55
+ model_config = SettingsConfigDict(
56
+ yaml_file=config_locations,
57
+ yaml_file_encoding="utf-8",
58
+ strict=True,
59
+ env_nested_delimiter="__",
60
+ env_prefix=f"{PLUGIN_NAME.upper()}_",
61
+ )
62
+
63
+
64
+ config = SentinelConfig()
65
+
66
+ if __name__ == "__main__":
67
+ # When executed, the config model will print the default values of the configuration
68
+ import yaml
69
+
70
+ print(yaml.safe_dump(SentinelConfig().model_dump(mode="json"))) # noqa: T201
sentinel/manifest.yml ADDED
@@ -0,0 +1,13 @@
1
+ name: sentinel
2
+ modules:
3
+ odm:
4
+ modify_odm:
5
+ hit: true
6
+ generation:
7
+ hit: true
8
+ operations:
9
+ - azure_emit_hash
10
+ - send_to_sentinel
11
+ - update_defender_xdr_alert
12
+ routes:
13
+ - ingest:sentinel_api
@@ -100,7 +100,7 @@ class SentinelIncident:
100
100
  # Add assessment conditionally if classification is not null
101
101
  if classification is not None:
102
102
  bundle["howler"]["assessment"] = self.map_classification(classification)
103
- logger.info("Successfully mapped Sentiel Incident %s", incident_id)
103
+ logger.info("Successfully mapped Sentinel Incident %s", incident_id)
104
104
  return bundle
105
105
 
106
106
  except Exception as exc:
File without changes
sentinel/odm/hit.py CHANGED
@@ -1,24 +1,33 @@
1
+ from typing import TYPE_CHECKING
2
+
1
3
  import howler.odm as odm
2
4
  from howler.common.logging import get_logger
3
- from howler.odm.models.hit import Hit
5
+ from howler.odm.models.azure import Azure
4
6
 
5
7
  from sentinel.odm.models.sentinel import Sentinel
6
8
 
9
+ if TYPE_CHECKING:
10
+ from howler.odm.models.hit import Hit
11
+
12
+
7
13
  logger = get_logger(__file__)
8
14
 
9
15
 
10
16
  def modify_odm(target):
11
17
  "Add additional internal fields to the ODM"
12
- logger.info("Modifying ODM with additional fields")
13
-
14
18
  target.add_namespace(
15
19
  "sentinel",
16
20
  odm.Optional(odm.Compound(Sentinel, description="Sentinel metadata associated with this alert")),
17
21
  )
18
22
 
19
23
 
20
- def generate_useful_hit(hit: Hit) -> Hit:
24
+ def generate(hit: "Hit") -> "Hit": # pragma: no cover
21
25
  "Add cccs-specific changes to hits on generation"
22
26
  hit.sentinel = Sentinel({"id": "example-sentinel-id"})
23
27
 
28
+ if not hit.azure:
29
+ hit.azure = Azure({"tenant_id": "example-tenant-id"})
30
+ else:
31
+ hit.azure.tenant_id = "example-tenant-id"
32
+
24
33
  return ["sentinel"], hit
@@ -1,5 +0,0 @@
1
- from flask.blueprints import Blueprint
2
-
3
- from sentinel.routes.ingest import sentinel_api
4
-
5
- ROUTES: list[Blueprint] = [sentinel_api]
sentinel/routes/ingest.py CHANGED
@@ -1,4 +1,3 @@
1
- import os
2
1
  import re
3
2
  from typing import Any
4
3
 
@@ -10,8 +9,8 @@ from howler.common.logging import get_logger
10
9
  from howler.common.swagger import generate_swagger_docs
11
10
  from howler.services import action_service, analytic_service, hit_service
12
11
 
13
- from ..mapping.sentinel_incident import SentinelIncident
14
- from ..mapping.xdr_alert import XDRAlert
12
+ from sentinel.mapping.sentinel_incident import SentinelIncident
13
+ from sentinel.mapping.xdr_alert import XDRAlert
15
14
 
16
15
  SUB_API = "sentinel"
17
16
  sentinel_api = make_subapi_blueprint(SUB_API, api_version=1)
@@ -19,12 +18,6 @@ sentinel_api._doc = "Ingest Microsoft Sentinel XDR incidents into Howler"
19
18
 
20
19
  logger = get_logger(__file__)
21
20
 
22
- # For testing purposes, replace with actual secret in production
23
- SECRET = os.environ.get("SENTINEL_LINK_KEY", "abcdefghijklmnopqrstuvwxyz1234567890")
24
-
25
- if SECRET.startswith("abcdef"):
26
- logger.warning("Default secret used!")
27
-
28
21
 
29
22
  @generate_swagger_docs()
30
23
  @sentinel_api.route("/ingest", methods=["POST"])
@@ -77,8 +70,14 @@ def ingest_xdr_incident(**kwargs) -> tuple[dict[str, Any], int]: # noqa C901
77
70
  Receives a Microsoft Sentinel XDR incident as JSON, maps it to Howler format, and creates or updates a bundle
78
71
  and its underlying alerts in Howler. Returns details about the created or updated bundle and alerts.
79
72
  """
73
+ from sentinel.config import config
74
+
75
+ # API Key authentication
80
76
  apikey = request.headers.get("Authorization", "Basic ", type=str).split(" ")[1]
81
- if not apikey or apikey != SECRET:
77
+
78
+ link_key = config.auth.link_key
79
+
80
+ if not apikey or apikey != link_key:
82
81
  return unauthorized(err="API Key does not match expected value.")
83
82
 
84
83
  logger.info("Received authorization header with value %s", re.sub(r"^(.{3}).+(.{3})$", r"\1...\2", apikey))
@@ -1,5 +1,5 @@
1
- import json
2
- import os
1
+ import sys
2
+ from typing import Optional
3
3
 
4
4
  import requests
5
5
  from howler.common.exceptions import HowlerRuntimeError
@@ -9,32 +9,48 @@ from howler.config import cache
9
9
  logger = get_logger(__file__)
10
10
 
11
11
 
12
- @cache.memoize(15 * 60)
13
- def get_token(tenant_id: str, scope: str) -> tuple[str, dict[str, str]]:
12
+ def skip_cache(*args):
13
+ "Function to skip cache in testing mode"
14
+ return "pytest" in sys.modules
15
+
16
+
17
+ @cache.memoize(15 * 60, unless=skip_cache)
18
+ def get_token(tenant_id: str, scope: str) -> Optional[str]:
14
19
  """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
- logger.info("Generating client credential token for client id %s with scope %s", credentials["client_id"], scope)
22
-
23
- token_request_url = f"https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token"
24
- response = requests.post(
25
- token_request_url,
26
- data={
27
- "grant_type": "client_credentials",
28
- "client_id": credentials["client_id"],
29
- "client_secret": credentials["client_secret"],
30
- "scope": scope,
31
- },
32
- timeout=5.0,
33
- )
34
-
35
- if not response.ok:
36
- raise HowlerRuntimeError(f"Authentication to Azure Monitor API failed with status code {response.status_code}.")
37
-
38
- token = response.json()["access_token"]
39
-
40
- return token, credentials
20
+ from sentinel.config import config
21
+
22
+ token = None
23
+
24
+ if config.auth.client_credentials:
25
+ logger.info(
26
+ "Using client_credentials flow for client id %s with scope %s",
27
+ config.auth.client_credentials.client_id,
28
+ scope,
29
+ )
30
+
31
+ token_request_url = f"https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token"
32
+ response = requests.post(
33
+ token_request_url,
34
+ data={
35
+ "grant_type": "client_credentials",
36
+ "client_id": config.auth.client_credentials.client_id,
37
+ "client_secret": config.auth.client_credentials.client_secret,
38
+ "scope": scope,
39
+ },
40
+ timeout=5.0,
41
+ )
42
+
43
+ if not response.ok:
44
+ raise HowlerRuntimeError(
45
+ "Authentication to Azure Monitor API using client_credentials flow failed with status code"
46
+ f" {response.status_code}. Response:\n{response.text}"
47
+ )
48
+
49
+ token = response.json()["access_token"]
50
+ elif config.auth.custom_auth:
51
+ token = config.auth.custom_auth(tenant_id, scope)
52
+
53
+ if not token:
54
+ logger.warning("No access token received")
55
+
56
+ return token
@@ -1,18 +0,0 @@
1
- sentinel/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- sentinel/actions/azure_emit_hash.py,sha256=ES9u3iIkm18qcwVT7-f3r8K5d-haCnjSTC7cyOKxH7A,4000
3
- sentinel/actions/send_to_sentinel.py,sha256=RRmUSiPDKr7oQjA4f-iSGjEtziUQH77O2s-578pb1Uc,4022
4
- sentinel/actions/update_defender_xdr_alert.py,sha256=bHBHEZwAAT1pHTF2L4JK5gttzguq12NdV58fx0tj4dQ,6799
5
- sentinel/actions/update_defender_xdr_incident.py,sha256=HjvK8yTDafYePY5LoMkYat_cFUHnFWK3tzpCfAO3JNE,6854
6
- sentinel/mapping/sentinel_incident.py,sha256=UtC430gqxV8r_7L56TCXdvwQKgjgXaNzmWmKmgVV3mI,9740
7
- sentinel/mapping/xdr_alert.py,sha256=J-o76G6gJy2R_bZO7FBgG3Ks8QToIkr5Uy7HRHzmb6w,17994
8
- sentinel/mapping/xdr_alert_evidence.py,sha256=iMn9Wd5NB7Wi9l0Fl0vmJhugX8L6hAO9jYA9AtLLX2o,31429
9
- sentinel/odm/hit.py,sha256=hAuO2ONMK3Ml8Xu6E7tHrmZ7M6HG5tT38RD9ZxwY254,666
10
- sentinel/odm/models/sentinel.py,sha256=XT3XdT92uoCV5vmY9dT1jmcxRyuu9vp1gE8AwZdKBIc,337
11
- sentinel/routes/__init__.py,sha256=JYmKRwIfEsiPos1XuMQ2mlGDbxk6TN_cVEM0K_RNze4,130
12
- sentinel/routes/ingest.py,sha256=lVBr6I5p5WUAUCkgHe4UBUwdjsJCGb_FjwkeXQLXcaI,10902
13
- sentinel/utils/tenant_utils.py,sha256=nGOCbLzUx9OyATLAZ5UbW0WNao_1ioW4wL-htn2ltKU,1324
14
- howler_sentinel_plugin-0.2.0.dev137.dist-info/LICENSE,sha256=Wg2luVnxEkP2NSn11nh1US6W_nFFbICBAVTG9iG3t5M,1091
15
- howler_sentinel_plugin-0.2.0.dev137.dist-info/METADATA,sha256=1lrkeLVxG_TuAedKYDmEKGj9n3OS5ZXPz0SSOYvUo2c,749
16
- howler_sentinel_plugin-0.2.0.dev137.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
17
- howler_sentinel_plugin-0.2.0.dev137.dist-info/entry_points.txt,sha256=4IJyMY0V49s3Wp659ngN_7U8g66-czeKxI-_dNAFP5g,60
18
- howler_sentinel_plugin-0.2.0.dev137.dist-info/RECORD,,