django-log-formatter-asim 1.1.0a4__tar.gz → 1.2.0a0__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: django-log-formatter-asim
3
- Version: 1.1.0a4
3
+ Version: 1.2.0a0
4
4
  Summary: Formats Django logs in ASIM format.
5
5
  License: MIT
6
6
  Author: Department for Business and Trade Platform Team
@@ -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
@@ -4,8 +4,6 @@ The library formats Django logs in [ASIM format](https://learn.microsoft.com/en-
4
4
 
5
5
  Mapping to the format may not be complete, but best effort has been made to create logical field mappings.
6
6
 
7
- If you need to amend the mapping, you can implement a custom formatter.
8
-
9
7
  ## Installation
10
8
 
11
9
  ``` shell
@@ -14,7 +12,16 @@ pip install django-log-formatter-asim
14
12
 
15
13
  ## Usage
16
14
 
17
- Using in a Django logging configuration:
15
+ This package provides the following ASIM functionality:
16
+
17
+ - A Python [logging.Formatter] implementation.
18
+ - A module of functions `django_log_formatter_asim.events` which generate ASIM event log entries.
19
+
20
+ [logging.Formatter]: https://docs.python.org/3/library/logging.html#formatter-objects
21
+
22
+ ### `logging.Formatter` setup
23
+
24
+ Using the formatter in a Django logging configuration:
18
25
 
19
26
  ``` python
20
27
  from django_log_formatter_asim import ASIMFormatter
@@ -45,15 +52,61 @@ LOGGING = {
45
52
  },
46
53
  }
47
54
  ```
48
- 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.
49
55
 
50
- ## Dependencies
56
+ In this example we assign the ASIM formatter to a `handler` and ensure both `root` and `django` loggers use this `handler`.
57
+ We then set `propagate` to `False` on the `django` logger, to avoid duplicating logs at the root level.
51
58
 
52
- This package uses [Django IPware](https://github.com/un33k/django-ipware) for IP address capture.
59
+ ### ASIM Events
53
60
 
54
- This package is compatible with [Django User Agents](https://pypi.org/project/django-user-agents) which, when used, will enhance logged user agent information.
61
+ The events mostly follow the Microsoft schema but have been tailored to Department of Business and Trade needs.
62
+
63
+ Events are designed for simple integrate into your Django app.
64
+ Each will take additional information from the [Django HttpRequest object][django-request].
65
+
66
+ [django-request]: https://docs.djangoproject.com/en/5.2/ref/request-response/#httprequest-objects
67
+
68
+ #### Authentication event
55
69
 
56
- ## Settings
70
+ Following the [ASIM Authentication Schema](https://learn.microsoft.com/en-us/azure/sentinel/normalization-schema-authentication).
71
+
72
+ ```python
73
+ # Example usage
74
+ from django_log_formatter_asim.events import log_authentication
75
+
76
+ log_authentication(
77
+ request,
78
+ event=log_authentication.Event.Logoff,
79
+ result=log_authentication.Result.Success,
80
+ login_method=log_authentication.LoginMethod.UsernamePassword,
81
+ )
82
+
83
+ # Example JSON printed to standard output
84
+ {
85
+ # Values provided as arguments
86
+ "EventType": "Logoff",
87
+ "EventResult": "Success",
88
+ "LogonMethod": "Username & Password",
89
+
90
+ # Calculated / Hard coded fields
91
+ "EventStartTime": "2025-07-02T08:15:20+00:00",
92
+ "EventSeverity": "Informational",
93
+ "EventOriginalType": "001c",
94
+ "EventSchema": "Authentication",
95
+ "EventSchemaVersion": "0.1.4",
96
+
97
+ # Taken from Django HttpRequest object
98
+ "HttpHost": "WebServer.local",
99
+ "SrcIpAddr": "192.168.1.101",
100
+ "TargetUrl": "https://WebServer.local/steel",
101
+ "TargetSessionId": "def456",
102
+ "TargetUsername": "Adrian"
103
+
104
+ # Taken from DBT Platform environment variables
105
+ "TargetAppName": "export-analytics-frontend",
106
+ }
107
+ ```
108
+
109
+ ### Settings
57
110
 
58
111
  `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.
59
112
 
@@ -68,7 +121,7 @@ if is_copilot():
68
121
  DLFA_TRACE_HEADERS = ("X-B3-TraceId", "X-B3-SpanId")
69
122
  ```
70
123
 
71
- ## Formatter classes
124
+ ### Formatter classes
72
125
 
73
126
  ``` python
74
127
  ASIM_FORMATTERS = {
@@ -83,7 +136,7 @@ The default class for other loggers is:
83
136
  ASIMSystemFormatter
84
137
  ```
85
138
 
86
- ## Creating a custom formatter
139
+ ### Creating a custom `logging.Formatter`
87
140
 
88
141
  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:
89
142
 
@@ -97,6 +150,12 @@ If you wish to create your own ASIM formatter, you can inherit from ASIMSystemFo
97
150
  return logger_event
98
151
  ```
99
152
 
153
+ ## Dependencies
154
+
155
+ This package uses [Django IPware](https://github.com/un33k/django-ipware) for IP address capture.
156
+
157
+ This package is compatible with [Django User Agents](https://pypi.org/project/django-user-agents) which, when used, will enhance logged user agent information.
158
+
100
159
  ## Contributing to the `django-log-formatter-asim` package
101
160
 
102
161
  ### Getting started
@@ -2,6 +2,7 @@ import datetime
2
2
  import json
3
3
  import os
4
4
  from enum import Enum
5
+ from hashlib import sha3_512
5
6
  from typing import Literal
6
7
  from typing import Optional
7
8
  from typing import TypedDict
@@ -30,7 +31,7 @@ class AuthenticationLoginMethod(str, Enum):
30
31
  ExternalIDP = "External IdP"
31
32
 
32
33
 
33
- class AuthenticationUser(TypedDict):
34
+ class AuthenticationUser(TypedDict, total=False):
34
35
  """Dictionary to represent properties of the users session."""
35
36
 
36
37
  """What type of role best describes this Authentication event."""
@@ -109,17 +110,37 @@ def log_authentication(
109
110
 
110
111
  See also: https://learn.microsoft.com/en-us/azure/sentinel/normalization-schema-authentication
111
112
  """
112
- if user == None:
113
- user = {}
114
- if server == None:
115
- server = {}
116
- if client == None:
117
- client = {}
118
-
119
- event_created = time_generated or datetime.datetime.now(tz=datetime.timezone.utc)
120
113
 
114
+ _log_authentication(
115
+ request,
116
+ event,
117
+ result,
118
+ login_method,
119
+ user={} if user == None else user,
120
+ server={} if server == None else server,
121
+ client={} if client == None else client,
122
+ event_created=time_generated or datetime.datetime.now(tz=datetime.timezone.utc),
123
+ severity=severity,
124
+ result_details=result_details,
125
+ message=message,
126
+ )
127
+
128
+
129
+ def _log_authentication(
130
+ request: HttpRequest,
131
+ event: AuthenticationEvent,
132
+ result: Result,
133
+ login_method: AuthenticationLoginMethod,
134
+ user: AuthenticationUser,
135
+ server: Server,
136
+ client: Client,
137
+ event_created: datetime.datetime,
138
+ severity: Optional[Severity] = None,
139
+ result_details: Optional[str] = None,
140
+ message: Optional[str] = None,
141
+ ):
121
142
  log = {
122
- "EventCreated": event_created.isoformat(), # TODO: Should this really be EventCreated, or TimeGenerated
143
+ "EventStartTime": event_created.isoformat(),
123
144
  "EventSeverity": severity or _default_severity(result),
124
145
  "EventOriginalType": _event_code(event, result),
125
146
  "EventType": event,
@@ -141,7 +162,7 @@ def log_authentication(
141
162
  log["TargetAppName"] = app_name
142
163
 
143
164
  if container_id := _get_container_id():
144
- log["ContainerId"] = container_id
165
+ log["TargetContainerId"] = container_id
145
166
 
146
167
  if "ip_address" in client:
147
168
  log["SrcIpAddr"] = client["ip_address"]
@@ -157,9 +178,9 @@ def log_authentication(
157
178
  log["TargetUserType"] = user["role"]
158
179
 
159
180
  if "sessionId" in user:
160
- log["TargetSessionId"] = user["sessionId"]
161
- elif request.session.session_key:
162
- log["TargetSessionId"] = request.session.session_key
181
+ log["TargetSessionId"] = _cryptographically_hash(user["sessionId"])
182
+ elif hasattr(request, "session") and request.session.session_key:
183
+ log["TargetSessionId"] = _cryptographically_hash(request.session.session_key)
163
184
 
164
185
  if "username" in user:
165
186
  log["TargetUsername"] = user["username"]
@@ -184,6 +205,12 @@ log_authentication.LoginMethod = AuthenticationLoginMethod
184
205
  log_authentication.Severity = Severity
185
206
 
186
207
 
208
+ def _cryptographically_hash(data: Optional[str]) -> Optional[str]:
209
+ if data is None:
210
+ return None
211
+ return sha3_512(data.encode("UTF-8")).hexdigest()
212
+
213
+
187
214
  def _event_code(event: AuthenticationEvent, result: Result) -> str:
188
215
  if event == AuthenticationEvent.Logon:
189
216
  if result == Result.Success:
@@ -19,7 +19,7 @@ class Severity(str, Enum):
19
19
  High = "High"
20
20
 
21
21
 
22
- class Client(TypedDict):
22
+ class Client(TypedDict, total=False):
23
23
  """Dictionary to represent properties of the HTTP Client."""
24
24
 
25
25
  """Internet Protocol Address of the client making the Authentication
@@ -29,7 +29,7 @@ class Client(TypedDict):
29
29
  requested_url: Optional[str]
30
30
 
31
31
 
32
- class Server(TypedDict):
32
+ class Server(TypedDict, total=False):
33
33
  """Dictionary to represent properties of the HTTP Server."""
34
34
 
35
35
  """
@@ -31,24 +31,30 @@ class FileActivityEvent(str, Enum):
31
31
  FolderModified = "FolderModified"
32
32
 
33
33
 
34
- class FileActivityFile(TypedDict):
35
- """Dictionary to represent properties of the target file."""
34
+ class FileActivityFileBase(TypedDict):
35
+ """Mandatory field definitions of FileActivityFile."""
36
36
 
37
37
  """
38
- The full, normalized path of the target file, including the folder or location,
39
- the file name, and the extension.
38
+ The full, normalized path of the target file, including the folder or
39
+ location, the file name, and the extension.
40
40
  """
41
41
  path: str
42
+
43
+
44
+ class FileActivityFile(FileActivityFileBase, total=False):
45
+ """Dictionary to represent properties of either the target or source
46
+ file."""
47
+
42
48
  """
43
49
  The name of the target file, without a path or a location, but with an
44
50
  extension if available. This field should be similar to the final element in
45
- the TargetFilePath field.
51
+ the *FilePath field.
46
52
 
47
53
  Defaults to extracting the name based off the path if not provided.
48
54
  """
49
55
  name: Optional[str]
50
56
  """
51
- The target file extension.
57
+ The file extension.
52
58
 
53
59
  Defaults to extracting the extension based off the path if not provided.
54
60
  """
@@ -59,13 +65,13 @@ class FileActivityFile(TypedDict):
59
65
  Allowed values are listed in the IANA Media Types repository.
60
66
  """
61
67
  content_type: Optional[str]
62
- """The SHA256 value of the target file."""
68
+ """The SHA256 value of the file."""
63
69
  sha256: Optional[str]
64
- """The size of the target file in bytes."""
70
+ """The size of the file in bytes."""
65
71
  size: Optional[int]
66
72
 
67
73
 
68
- class FileActivityUser(TypedDict):
74
+ class FileActivityUser(TypedDict, total=False):
69
75
  """
70
76
  A unique identifier for the user.
71
77
 
@@ -80,6 +86,7 @@ def log_file_activity(
80
86
  event: FileActivityEvent,
81
87
  result: Result,
82
88
  file: FileActivityFile,
89
+ source_file: Optional[FileActivityFile] = None,
83
90
  user: Optional[FileActivityUser] = None,
84
91
  server: Optional[Server] = None,
85
92
  client: Optional[Client] = None,
@@ -111,7 +118,10 @@ def log_file_activity(
111
118
  - FolderModified
112
119
  :param result: What outcome did the action have, either "Success", "Failure", "Partial", "NA"
113
120
  :param file: Dictionary containing information on the target of this File event see
114
- FileActivityFile for more details.
121
+ FileActivityFile for more details.
122
+ :param source_file: Dictionary containing information on the source of this File event,
123
+ this MUST be used for a FileRenamed, FileMoved, FileCopied, FolderMoved
124
+ operation. See FileActivityFile for more details.
115
125
  :param user: Dictionary containing information on the logged in users username.
116
126
  :param server: Dictionary containing information on the server servicing this File event
117
127
  see Server class for more details.
@@ -129,45 +139,49 @@ def log_file_activity(
129
139
 
130
140
  See also: https://learn.microsoft.com/en-us/azure/sentinel/normalization-schema-file-event
131
141
  """
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
142
 
143
+ _log_file_activity(
144
+ request,
145
+ event,
146
+ result,
147
+ file,
148
+ source_file,
149
+ user={} if user == None else user,
150
+ server={} if server == None else server,
151
+ client={} if client == None else client,
152
+ event_created=time_generated or datetime.datetime.now(tz=datetime.timezone.utc),
153
+ severity=severity,
154
+ result_details=result_details,
155
+ message=message,
156
+ )
157
+
158
+
159
+ def _log_file_activity(
160
+ request: HttpRequest,
161
+ event: FileActivityEvent,
162
+ result: Result,
163
+ file: FileActivityFile,
164
+ source_file: Optional[FileActivityFile],
165
+ user: FileActivityUser,
166
+ server: Server,
167
+ client: Client,
168
+ event_created: datetime.datetime,
169
+ severity: Optional[Severity] = None,
170
+ result_details: Optional[str] = None,
171
+ message: Optional[str] = None,
172
+ ):
141
173
  log = {
142
174
  "EventSchema": "FileEvent",
143
175
  "EventSchemaVersion": "0.2.1",
144
176
  "EventType": event,
145
177
  "EventResult": result,
146
- "EventCreated": event_created.isoformat(), # TODO: Should this really be EventCreated, or TimeGenerated
178
+ "EventStartTime": event_created.isoformat(),
147
179
  "EventSeverity": severity or _default_severity(result),
148
- "TargetFilePath": file["path"],
149
180
  }
150
181
 
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"]
182
+ log.update(_generate_file_attributes(file, "Target"))
183
+ if source_file:
184
+ log.update(_generate_file_attributes(source_file, "Src"))
171
185
 
172
186
  if "domain_name" in server:
173
187
  log["HttpHost"] = server["domain_name"]
@@ -181,7 +195,7 @@ def log_file_activity(
181
195
  log["TargetAppName"] = app_name
182
196
 
183
197
  if container_id := _get_container_id():
184
- log["ContainerId"] = container_id
198
+ log["TargetContainerId"] = container_id
185
199
 
186
200
  if "ip_address" in client:
187
201
  log["SrcIpAddr"] = client["ip_address"]
@@ -210,6 +224,33 @@ def log_file_activity(
210
224
  print(json.dumps(log), flush=True)
211
225
 
212
226
 
227
+ def _generate_file_attributes(file: FileActivityFile, prefix: str) -> dict:
228
+ log = {prefix + "FilePath": file["path"]}
229
+
230
+ if "name" in file:
231
+ log[prefix + "FileName"] = file["name"]
232
+ else:
233
+ log[prefix + "FileName"] = os.path.basename(file["path"])
234
+
235
+ if "extension" in file:
236
+ log[prefix + "FileExtension"] = file["extension"]
237
+ else:
238
+ file_name_parts = list(filter(None, log[prefix + "FileName"].split(".", 1)))
239
+ if len(file_name_parts) > 1:
240
+ log[prefix + "FileExtension"] = file_name_parts[1]
241
+
242
+ if "content_type" in file:
243
+ log[prefix + "FileMimeType"] = file["content_type"]
244
+
245
+ if "sha256" in file:
246
+ log[prefix + "FileSHA256"] = file["sha256"]
247
+
248
+ if "size" in file:
249
+ log[prefix + "FileSize"] = file["size"]
250
+
251
+ return log
252
+
253
+
213
254
  log_file_activity.Event = FileActivityEvent
214
255
  log_file_activity.Result = Result
215
256
  log_file_activity.Severity = Severity
@@ -3,7 +3,7 @@ line-length = 100
3
3
 
4
4
  [tool.poetry]
5
5
  name = "django-log-formatter-asim"
6
- version = "1.1.0a4"
6
+ version = "1.2.0a0"
7
7
  description = "Formats Django logs in ASIM format."
8
8
  authors = ["Department for Business and Trade Platform Team <sre-team@digital.trade.gov.uk>"]
9
9
  license = "MIT"