howler-sentinel-plugin 0.2.0.dev418__tar.gz → 0.2.0.dev436__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.
Files changed (20) hide show
  1. {howler_sentinel_plugin-0.2.0.dev418 → howler_sentinel_plugin-0.2.0.dev436}/PKG-INFO +1 -1
  2. {howler_sentinel_plugin-0.2.0.dev418 → howler_sentinel_plugin-0.2.0.dev436}/pyproject.toml +6 -6
  3. {howler_sentinel_plugin-0.2.0.dev418 → howler_sentinel_plugin-0.2.0.dev436}/sentinel/mapping/sentinel_incident.py +20 -12
  4. {howler_sentinel_plugin-0.2.0.dev418 → howler_sentinel_plugin-0.2.0.dev436}/LICENSE +0 -0
  5. {howler_sentinel_plugin-0.2.0.dev418 → howler_sentinel_plugin-0.2.0.dev436}/README.md +0 -0
  6. {howler_sentinel_plugin-0.2.0.dev418 → howler_sentinel_plugin-0.2.0.dev436}/sentinel/__init__.py +0 -0
  7. {howler_sentinel_plugin-0.2.0.dev418 → howler_sentinel_plugin-0.2.0.dev436}/sentinel/actions/azure_emit_hash.py +0 -0
  8. {howler_sentinel_plugin-0.2.0.dev418 → howler_sentinel_plugin-0.2.0.dev436}/sentinel/actions/send_to_sentinel.py +0 -0
  9. {howler_sentinel_plugin-0.2.0.dev418 → howler_sentinel_plugin-0.2.0.dev436}/sentinel/actions/update_defender_xdr_alert.py +0 -0
  10. {howler_sentinel_plugin-0.2.0.dev418 → howler_sentinel_plugin-0.2.0.dev436}/sentinel/actions/update_defender_xdr_incident.py +0 -0
  11. {howler_sentinel_plugin-0.2.0.dev418 → howler_sentinel_plugin-0.2.0.dev436}/sentinel/config.py +0 -0
  12. {howler_sentinel_plugin-0.2.0.dev418 → howler_sentinel_plugin-0.2.0.dev436}/sentinel/manifest.yml +0 -0
  13. {howler_sentinel_plugin-0.2.0.dev418 → howler_sentinel_plugin-0.2.0.dev436}/sentinel/mapping/xdr_alert.py +0 -0
  14. {howler_sentinel_plugin-0.2.0.dev418 → howler_sentinel_plugin-0.2.0.dev436}/sentinel/mapping/xdr_alert_evidence.py +0 -0
  15. {howler_sentinel_plugin-0.2.0.dev418 → howler_sentinel_plugin-0.2.0.dev436}/sentinel/odm/__init__.py +0 -0
  16. {howler_sentinel_plugin-0.2.0.dev418 → howler_sentinel_plugin-0.2.0.dev436}/sentinel/odm/hit.py +0 -0
  17. {howler_sentinel_plugin-0.2.0.dev418 → howler_sentinel_plugin-0.2.0.dev436}/sentinel/odm/models/sentinel.py +0 -0
  18. {howler_sentinel_plugin-0.2.0.dev418 → howler_sentinel_plugin-0.2.0.dev436}/sentinel/routes/__init__.py +0 -0
  19. {howler_sentinel_plugin-0.2.0.dev418 → howler_sentinel_plugin-0.2.0.dev436}/sentinel/routes/ingest.py +0 -0
  20. {howler_sentinel_plugin-0.2.0.dev418 → howler_sentinel_plugin-0.2.0.dev436}/sentinel/utils/tenant_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: howler-sentinel-plugin
3
- Version: 0.2.0.dev418
3
+ Version: 0.2.0.dev436
4
4
  Summary: A howler plugin for integration with Microsoft's Sentinel API
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "howler-sentinel-plugin"
3
- version = "0.2.0.dev418"
3
+ version = "0.2.0.dev436"
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" }
@@ -131,21 +131,21 @@ packages = [{ include = "sentinel" }]
131
131
 
132
132
 
133
133
  [tool.poetry.group.dev.dependencies]
134
- ruff = "^0.15.20"
134
+ ruff = "^0.16.0"
135
135
  mypy = "^1.20.2"
136
- pre-commit = "^4.6.0"
136
+ pre-commit = "^4.6.1"
137
137
  howler-api = { path = "../../api" }
138
138
  pytest = "^9.1.1"
139
139
  mock = "^5.2.0"
140
140
  pytest-cov = "^6.3.0"
141
- coverage = { extras = ["toml"], version = "^7.15.0" }
142
- diff-cover = "^10.3.0"
141
+ coverage = { extras = ["toml"], version = "^7.15.2" }
142
+ diff-cover = "^10.4.1"
143
143
  python-dotenv = "^1.2.2"
144
144
 
145
145
 
146
146
  [tool.poetry.group.types.dependencies]
147
147
  types-mock = "^5.2.0.20260518"
148
- types-python-dateutil = "^2.9.0.20260518"
148
+ types-python-dateutil = "^2.9.0.20260716"
149
149
 
150
150
 
151
151
  [tool.poetry.dependencies]
@@ -2,10 +2,16 @@
2
2
 
3
3
  import json
4
4
  import logging
5
+ import sys
5
6
  from typing import Any, Optional
6
7
 
7
8
  from dateutil import parser
8
9
 
10
+ if sys.version_info >= (3, 11):
11
+ from typing import Self
12
+ else:
13
+ from typing_extensions import Self
14
+
9
15
  # Use standard logging for now
10
16
  logger = logging.getLogger(__name__)
11
17
 
@@ -15,7 +21,7 @@ class SentinelIncident:
15
21
 
16
22
  DEFAULT_CUSTOMER_NAME = "Unknown Customer"
17
23
 
18
- def __init__(self, tid_mapping: Optional[dict[str, str]] = None):
24
+ def __init__(self: Self, tid_mapping: Optional[dict[str, str]] = None):
19
25
  """Initialize the Sentiel Incident mapper.
20
26
 
21
27
  Args:
@@ -25,7 +31,7 @@ class SentinelIncident:
25
31
 
26
32
  # --- Public mapping methods ---
27
33
 
28
- def map_incident_to_bundle(self, sentinel_incident: dict[str, Any]) -> Optional[dict[str, Any]]:
34
+ def map_incident_to_bundle(self: Self, sentinel_incident: dict[str, Any]) -> Optional[dict[str, Any]]:
29
35
  """Map an Sentiel Incident to a Howler bundle.
30
36
 
31
37
  Args:
@@ -97,8 +103,10 @@ class SentinelIncident:
97
103
  }
98
104
  self._map_graph_host_link(sentinel_incident, bundle)
99
105
  self._map_timestamps(sentinel_incident, bundle)
100
- # Add assessment conditionally if classification is not null
101
- if classification is not None:
106
+ # Only apply assessment for incidents that are already resolved.
107
+ # Otherwise, convert_hit may coerce status to resolved based on
108
+ # escalation inferred from assessment.
109
+ if classification is not None and bundle["howler"]["status"] == "resolved":
102
110
  bundle["howler"]["assessment"] = self.map_classification(classification)
103
111
  logger.info("Successfully mapped Sentinel Incident %s", incident_id)
104
112
  return bundle
@@ -107,7 +115,7 @@ class SentinelIncident:
107
115
  logger.error("Failed to map Sentiel Incident: %s", exc, exc_info=True)
108
116
  return None
109
117
 
110
- def get_customer_name(self, tid: str) -> str:
118
+ def get_customer_name(self: Self, tid: str) -> str:
111
119
  """Get customer name from tenant ID, return default if not found.
112
120
 
113
121
  Args:
@@ -117,7 +125,7 @@ class SentinelIncident:
117
125
  """
118
126
  return self.tid_mapping.get(tid, self.DEFAULT_CUSTOMER_NAME)
119
127
 
120
- def map_sentinel_status_to_howler(self, sentinel_status: Optional[str]) -> str:
128
+ def map_sentinel_status_to_howler(self: Self, sentinel_status: Optional[str]) -> str:
121
129
  """Map Sentinel Incident status to Howler status.
122
130
 
123
131
  Args:
@@ -138,7 +146,7 @@ class SentinelIncident:
138
146
  }
139
147
  return status_mapping.get(sentinel_status, "open")
140
148
 
141
- def map_sentinel_user_to_howler(self, sentinel_user: Optional[str]) -> str:
149
+ def map_sentinel_user_to_howler(self: Self, sentinel_user: Optional[str]) -> str:
142
150
  """Map Sentinel user assignment to Howler format.
143
151
 
144
152
  Args:
@@ -150,7 +158,7 @@ class SentinelIncident:
150
158
  return "unassigned"
151
159
  return sentinel_user
152
160
 
153
- def map_severity_to_score(self, severity: str) -> int:
161
+ def map_severity_to_score(self: Self, severity: str) -> int:
154
162
  """Map string severity to numeric score.
155
163
 
156
164
  Args:
@@ -161,7 +169,7 @@ class SentinelIncident:
161
169
  severity_mapping: dict[str, int] = {"low": 25, "medium": 50, "high": 75, "critical": 100}
162
170
  return severity_mapping.get(severity.lower() if severity else "medium", 50)
163
171
 
164
- def map_classification(self, classification: str) -> str:
172
+ def map_classification(self: Self, classification: str) -> str:
165
173
  """Map Sentinel classification to Howler assessment.
166
174
 
167
175
  Args:
@@ -180,7 +188,7 @@ class SentinelIncident:
180
188
 
181
189
  # --- Private helper methods ---
182
190
 
183
- def _map_graph_host_link(self, graph_alert: dict[str, Any], howler_hit: dict[str, Any]) -> None:
191
+ def _map_graph_host_link(self: Self, graph_alert: dict[str, Any], howler_hit: dict[str, Any]) -> None:
184
192
  """Map Graph host link from Graph alert to Howler hit.
185
193
 
186
194
  Args:
@@ -196,7 +204,7 @@ class SentinelIncident:
196
204
  howler_hit["howler"]["links"] = howler_hit["howler"].get("links", [])
197
205
  howler_hit["howler"]["links"].append(link)
198
206
 
199
- def _map_timestamps(self, graph_alert: dict[str, Any], howler_hit: dict[str, Any]) -> None:
207
+ def _map_timestamps(self: Self, graph_alert: dict[str, Any], howler_hit: dict[str, Any]) -> None:
200
208
  """Map timestamps from Graph alert to Howler hit.
201
209
 
202
210
  Args:
@@ -222,7 +230,7 @@ class SentinelIncident:
222
230
  except Exception as exc:
223
231
  logger.warning("Invalid timestamp format for %s: %s (%s)", time_field, timestamp, exc)
224
232
 
225
- def _build_labels(self, custom_tags: list[str], system_tags: list[str]) -> list[str]:
233
+ def _build_labels(self: Self, custom_tags: list[str], system_tags: list[str]) -> list[str]:
226
234
  """Build combined labels from custom and system tags.
227
235
 
228
236
  Args: