django-log-formatter-asim 1.1.0a3__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.
- {django_log_formatter_asim-1.1.0a3 → django_log_formatter_asim-1.2.0a0}/PKG-INFO +70 -11
- {django_log_formatter_asim-1.1.0a3 → django_log_formatter_asim-1.2.0a0}/README.md +69 -10
- {django_log_formatter_asim-1.1.0a3 → django_log_formatter_asim-1.2.0a0}/django_log_formatter_asim/__init__.py +3 -15
- django_log_formatter_asim-1.2.0a0/django_log_formatter_asim/ecs.py +16 -0
- {django_log_formatter_asim-1.1.0a3 → django_log_formatter_asim-1.2.0a0}/django_log_formatter_asim/events/authentication.py +57 -29
- {django_log_formatter_asim-1.1.0a3 → django_log_formatter_asim-1.2.0a0}/django_log_formatter_asim/events/common.py +13 -4
- {django_log_formatter_asim-1.1.0a3 → django_log_formatter_asim-1.2.0a0}/django_log_formatter_asim/events/file_activity.py +96 -44
- {django_log_formatter_asim-1.1.0a3 → django_log_formatter_asim-1.2.0a0}/pyproject.toml +1 -1
- {django_log_formatter_asim-1.1.0a3 → django_log_formatter_asim-1.2.0a0}/LICENSE +0 -0
- {django_log_formatter_asim-1.1.0a3 → django_log_formatter_asim-1.2.0a0}/django_log_formatter_asim/events/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: django-log-formatter-asim
|
|
3
|
-
Version: 1.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
80
|
+
### ASIM Events
|
|
74
81
|
|
|
75
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
59
|
+
### ASIM Events
|
|
53
60
|
|
|
54
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import logging
|
|
3
|
-
import os
|
|
4
3
|
from datetime import datetime
|
|
5
4
|
from datetime import timezone
|
|
6
5
|
from importlib.metadata import distribution
|
|
@@ -9,6 +8,8 @@ import ddtrace
|
|
|
9
8
|
from ddtrace.trace import tracer
|
|
10
9
|
from django.conf import settings
|
|
11
10
|
|
|
11
|
+
from .ecs import _get_container_id
|
|
12
|
+
|
|
12
13
|
|
|
13
14
|
class ASIMRootFormatter:
|
|
14
15
|
def __init__(self, record):
|
|
@@ -29,19 +30,6 @@ class ASIMRootFormatter:
|
|
|
29
30
|
|
|
30
31
|
return copied_dict
|
|
31
32
|
|
|
32
|
-
def _get_container_id(self):
|
|
33
|
-
"""
|
|
34
|
-
The dockerId (container Id) is available via the metadata endpoint. However, it looks like it is embedded in the
|
|
35
|
-
metadata URL e.g.:
|
|
36
|
-
ECS_CONTAINER_METADATA_URI=http://169.254.170.2/v3/709d1c10779d47b2a84db9eef2ebd041-0265927825
|
|
37
|
-
See: https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-metadata-endpoint-v4-response.html
|
|
38
|
-
"""
|
|
39
|
-
|
|
40
|
-
try:
|
|
41
|
-
return os.environ["ECS_CONTAINER_METADATA_URI"].split("/")[-1]
|
|
42
|
-
except (KeyError, IndexError):
|
|
43
|
-
return ""
|
|
44
|
-
|
|
45
33
|
def _get_first_64_bits_of(self, trace_id):
|
|
46
34
|
# See https://docs.datadoghq.com/tracing/other_telemetry/connect_logs_and_traces/python/#no-standard-library-logging
|
|
47
35
|
return str((1 << 64) - 1 & trace_id)
|
|
@@ -65,7 +53,7 @@ class ASIMRootFormatter:
|
|
|
65
53
|
event_dict["service"] = ddtrace.config.service or ""
|
|
66
54
|
event_dict["version"] = ddtrace.config.version or ""
|
|
67
55
|
|
|
68
|
-
event_dict["container_id"] =
|
|
56
|
+
event_dict["container_id"] = _get_container_id()
|
|
69
57
|
|
|
70
58
|
return event_dict
|
|
71
59
|
|
|
@@ -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 ""
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import datetime
|
|
2
2
|
import json
|
|
3
|
+
import os
|
|
3
4
|
from enum import Enum
|
|
5
|
+
from hashlib import sha3_512
|
|
4
6
|
from typing import Literal
|
|
5
7
|
from typing import Optional
|
|
6
8
|
from typing import TypedDict
|
|
7
9
|
|
|
8
10
|
from django.http import HttpRequest
|
|
9
11
|
|
|
12
|
+
from django_log_formatter_asim.ecs import _get_container_id
|
|
13
|
+
|
|
10
14
|
from .common import Client
|
|
11
15
|
from .common import Result
|
|
12
16
|
from .common import Server
|
|
@@ -27,7 +31,7 @@ class AuthenticationLoginMethod(str, Enum):
|
|
|
27
31
|
ExternalIDP = "External IdP"
|
|
28
32
|
|
|
29
33
|
|
|
30
|
-
class AuthenticationUser(TypedDict):
|
|
34
|
+
class AuthenticationUser(TypedDict, total=False):
|
|
31
35
|
"""Dictionary to represent properties of the users session."""
|
|
32
36
|
|
|
33
37
|
"""What type of role best describes this Authentication event."""
|
|
@@ -51,12 +55,6 @@ class AuthenticationUser(TypedDict):
|
|
|
51
55
|
"""
|
|
52
56
|
username: Optional[str]
|
|
53
57
|
"""
|
|
54
|
-
Email address for the user if one exists.
|
|
55
|
-
|
|
56
|
-
Defaults to the logged in Django User.email if not provided.
|
|
57
|
-
"""
|
|
58
|
-
email: Optional[str]
|
|
59
|
-
"""
|
|
60
58
|
A unique identifier for this authentication session if one exists.
|
|
61
59
|
|
|
62
60
|
Defaults to the Django Sessions session key if not provided.
|
|
@@ -86,7 +84,7 @@ def log_authentication(
|
|
|
86
84
|
- Django Session middlewares Session Key
|
|
87
85
|
- Client IP address
|
|
88
86
|
- URL requested by the client
|
|
89
|
-
- Server
|
|
87
|
+
- Server domain name
|
|
90
88
|
:param event: What authentication action was attempted, either "Logon" or "Logoff"
|
|
91
89
|
:param result: What outcome did the action have, either "Success", "Failure", "Partial", "NA"
|
|
92
90
|
:param login_method: What authentication mechanism was being used, one of:
|
|
@@ -112,17 +110,37 @@ def log_authentication(
|
|
|
112
110
|
|
|
113
111
|
See also: https://learn.microsoft.com/en-us/azure/sentinel/normalization-schema-authentication
|
|
114
112
|
"""
|
|
115
|
-
if user == None:
|
|
116
|
-
user = {}
|
|
117
|
-
if server == None:
|
|
118
|
-
server = {}
|
|
119
|
-
if client == None:
|
|
120
|
-
client = {}
|
|
121
|
-
|
|
122
|
-
event_created = time_generated or datetime.datetime.now(tz=datetime.timezone.utc)
|
|
123
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
|
+
):
|
|
124
142
|
log = {
|
|
125
|
-
"
|
|
143
|
+
"EventStartTime": event_created.isoformat(),
|
|
126
144
|
"EventSeverity": severity or _default_severity(result),
|
|
127
145
|
"EventOriginalType": _event_code(event, result),
|
|
128
146
|
"EventType": event,
|
|
@@ -132,10 +150,19 @@ def log_authentication(
|
|
|
132
150
|
"EventSchemaVersion": "0.1.4",
|
|
133
151
|
}
|
|
134
152
|
|
|
135
|
-
if "
|
|
136
|
-
log["
|
|
153
|
+
if "domain_name" in server:
|
|
154
|
+
log["HttpHost"] = server["domain_name"]
|
|
137
155
|
elif "HTTP_HOST" in request.META:
|
|
138
|
-
log["
|
|
156
|
+
log["HttpHost"] = request.get_host()
|
|
157
|
+
|
|
158
|
+
if "service_name" in server:
|
|
159
|
+
log["TargetAppName"] = server["service_name"]
|
|
160
|
+
elif os.environ.get("COPILOT_APPLICATION_NAME") and os.environ.get("COPILOT_SERVICE_NAME"):
|
|
161
|
+
app_name = f"{os.environ['COPILOT_APPLICATION_NAME']}-{os.environ['COPILOT_SERVICE_NAME']}"
|
|
162
|
+
log["TargetAppName"] = app_name
|
|
163
|
+
|
|
164
|
+
if container_id := _get_container_id():
|
|
165
|
+
log["TargetContainerId"] = container_id
|
|
139
166
|
|
|
140
167
|
if "ip_address" in client:
|
|
141
168
|
log["SrcIpAddr"] = client["ip_address"]
|
|
@@ -148,23 +175,18 @@ def log_authentication(
|
|
|
148
175
|
log["TargetUrl"] = request.scheme + "://" + request.get_host() + request.get_full_path()
|
|
149
176
|
|
|
150
177
|
if "role" in user:
|
|
151
|
-
log["
|
|
178
|
+
log["TargetUserType"] = user["role"]
|
|
152
179
|
|
|
153
180
|
if "sessionId" in user:
|
|
154
|
-
log["
|
|
155
|
-
elif request.session.session_key:
|
|
156
|
-
log["
|
|
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)
|
|
157
184
|
|
|
158
185
|
if "username" in user:
|
|
159
186
|
log["TargetUsername"] = user["username"]
|
|
160
187
|
elif hasattr(request, "user") and request.user.username:
|
|
161
188
|
log["TargetUsername"] = request.user.username
|
|
162
189
|
|
|
163
|
-
if "email" in user:
|
|
164
|
-
log["ActorUsername"] = user["email"]
|
|
165
|
-
elif hasattr(request, "user") and hasattr(request.user, "email") and request.user.email:
|
|
166
|
-
log["ActorUsername"] = request.user.email
|
|
167
|
-
|
|
168
190
|
if result_details:
|
|
169
191
|
log["EventResultDetails"] = result_details
|
|
170
192
|
|
|
@@ -183,6 +205,12 @@ log_authentication.LoginMethod = AuthenticationLoginMethod
|
|
|
183
205
|
log_authentication.Severity = Severity
|
|
184
206
|
|
|
185
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
|
+
|
|
186
214
|
def _event_code(event: AuthenticationEvent, result: Result) -> str:
|
|
187
215
|
if event == AuthenticationEvent.Logon:
|
|
188
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,17 +29,26 @@ 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
|
"""
|
|
36
|
-
|
|
36
|
+
The FQDN that this server is listening to HTTP requests on. For example:
|
|
37
|
+
web.trade.gov.uk
|
|
37
38
|
|
|
38
39
|
Defaults to the WSGI HTTP_HOST field if not provided.
|
|
39
40
|
"""
|
|
40
|
-
|
|
41
|
+
domain_name: Optional[str]
|
|
41
42
|
"""Internet Protocol Address of the server serving this request."""
|
|
42
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]
|
|
43
52
|
|
|
44
53
|
|
|
45
54
|
def _default_severity(result: Result) -> Severity:
|
|
@@ -7,6 +7,8 @@ from typing import TypedDict
|
|
|
7
7
|
|
|
8
8
|
from django.http import HttpRequest
|
|
9
9
|
|
|
10
|
+
from django_log_formatter_asim.ecs import _get_container_id
|
|
11
|
+
|
|
10
12
|
from .common import Client
|
|
11
13
|
from .common import Result
|
|
12
14
|
from .common import Server
|
|
@@ -29,24 +31,30 @@ class FileActivityEvent(str, Enum):
|
|
|
29
31
|
FolderModified = "FolderModified"
|
|
30
32
|
|
|
31
33
|
|
|
32
|
-
class
|
|
33
|
-
"""
|
|
34
|
+
class FileActivityFileBase(TypedDict):
|
|
35
|
+
"""Mandatory field definitions of FileActivityFile."""
|
|
34
36
|
|
|
35
37
|
"""
|
|
36
|
-
The full, normalized path of the target file, including the folder or
|
|
37
|
-
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.
|
|
38
40
|
"""
|
|
39
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
|
+
|
|
40
48
|
"""
|
|
41
49
|
The name of the target file, without a path or a location, but with an
|
|
42
50
|
extension if available. This field should be similar to the final element in
|
|
43
|
-
the
|
|
51
|
+
the *FilePath field.
|
|
44
52
|
|
|
45
53
|
Defaults to extracting the name based off the path if not provided.
|
|
46
54
|
"""
|
|
47
55
|
name: Optional[str]
|
|
48
56
|
"""
|
|
49
|
-
The
|
|
57
|
+
The file extension.
|
|
50
58
|
|
|
51
59
|
Defaults to extracting the extension based off the path if not provided.
|
|
52
60
|
"""
|
|
@@ -57,13 +65,13 @@ class FileActivityFile(TypedDict):
|
|
|
57
65
|
Allowed values are listed in the IANA Media Types repository.
|
|
58
66
|
"""
|
|
59
67
|
content_type: Optional[str]
|
|
60
|
-
"""The SHA256 value of the
|
|
68
|
+
"""The SHA256 value of the file."""
|
|
61
69
|
sha256: Optional[str]
|
|
62
|
-
"""The size of the
|
|
70
|
+
"""The size of the file in bytes."""
|
|
63
71
|
size: Optional[int]
|
|
64
72
|
|
|
65
73
|
|
|
66
|
-
class FileActivityUser(TypedDict):
|
|
74
|
+
class FileActivityUser(TypedDict, total=False):
|
|
67
75
|
"""
|
|
68
76
|
A unique identifier for the user.
|
|
69
77
|
|
|
@@ -78,6 +86,7 @@ def log_file_activity(
|
|
|
78
86
|
event: FileActivityEvent,
|
|
79
87
|
result: Result,
|
|
80
88
|
file: FileActivityFile,
|
|
89
|
+
source_file: Optional[FileActivityFile] = None,
|
|
81
90
|
user: Optional[FileActivityUser] = None,
|
|
82
91
|
server: Optional[Server] = None,
|
|
83
92
|
client: Optional[Client] = None,
|
|
@@ -94,7 +103,7 @@ def log_file_activity(
|
|
|
94
103
|
- Django Authentication systems current username
|
|
95
104
|
- Client IP address
|
|
96
105
|
- URL requested by the client
|
|
97
|
-
- Server
|
|
106
|
+
- Server domain name
|
|
98
107
|
:param event: What File Event action was attempted, one of:
|
|
99
108
|
- FileAccessed
|
|
100
109
|
- FileCreated
|
|
@@ -109,7 +118,10 @@ def log_file_activity(
|
|
|
109
118
|
- FolderModified
|
|
110
119
|
:param result: What outcome did the action have, either "Success", "Failure", "Partial", "NA"
|
|
111
120
|
:param file: Dictionary containing information on the target of this File event see
|
|
112
|
-
|
|
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.
|
|
113
125
|
:param user: Dictionary containing information on the logged in users username.
|
|
114
126
|
:param server: Dictionary containing information on the server servicing this File event
|
|
115
127
|
see Server class for more details.
|
|
@@ -127,50 +139,63 @@ def log_file_activity(
|
|
|
127
139
|
|
|
128
140
|
See also: https://learn.microsoft.com/en-us/azure/sentinel/normalization-schema-file-event
|
|
129
141
|
"""
|
|
130
|
-
if user == None:
|
|
131
|
-
user = {}
|
|
132
|
-
if server == None:
|
|
133
|
-
server = {}
|
|
134
|
-
if client == None:
|
|
135
|
-
client = {}
|
|
136
|
-
|
|
137
|
-
event_created = time_generated or datetime.datetime.now(tz=datetime.timezone.utc)
|
|
138
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
|
+
):
|
|
139
173
|
log = {
|
|
140
174
|
"EventSchema": "FileEvent",
|
|
141
175
|
"EventSchemaVersion": "0.2.1",
|
|
142
176
|
"EventType": event,
|
|
143
177
|
"EventResult": result,
|
|
144
|
-
"
|
|
178
|
+
"EventStartTime": event_created.isoformat(),
|
|
145
179
|
"EventSeverity": severity or _default_severity(result),
|
|
146
|
-
"TargetFilePath": file["path"],
|
|
147
180
|
}
|
|
148
181
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
log["TargetFileName"] = os.path.basename(file["path"])
|
|
153
|
-
|
|
154
|
-
if "extension" in file:
|
|
155
|
-
log["TargetFileExtension"] = file["extension"]
|
|
156
|
-
else:
|
|
157
|
-
file_name_parts = list(filter(None, log["TargetFileName"].split(".", 1)))
|
|
158
|
-
if len(file_name_parts) > 1:
|
|
159
|
-
log["TargetFileExtension"] = file_name_parts[1]
|
|
160
|
-
|
|
161
|
-
if "content_type" in file:
|
|
162
|
-
log["TargetFileMimeType"] = file["content_type"]
|
|
182
|
+
log.update(_generate_file_attributes(file, "Target"))
|
|
183
|
+
if source_file:
|
|
184
|
+
log.update(_generate_file_attributes(source_file, "Src"))
|
|
163
185
|
|
|
164
|
-
if "
|
|
165
|
-
log["
|
|
186
|
+
if "domain_name" in server:
|
|
187
|
+
log["HttpHost"] = server["domain_name"]
|
|
188
|
+
elif "HTTP_HOST" in request.META:
|
|
189
|
+
log["HttpHost"] = request.get_host()
|
|
166
190
|
|
|
167
|
-
if "
|
|
168
|
-
log["
|
|
191
|
+
if "service_name" in server:
|
|
192
|
+
log["TargetAppName"] = server["service_name"]
|
|
193
|
+
elif os.environ.get("COPILOT_APPLICATION_NAME") and os.environ.get("COPILOT_SERVICE_NAME"):
|
|
194
|
+
app_name = f"{os.environ['COPILOT_APPLICATION_NAME']}-{os.environ['COPILOT_SERVICE_NAME']}"
|
|
195
|
+
log["TargetAppName"] = app_name
|
|
169
196
|
|
|
170
|
-
if
|
|
171
|
-
log["
|
|
172
|
-
elif "HTTP_HOST" in request.META:
|
|
173
|
-
log["DvcHostname"] = request.get_host()
|
|
197
|
+
if container_id := _get_container_id():
|
|
198
|
+
log["TargetContainerId"] = container_id
|
|
174
199
|
|
|
175
200
|
if "ip_address" in client:
|
|
176
201
|
log["SrcIpAddr"] = client["ip_address"]
|
|
@@ -184,7 +209,7 @@ def log_file_activity(
|
|
|
184
209
|
|
|
185
210
|
if "username" in user:
|
|
186
211
|
log["TargetUsername"] = user["username"]
|
|
187
|
-
elif request.user.username:
|
|
212
|
+
elif hasattr(request, "user") and request.user.username:
|
|
188
213
|
log["TargetUsername"] = request.user.username
|
|
189
214
|
|
|
190
215
|
if result_details:
|
|
@@ -199,6 +224,33 @@ def log_file_activity(
|
|
|
199
224
|
print(json.dumps(log), flush=True)
|
|
200
225
|
|
|
201
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
|
+
|
|
202
254
|
log_file_activity.Event = FileActivityEvent
|
|
203
255
|
log_file_activity.Result = Result
|
|
204
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.
|
|
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"
|
|
File without changes
|
|
File without changes
|