django-log-formatter-asim 1.0.0__py3-none-any.whl → 1.1.0__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,13 +1,15 @@
1
1
  import json
2
2
  import logging
3
3
  from datetime import datetime
4
+ from datetime import timezone
4
5
  from importlib.metadata import distribution
5
- import os
6
6
 
7
7
  import ddtrace
8
8
  from ddtrace.trace import tracer
9
9
  from django.conf import settings
10
10
 
11
+ from .ecs import _get_container_id
12
+
11
13
 
12
14
  class ASIMRootFormatter:
13
15
  def __init__(self, record):
@@ -28,19 +30,6 @@ class ASIMRootFormatter:
28
30
 
29
31
  return copied_dict
30
32
 
31
- def _get_container_id(self):
32
- """
33
- The dockerId (container Id) is available via the metadata endpoint. However, it looks like it is embedded in the
34
- metadata URL e.g.:
35
- ECS_CONTAINER_METADATA_URI=http://169.254.170.2/v3/709d1c10779d47b2a84db9eef2ebd041-0265927825
36
- See: https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-metadata-endpoint-v4-response.html
37
- """
38
-
39
- try:
40
- return os.environ["ECS_CONTAINER_METADATA_URI"].split("/")[-1]
41
- except (KeyError, IndexError):
42
- return ""
43
-
44
33
  def _get_first_64_bits_of(self, trace_id):
45
34
  # See https://docs.datadoghq.com/tracing/other_telemetry/connect_logs_and_traces/python/#no-standard-library-logging
46
35
  return str((1 << 64) - 1 & trace_id)
@@ -64,13 +53,13 @@ class ASIMRootFormatter:
64
53
  event_dict["service"] = ddtrace.config.service or ""
65
54
  event_dict["version"] = ddtrace.config.version or ""
66
55
 
67
- event_dict["container_id"] = self._get_container_id()
56
+ event_dict["container_id"] = _get_container_id()
68
57
 
69
58
  return event_dict
70
59
 
71
60
  def get_log_dict(self):
72
61
  record = self.record
73
- log_time = datetime.utcfromtimestamp(record.created).isoformat()
62
+ log_time = datetime.fromtimestamp(record.created, timezone.utc).isoformat()
74
63
  log_dict = {
75
64
  # Event fields...
76
65
  "EventMessage": record.getMessage(),
@@ -200,14 +189,6 @@ class ASIMRequestFormatter(ASIMRootFormatter):
200
189
  http_user_agent = request.META.get("HTTP_USER_AGENT", None)
201
190
  return http_user_agent
202
191
 
203
- def _get_ip_address(self, request):
204
- # Import here as ipware uses settings
205
- from ipware import get_client_ip
206
-
207
- client_ip, is_routable = get_client_ip(request)
208
- return client_ip or "Unknown"
209
-
210
-
211
192
  ASIM_FORMATTERS = {
212
193
  "root": ASIMRootFormatter,
213
194
  "django.request": ASIMRequestFormatter,
@@ -0,0 +1,16 @@
1
+ import os
2
+
3
+
4
+ def _get_container_id():
5
+ """
6
+ The dockerId (container Id) is available via the metadata endpoint.
7
+
8
+ However, it looks like it is embedded in the metadata URL e.g.:
9
+ ECS_CONTAINER_METADATA_URI=http://169.254.170.2/v3/709d1c10779d47b2a84db9eef2ebd041-0265927825
10
+ See: https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-metadata-endpoint-v4-response.html
11
+ """
12
+
13
+ try:
14
+ return os.environ["ECS_CONTAINER_METADATA_URI"].split("/")[-1]
15
+ except (KeyError, IndexError):
16
+ return ""
@@ -0,0 +1,2 @@
1
+ from .authentication import log_authentication
2
+ from .file_activity import log_file_activity
@@ -0,0 +1,205 @@
1
+ import datetime
2
+ import json
3
+ import os
4
+ from enum import Enum
5
+ from hashlib import sha3_512
6
+ from typing import Literal
7
+ from typing import Optional
8
+ from typing import TypedDict
9
+
10
+ from django.http import HttpRequest
11
+
12
+ from django_log_formatter_asim.ecs import _get_container_id
13
+
14
+ from .common import Client
15
+ from .common import Result
16
+ from .common import Server
17
+ from .common import Severity
18
+ from .common import _default_severity
19
+ from .common import _get_client_ip_address
20
+
21
+
22
+ class AuthenticationEvent(str, Enum):
23
+ Logon = "Logon"
24
+ Logoff = "Logoff"
25
+
26
+
27
+ class AuthenticationLoginMethod(str, Enum):
28
+ UsernamePassword = "Username & Password"
29
+ StaffSSO = "Staff-SSO"
30
+ UKGOVSSO = "UK.GOV-SSO"
31
+ ExternalIDP = "External IdP"
32
+
33
+
34
+ class AuthenticationUser(TypedDict):
35
+ """Dictionary to represent properties of the users session."""
36
+
37
+ """What type of role best describes this Authentication event."""
38
+ role: Optional[
39
+ Literal[
40
+ "Regular",
41
+ "Machine",
42
+ "Admin",
43
+ "System",
44
+ "Application",
45
+ "Service Principal",
46
+ "Service",
47
+ "Anonymous",
48
+ "Other",
49
+ ]
50
+ ]
51
+ """
52
+ A unique identifier for the user.
53
+
54
+ Defaults to the logged in Django User.username if not provided.
55
+ """
56
+ username: Optional[str]
57
+ """
58
+ A unique identifier for this authentication session if one exists.
59
+
60
+ Defaults to the Django Sessions session key if not provided.
61
+ """
62
+ sessionId: Optional[str]
63
+
64
+
65
+ def log_authentication(
66
+ request: HttpRequest,
67
+ event: AuthenticationEvent,
68
+ result: Result,
69
+ login_method: AuthenticationLoginMethod,
70
+ user: Optional[AuthenticationUser] = None,
71
+ server: Optional[Server] = None,
72
+ client: Optional[Client] = None,
73
+ severity: Optional[Severity] = None,
74
+ time_generated: Optional[datetime.datetime] = None,
75
+ result_details: Optional[str] = None,
76
+ message: Optional[str] = None,
77
+ ):
78
+ """
79
+ Log an ASIM Authentication Event to standard output.
80
+
81
+ :param request: django.http.HttpRequest object which initiated this Authentication request
82
+ from which the following data will be logged if available
83
+ - Django Authentication systems current username
84
+ - Django Session middlewares Session Key
85
+ - Client IP address
86
+ - URL requested by the client
87
+ - Server domain name
88
+ :param event: What authentication action was attempted, either "Logon" or "Logoff"
89
+ :param result: What outcome did the action have, either "Success", "Failure", "Partial", "NA"
90
+ :param login_method: What authentication mechanism was being used, one of:
91
+ - "Username & Password"
92
+ - "Staff-SSO"
93
+ - "UK.GOV-SSO"
94
+ - "External IdP"
95
+ :param user: Dictionary containing information on the subject of this Authentication event
96
+ see AuthenticationUser class for more details.
97
+ :param server: Dictionary containing information on the server servicing this Authentication event
98
+ see Server class for more details.
99
+ :param client: Dictionary containing information on the client performing this Authentication event
100
+ see Client class for more details.
101
+ :param severity: Optional severity of the event, defaults to "Informational", otherwise one of:
102
+ - "Informational"
103
+ - "Low"
104
+ - "Medium"
105
+ - "High"
106
+ :param time_generated: Optional datetime for when the event happened, otherwise datetime.now
107
+ :param result_details: Optional string describing any details associated with the events outcome.
108
+ This field is typically populated when the result is a failure.
109
+ :param message: Optional string describing the reason why the log was generated.
110
+
111
+ See also: https://learn.microsoft.com/en-us/azure/sentinel/normalization-schema-authentication
112
+ """
113
+ if user == None:
114
+ user = {}
115
+ if server == None:
116
+ server = {}
117
+ if client == None:
118
+ client = {}
119
+
120
+ event_created = time_generated or datetime.datetime.now(tz=datetime.timezone.utc)
121
+
122
+ log = {
123
+ "EventStartTime": event_created.isoformat(),
124
+ "EventSeverity": severity or _default_severity(result),
125
+ "EventOriginalType": _event_code(event, result),
126
+ "EventType": event,
127
+ "EventResult": result,
128
+ "LogonMethod": login_method,
129
+ "EventSchema": "Authentication",
130
+ "EventSchemaVersion": "0.1.4",
131
+ }
132
+
133
+ if "domain_name" in server:
134
+ log["HttpHost"] = server["domain_name"]
135
+ elif "HTTP_HOST" in request.META:
136
+ log["HttpHost"] = request.get_host()
137
+
138
+ if "service_name" in server:
139
+ log["TargetAppName"] = server["service_name"]
140
+ elif os.environ.get("COPILOT_APPLICATION_NAME") and os.environ.get("COPILOT_SERVICE_NAME"):
141
+ app_name = f"{os.environ['COPILOT_APPLICATION_NAME']}-{os.environ['COPILOT_SERVICE_NAME']}"
142
+ log["TargetAppName"] = app_name
143
+
144
+ if container_id := _get_container_id():
145
+ log["TargetContainerId"] = container_id
146
+
147
+ if "ip_address" in client:
148
+ log["SrcIpAddr"] = client["ip_address"]
149
+ elif client_ip := _get_client_ip_address(request):
150
+ log["SrcIpAddr"] = client_ip
151
+
152
+ if "requested_url" in client:
153
+ log["TargetUrl"] = client["requested_url"]
154
+ elif "HTTP_HOST" in request.META:
155
+ log["TargetUrl"] = request.scheme + "://" + request.get_host() + request.get_full_path()
156
+
157
+ if "role" in user:
158
+ log["TargetUserType"] = user["role"]
159
+
160
+ if "sessionId" in user:
161
+ log["TargetSessionId"] = _cryptographically_hash(user["sessionId"])
162
+ elif hasattr(request, "session") and request.session.session_key:
163
+ log["TargetSessionId"] = _cryptographically_hash(request.session.session_key)
164
+
165
+ if "username" in user:
166
+ log["TargetUsername"] = user["username"]
167
+ elif hasattr(request, "user") and request.user.username:
168
+ log["TargetUsername"] = request.user.username
169
+
170
+ if result_details:
171
+ log["EventResultDetails"] = result_details
172
+
173
+ if message:
174
+ log["EventMessage"] = message
175
+
176
+ if "ip_address" in server:
177
+ log["DvcIpAddr"] = server["ip_address"]
178
+
179
+ print(json.dumps(log), flush=True)
180
+
181
+
182
+ log_authentication.Event = AuthenticationEvent
183
+ log_authentication.Result = Result
184
+ log_authentication.LoginMethod = AuthenticationLoginMethod
185
+ log_authentication.Severity = Severity
186
+
187
+
188
+ def _cryptographically_hash(data: Optional[str]) -> Optional[str]:
189
+ if data is None:
190
+ return None
191
+ return sha3_512(data.encode("UTF-8")).hexdigest()
192
+
193
+
194
+ def _event_code(event: AuthenticationEvent, result: Result) -> str:
195
+ if event == AuthenticationEvent.Logon:
196
+ if result == Result.Success:
197
+ return "001a"
198
+ elif result == Result.Failure:
199
+ return "001b"
200
+ elif event == AuthenticationEvent.Logoff:
201
+ if result == Result.Success:
202
+ return "001c"
203
+ elif result == Result.Failure:
204
+ return "001d"
205
+ return "001"
@@ -0,0 +1,63 @@
1
+ from enum import Enum
2
+ from typing import Optional
3
+ from typing import TypedDict
4
+
5
+ from django.http import HttpRequest
6
+
7
+
8
+ class Result(str, Enum):
9
+ Success = "Success"
10
+ Partial = "Partial"
11
+ Failure = "Failure"
12
+ NA = "NA"
13
+
14
+
15
+ class Severity(str, Enum):
16
+ Informational = "Informational"
17
+ Low = "Low"
18
+ Medium = "Medium"
19
+ High = "High"
20
+
21
+
22
+ class Client(TypedDict):
23
+ """Dictionary to represent properties of the HTTP Client."""
24
+
25
+ """Internet Protocol Address of the client making the Authentication
26
+ event."""
27
+ ip_address: Optional[str]
28
+ """URL requested by the client."""
29
+ requested_url: Optional[str]
30
+
31
+
32
+ class Server(TypedDict):
33
+ """Dictionary to represent properties of the HTTP Server."""
34
+
35
+ """
36
+ The FQDN that this server is listening to HTTP requests on. For example:
37
+ web.trade.gov.uk
38
+
39
+ Defaults to the WSGI HTTP_HOST field if not provided.
40
+ """
41
+ domain_name: Optional[str]
42
+ """Internet Protocol Address of the server serving this request."""
43
+ ip_address: Optional[str]
44
+ """
45
+ A unique (within DBT) identifier for the software running on the server.
46
+ For example: berry-auctions-frontend
47
+
48
+ Defaults to combining the environment variables COPILOT_APPLICATION_NAME and
49
+ COPILOT_SERVICE_NAME separated by a '-'.
50
+ """
51
+ service_name: Optional[str]
52
+
53
+
54
+ def _default_severity(result: Result) -> Severity:
55
+ return Severity.Informational if result == Result.Success else Severity.Medium
56
+
57
+
58
+ def _get_client_ip_address(request: HttpRequest) -> Optional[str]:
59
+ # Import here as ipware uses settings
60
+ from ipware import get_client_ip
61
+
62
+ client_ip, _ = get_client_ip(request)
63
+ return client_ip
@@ -0,0 +1,215 @@
1
+ import datetime
2
+ import json
3
+ import os
4
+ from enum import Enum
5
+ from typing import Optional
6
+ from typing import TypedDict
7
+
8
+ from django.http import HttpRequest
9
+
10
+ from django_log_formatter_asim.ecs import _get_container_id
11
+
12
+ from .common import Client
13
+ from .common import Result
14
+ from .common import Server
15
+ from .common import Severity
16
+ from .common import _default_severity
17
+ from .common import _get_client_ip_address
18
+
19
+
20
+ class FileActivityEvent(str, Enum):
21
+ FileAccessed = "FileAccessed"
22
+ FileCreated = "FileCreated"
23
+ FileModified = "FileModified"
24
+ FileDeleted = "FileDeleted"
25
+ FileRenamed = "FileRenamed"
26
+ FileCopied = "FileCopied"
27
+ FileMoved = "FileMoved"
28
+ FolderCreated = "FolderCreated"
29
+ FolderDeleted = "FolderDeleted"
30
+ FolderMoved = "FolderMoved"
31
+ FolderModified = "FolderModified"
32
+
33
+
34
+ class FileActivityFile(TypedDict):
35
+ """Dictionary to represent properties of the target file."""
36
+
37
+ """
38
+ The full, normalized path of the target file, including the folder or location,
39
+ the file name, and the extension.
40
+ """
41
+ path: str
42
+ """
43
+ The name of the target file, without a path or a location, but with an
44
+ extension if available. This field should be similar to the final element in
45
+ the TargetFilePath field.
46
+
47
+ Defaults to extracting the name based off the path if not provided.
48
+ """
49
+ name: Optional[str]
50
+ """
51
+ The target file extension.
52
+
53
+ Defaults to extracting the extension based off the path if not provided.
54
+ """
55
+ extension: Optional[str]
56
+ """
57
+ The Mime, or Media, type of the target file.
58
+
59
+ Allowed values are listed in the IANA Media Types repository.
60
+ """
61
+ content_type: Optional[str]
62
+ """The SHA256 value of the target file."""
63
+ sha256: Optional[str]
64
+ """The size of the target file in bytes."""
65
+ size: Optional[int]
66
+
67
+
68
+ class FileActivityUser(TypedDict):
69
+ """
70
+ A unique identifier for the user.
71
+
72
+ Defaults to the logged in Django User.username if not provided.
73
+ """
74
+
75
+ username: Optional[str]
76
+
77
+
78
+ def log_file_activity(
79
+ request: HttpRequest,
80
+ event: FileActivityEvent,
81
+ result: Result,
82
+ file: FileActivityFile,
83
+ user: Optional[FileActivityUser] = None,
84
+ server: Optional[Server] = None,
85
+ client: Optional[Client] = None,
86
+ severity: Optional[Severity] = None,
87
+ time_generated: Optional[datetime.datetime] = None,
88
+ result_details: Optional[str] = None,
89
+ message: Optional[str] = None,
90
+ ):
91
+ """
92
+ Log an ASIM File Event to standard output.
93
+
94
+ :param request: django.http.HttpRequest object which initiated this Authentication request
95
+ from which the following data will be logged if available
96
+ - Django Authentication systems current username
97
+ - Client IP address
98
+ - URL requested by the client
99
+ - Server domain name
100
+ :param event: What File Event action was attempted, one of:
101
+ - FileAccessed
102
+ - FileCreated
103
+ - FileModified
104
+ - FileDeleted
105
+ - FileRenamed
106
+ - FileCopied
107
+ - FileMoved
108
+ - FolderCreated
109
+ - FolderDeleted
110
+ - FolderMoved
111
+ - FolderModified
112
+ :param result: What outcome did the action have, either "Success", "Failure", "Partial", "NA"
113
+ :param file: Dictionary containing information on the target of this File event see
114
+ FileActivityFile for more details.
115
+ :param user: Dictionary containing information on the logged in users username.
116
+ :param server: Dictionary containing information on the server servicing this File event
117
+ see Server class for more details.
118
+ :param client: Dictionary containing information on the client performing this File event
119
+ see Client class for more details.
120
+ :param severity: Optional severity of the event, defaults to "Informational", otherwise one of:
121
+ - "Informational"
122
+ - "Low"
123
+ - "Medium"
124
+ - "High"
125
+ :param time_generated: Optional datetime for when the event happened, otherwise datetime.now
126
+ :param result_details: Optional string describing any details associated with the events outcome.
127
+ This field is typically populated when the result is a failure.
128
+ :param message: Optional string describing the reason why the log was generated.
129
+
130
+ See also: https://learn.microsoft.com/en-us/azure/sentinel/normalization-schema-file-event
131
+ """
132
+ if user == None:
133
+ user = {}
134
+ if server == None:
135
+ server = {}
136
+ if client == None:
137
+ client = {}
138
+
139
+ event_created = time_generated or datetime.datetime.now(tz=datetime.timezone.utc)
140
+
141
+ log = {
142
+ "EventSchema": "FileEvent",
143
+ "EventSchemaVersion": "0.2.1",
144
+ "EventType": event,
145
+ "EventResult": result,
146
+ "EventStartTime": event_created.isoformat(),
147
+ "EventSeverity": severity or _default_severity(result),
148
+ "TargetFilePath": file["path"],
149
+ }
150
+
151
+ if "name" in file:
152
+ log["TargetFileName"] = file["name"]
153
+ else:
154
+ log["TargetFileName"] = os.path.basename(file["path"])
155
+
156
+ if "extension" in file:
157
+ log["TargetFileExtension"] = file["extension"]
158
+ else:
159
+ file_name_parts = list(filter(None, log["TargetFileName"].split(".", 1)))
160
+ if len(file_name_parts) > 1:
161
+ log["TargetFileExtension"] = file_name_parts[1]
162
+
163
+ if "content_type" in file:
164
+ log["TargetFileMimeType"] = file["content_type"]
165
+
166
+ if "sha256" in file:
167
+ log["TargetFileSHA256"] = file["sha256"]
168
+
169
+ if "size" in file:
170
+ log["TargetFileSize"] = file["size"]
171
+
172
+ if "domain_name" in server:
173
+ log["HttpHost"] = server["domain_name"]
174
+ elif "HTTP_HOST" in request.META:
175
+ log["HttpHost"] = request.get_host()
176
+
177
+ if "service_name" in server:
178
+ log["TargetAppName"] = server["service_name"]
179
+ elif os.environ.get("COPILOT_APPLICATION_NAME") and os.environ.get("COPILOT_SERVICE_NAME"):
180
+ app_name = f"{os.environ['COPILOT_APPLICATION_NAME']}-{os.environ['COPILOT_SERVICE_NAME']}"
181
+ log["TargetAppName"] = app_name
182
+
183
+ if container_id := _get_container_id():
184
+ log["TargetContainerId"] = container_id
185
+
186
+ if "ip_address" in client:
187
+ log["SrcIpAddr"] = client["ip_address"]
188
+ elif client_ip := _get_client_ip_address(request):
189
+ log["SrcIpAddr"] = client_ip
190
+
191
+ if "requested_url" in client:
192
+ log["TargetUrl"] = client["requested_url"]
193
+ elif "HTTP_HOST" in request.META:
194
+ log["TargetUrl"] = request.scheme + "://" + request.get_host() + request.get_full_path()
195
+
196
+ if "username" in user:
197
+ log["TargetUsername"] = user["username"]
198
+ elif hasattr(request, "user") and request.user.username:
199
+ log["TargetUsername"] = request.user.username
200
+
201
+ if result_details:
202
+ log["EventResultDetails"] = result_details
203
+
204
+ if message:
205
+ log["EventMessage"] = message
206
+
207
+ if "ip_address" in server:
208
+ log["DvcIpAddr"] = server["ip_address"]
209
+
210
+ print(json.dumps(log), flush=True)
211
+
212
+
213
+ log_file_activity.Event = FileActivityEvent
214
+ log_file_activity.Result = Result
215
+ log_file_activity.Severity = Severity
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: django-log-formatter-asim
3
- Version: 1.0.0
3
+ Version: 1.1.0
4
4
  Summary: Formats Django logs in ASIM format.
5
5
  License: MIT
6
6
  Author: Department for Business and Trade Platform Team
@@ -16,7 +16,7 @@ Classifier: Programming Language :: Python :: 3.13
16
16
  Requires-Dist: ddtrace (>=3.2.1,<4.0.0)
17
17
  Requires-Dist: django (>=3,<5) ; python_version == "3.9"
18
18
  Requires-Dist: django (>=3,<6) ; python_version >= "3.10" and python_version < "4"
19
- Requires-Dist: pre-commit (>=3.5.0,<4.0.0)
19
+ Requires-Dist: django-ipware (>=7.0.1,<8.0.0)
20
20
  Description-Content-Type: text/markdown
21
21
 
22
22
  # Django ASIM log formatter
@@ -25,8 +25,6 @@ The library formats Django logs in [ASIM format](https://learn.microsoft.com/en-
25
25
 
26
26
  Mapping to the format may not be complete, but best effort has been made to create logical field mappings.
27
27
 
28
- If you need to amend the mapping, you can implement a custom formatter.
29
-
30
28
  ## Installation
31
29
 
32
30
  ``` shell
@@ -35,7 +33,16 @@ pip install django-log-formatter-asim
35
33
 
36
34
  ## Usage
37
35
 
38
- Using in a Django logging configuration:
36
+ This package provides the following ASIM functionality:
37
+
38
+ - A Python [logging.Formatter] implementation.
39
+ - A module of functions `django_log_formatter_asim.events` which generate ASIM event log entries.
40
+
41
+ [logging.Formatter]: https://docs.python.org/3/library/logging.html#formatter-objects
42
+
43
+ ### `logging.Formatter` setup
44
+
45
+ Using the formatter in a Django logging configuration:
39
46
 
40
47
  ``` python
41
48
  from django_log_formatter_asim import ASIMFormatter
@@ -66,15 +73,61 @@ LOGGING = {
66
73
  },
67
74
  }
68
75
  ```
69
- In this example we assign the ASIM formatter to a `handler` and ensure both `root` and `django` loggers use this `handler`. We then set `propagate` to `False` on the `django` logger, to avoid duplicating logs at the root level.
70
76
 
71
- ## Dependencies
77
+ In this example we assign the ASIM formatter to a `handler` and ensure both `root` and `django` loggers use this `handler`.
78
+ We then set `propagate` to `False` on the `django` logger, to avoid duplicating logs at the root level.
72
79
 
73
- This package uses [Django IPware](https://github.com/un33k/django-ipware) for IP address capture.
80
+ ### ASIM Events
74
81
 
75
- This package is compatible with [Django User Agents](https://pypi.org/project/django-user-agents) which, when used, will enhance logged user agent information.
82
+ The events mostly follow the Microsoft schema but have been tailored to Department of Business and Trade needs.
83
+
84
+ Events are designed for simple integrate into your Django app.
85
+ Each will take additional information from the [Django HttpRequest object][django-request].
86
+
87
+ [django-request]: https://docs.djangoproject.com/en/5.2/ref/request-response/#httprequest-objects
88
+
89
+ #### Authentication event
76
90
 
77
- ## Settings
91
+ Following the [ASIM Authentication Schema](https://learn.microsoft.com/en-us/azure/sentinel/normalization-schema-authentication).
92
+
93
+ ```python
94
+ # Example usage
95
+ from django_log_formatter_asim.events import log_authentication
96
+
97
+ log_authentication(
98
+ request,
99
+ event=log_authentication.Event.Logoff,
100
+ result=log_authentication.Result.Success,
101
+ login_method=log_authentication.LoginMethod.UsernamePassword,
102
+ )
103
+
104
+ # Example JSON printed to standard output
105
+ {
106
+ # Values provided as arguments
107
+ "EventType": "Logoff",
108
+ "EventResult": "Success",
109
+ "LogonMethod": "Username & Password",
110
+
111
+ # Calculated / Hard coded fields
112
+ "EventStartTime": "2025-07-02T08:15:20+00:00",
113
+ "EventSeverity": "Informational",
114
+ "EventOriginalType": "001c",
115
+ "EventSchema": "Authentication",
116
+ "EventSchemaVersion": "0.1.4",
117
+
118
+ # Taken from Django HttpRequest object
119
+ "HttpHost": "WebServer.local",
120
+ "SrcIpAddr": "192.168.1.101",
121
+ "TargetUrl": "https://WebServer.local/steel",
122
+ "TargetSessionId": "def456",
123
+ "TargetUsername": "Adrian"
124
+
125
+ # Taken from DBT Platform environment variables
126
+ "TargetAppName": "export-analytics-frontend",
127
+ }
128
+ ```
129
+
130
+ ### Settings
78
131
 
79
132
  `DLFA_LOG_PERSONALLY_IDENTIFIABLE_INFORMATION` - the formatter checks this setting to see if personally identifiable information should be logged. If this is not set to true, only the user's id is logged.
80
133
 
@@ -89,7 +142,7 @@ if is_copilot():
89
142
  DLFA_TRACE_HEADERS = ("X-B3-TraceId", "X-B3-SpanId")
90
143
  ```
91
144
 
92
- ## Formatter classes
145
+ ### Formatter classes
93
146
 
94
147
  ``` python
95
148
  ASIM_FORMATTERS = {
@@ -104,7 +157,7 @@ The default class for other loggers is:
104
157
  ASIMSystemFormatter
105
158
  ```
106
159
 
107
- ## Creating a custom formatter
160
+ ### Creating a custom `logging.Formatter`
108
161
 
109
162
  If you wish to create your own ASIM formatter, you can inherit from ASIMSystemFormatter and call _get_event_base to get the base level logging data for use in augmentation:
110
163
 
@@ -118,6 +171,12 @@ If you wish to create your own ASIM formatter, you can inherit from ASIMSystemFo
118
171
  return logger_event
119
172
  ```
120
173
 
174
+ ## Dependencies
175
+
176
+ This package uses [Django IPware](https://github.com/un33k/django-ipware) for IP address capture.
177
+
178
+ This package is compatible with [Django User Agents](https://pypi.org/project/django-user-agents) which, when used, will enhance logged user agent information.
179
+
121
180
  ## Contributing to the `django-log-formatter-asim` package
122
181
 
123
182
  ### Getting started
@@ -0,0 +1,10 @@
1
+ django_log_formatter_asim/__init__.py,sha256=ftbSWdejSnWK_MpT3RSfTgcs3E-661FM-e3ugnoiBqA,7133
2
+ django_log_formatter_asim/ecs.py,sha256=SSg3A5pfS5E-Hm7AXQnN1RrtXclgq07oSBxPCRn5gDg,536
3
+ django_log_formatter_asim/events/__init__.py,sha256=th8AEFNM-J5lNlO-d8Lk465jXqplE3IoTwj4DlscwYo,92
4
+ django_log_formatter_asim/events/authentication.py,sha256=sBbuEBawoZJdTndKSNxJZDX6j3ALfLRg9E_X44mhNLk,7206
5
+ django_log_formatter_asim/events/common.py,sha256=9LeiEn11FJowf2QiI5MLXlGIbY4MVRChABVcfchX8ko,1676
6
+ django_log_formatter_asim/events/file_activity.py,sha256=CfbmmBTfP2gcxGVeXFSv_xDSSdSMfdsCosD7bndjLMI,7417
7
+ django_log_formatter_asim-1.1.0.dist-info/LICENSE,sha256=dP79lN73--7LMApnankTGLqDbImXg8iYFqWgnExGkGk,1090
8
+ django_log_formatter_asim-1.1.0.dist-info/METADATA,sha256=LKvHJwoGaKMtkR9CGTsG1BNhfgppM8kQrww0WJAgIYs,7426
9
+ django_log_formatter_asim-1.1.0.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
10
+ django_log_formatter_asim-1.1.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 2.1.1
2
+ Generator: poetry-core 2.1.3
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,5 +0,0 @@
1
- django_log_formatter_asim/__init__.py,sha256=i-7HqYE5s3hjmHLh4TTzLpEdgr5N22msFEQw16Pe_EI,7867
2
- django_log_formatter_asim-1.0.0.dist-info/LICENSE,sha256=dP79lN73--7LMApnankTGLqDbImXg8iYFqWgnExGkGk,1090
3
- django_log_formatter_asim-1.0.0.dist-info/METADATA,sha256=Gmn92QLejMsh63aivEaeqLfOKfnGl-9Hw5MPO-YC5KQ,5576
4
- django_log_formatter_asim-1.0.0.dist-info/WHEEL,sha256=XbeZDeTWKc1w7CSIyre5aMDU_-PohRwTQceYnisIYYY,88
5
- django_log_formatter_asim-1.0.0.dist-info/RECORD,,