django-log-formatter-asim 1.2.0a0__py3-none-any.whl → 1.2.0a1__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.
- django_log_formatter_asim/events/__init__.py +7 -2
- django_log_formatter_asim/events/account_management.py +133 -0
- django_log_formatter_asim/events/authentication.py +131 -166
- django_log_formatter_asim/events/common.py +78 -10
- django_log_formatter_asim/events/file_activity.py +145 -187
- {django_log_formatter_asim-1.2.0a0.dist-info → django_log_formatter_asim-1.2.0a1.dist-info}/METADATA +194 -30
- django_log_formatter_asim-1.2.0a1.dist-info/RECORD +11 -0
- {django_log_formatter_asim-1.2.0a0.dist-info → django_log_formatter_asim-1.2.0a1.dist-info}/WHEEL +1 -1
- django_log_formatter_asim-1.2.0a0.dist-info/RECORD +0 -10
- {django_log_formatter_asim-1.2.0a0.dist-info → django_log_formatter_asim-1.2.0a1.dist-info/licenses}/LICENSE +0 -0
|
@@ -1,2 +1,7 @@
|
|
|
1
|
-
from .
|
|
2
|
-
from .
|
|
1
|
+
from .account_management import LogAccountManagement
|
|
2
|
+
from .authentication import LogAuthentication
|
|
3
|
+
from .file_activity import LogFileActivity
|
|
4
|
+
|
|
5
|
+
log_account_management = LogAccountManagement()
|
|
6
|
+
log_authentication = LogAuthentication()
|
|
7
|
+
log_file_activity = LogFileActivity()
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import json
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from typing import Optional
|
|
5
|
+
from typing import TypedDict
|
|
6
|
+
|
|
7
|
+
from django.http import HttpRequest
|
|
8
|
+
|
|
9
|
+
from .common import Activity
|
|
10
|
+
from .common import Client
|
|
11
|
+
from .common import LoggedInUser
|
|
12
|
+
from .common import Result
|
|
13
|
+
from .common import Server
|
|
14
|
+
from .common import Severity
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class FileActivityEvent(str, Enum):
|
|
18
|
+
UserCreated = "UserCreated"
|
|
19
|
+
UserDeleted = "UserDeleted"
|
|
20
|
+
UserModified = "UserModified"
|
|
21
|
+
UserLocked = "UserLocked"
|
|
22
|
+
UserUnlocked = "UserUnlocked"
|
|
23
|
+
UserDisabled = "UserDisabled"
|
|
24
|
+
UserEnabled = "UserEnabled"
|
|
25
|
+
PasswordChanged = "PasswordChanged"
|
|
26
|
+
PasswordReset = "PasswordReset"
|
|
27
|
+
GroupCreated = "GroupCreated"
|
|
28
|
+
GroupDeleted = "GroupDeleted"
|
|
29
|
+
GroupModified = "GroupModified"
|
|
30
|
+
UserAddedToGroup = "UserAddedToGroup"
|
|
31
|
+
UserRemovedFromGroup = "UserRemovedFromGroup"
|
|
32
|
+
GroupEnumerated = "GroupEnumerated"
|
|
33
|
+
UserRead = "UserRead"
|
|
34
|
+
GroupRead = "GroupRead"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Account(TypedDict, total=False):
|
|
38
|
+
"""Dictionary to represent details of the account management event."""
|
|
39
|
+
|
|
40
|
+
"""
|
|
41
|
+
If a user was managed, the username of that user
|
|
42
|
+
"""
|
|
43
|
+
username: Optional[str]
|
|
44
|
+
"""If a group was managed, the name of the group."""
|
|
45
|
+
group: Optional[str]
|
|
46
|
+
"""
|
|
47
|
+
If the Account Management event is one of the following.
|
|
48
|
+
|
|
49
|
+
- UserModified
|
|
50
|
+
- GroupModified
|
|
51
|
+
|
|
52
|
+
Details of the property which was changed, in the form:
|
|
53
|
+
("propertyName", "oldValue", "newValue")
|
|
54
|
+
"""
|
|
55
|
+
changed: tuple[str, str, str]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class LogAccountManagement(Activity):
|
|
59
|
+
Event = FileActivityEvent
|
|
60
|
+
Result = Result
|
|
61
|
+
Severity = Severity
|
|
62
|
+
|
|
63
|
+
def __call__(
|
|
64
|
+
self,
|
|
65
|
+
request: HttpRequest,
|
|
66
|
+
event: Event,
|
|
67
|
+
account: Account,
|
|
68
|
+
result: Result,
|
|
69
|
+
user: Optional[LoggedInUser] = None,
|
|
70
|
+
server: Optional[Server] = None,
|
|
71
|
+
client: Optional[Client] = None,
|
|
72
|
+
severity: Optional[Severity] = None,
|
|
73
|
+
time_generated: Optional[datetime.datetime] = None,
|
|
74
|
+
result_details: Optional[str] = None,
|
|
75
|
+
message: Optional[str] = None,
|
|
76
|
+
):
|
|
77
|
+
self._log_account_management(
|
|
78
|
+
request,
|
|
79
|
+
event,
|
|
80
|
+
account,
|
|
81
|
+
result,
|
|
82
|
+
{} if user == None else user,
|
|
83
|
+
{} if server == None else server,
|
|
84
|
+
{} if client == None else client,
|
|
85
|
+
time_generated or datetime.datetime.now(tz=datetime.timezone.utc),
|
|
86
|
+
severity,
|
|
87
|
+
result_details,
|
|
88
|
+
message,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
def _log_account_management(
|
|
92
|
+
self,
|
|
93
|
+
request: HttpRequest,
|
|
94
|
+
event: Event,
|
|
95
|
+
account: Account,
|
|
96
|
+
result: Result,
|
|
97
|
+
user: LoggedInUser,
|
|
98
|
+
server: Server,
|
|
99
|
+
client: Client,
|
|
100
|
+
event_created: datetime.datetime,
|
|
101
|
+
severity: Optional[Severity] = None,
|
|
102
|
+
result_details: Optional[str] = None,
|
|
103
|
+
message: Optional[str] = None,
|
|
104
|
+
):
|
|
105
|
+
log = {
|
|
106
|
+
"EventSchema": "UserManagement",
|
|
107
|
+
"EventSchemaVersion": "0.1.1",
|
|
108
|
+
"EventType": event,
|
|
109
|
+
}
|
|
110
|
+
log.update(
|
|
111
|
+
self._activity_fields(
|
|
112
|
+
request, event_created, result, server, client, severity, result_details, message
|
|
113
|
+
)
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
if "username" in user:
|
|
117
|
+
log["ActorUsername"] = user["username"]
|
|
118
|
+
elif hasattr(request, "user") and request.user.username:
|
|
119
|
+
log["ActorUsername"] = request.user.username
|
|
120
|
+
|
|
121
|
+
if "username" in account:
|
|
122
|
+
log["TargetUsername"] = account["username"]
|
|
123
|
+
|
|
124
|
+
if "group" in account:
|
|
125
|
+
log["GroupName"] = account["group"]
|
|
126
|
+
|
|
127
|
+
if "changed" in account:
|
|
128
|
+
(propertyName, previousPropertyValue, newPropertyName) = account["changed"]
|
|
129
|
+
log["UpdatedPropertyName"] = propertyName
|
|
130
|
+
log["PreviousPropertyValue"] = previousPropertyValue
|
|
131
|
+
log["NewPropertyValue"] = newPropertyName
|
|
132
|
+
|
|
133
|
+
print(json.dumps(log), flush=True)
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import datetime
|
|
2
2
|
import json
|
|
3
|
-
import os
|
|
4
3
|
from enum import Enum
|
|
5
4
|
from hashlib import sha3_512
|
|
6
5
|
from typing import Literal
|
|
@@ -9,14 +8,11 @@ from typing import TypedDict
|
|
|
9
8
|
|
|
10
9
|
from django.http import HttpRequest
|
|
11
10
|
|
|
12
|
-
from
|
|
13
|
-
|
|
11
|
+
from .common import Activity
|
|
14
12
|
from .common import Client
|
|
15
13
|
from .common import Result
|
|
16
14
|
from .common import Server
|
|
17
15
|
from .common import Severity
|
|
18
|
-
from .common import _default_severity
|
|
19
|
-
from .common import _get_client_ip_address
|
|
20
16
|
|
|
21
17
|
|
|
22
18
|
class AuthenticationEvent(str, Enum):
|
|
@@ -62,164 +58,133 @@ class AuthenticationUser(TypedDict, total=False):
|
|
|
62
58
|
sessionId: Optional[str]
|
|
63
59
|
|
|
64
60
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
if "ip_address" in server:
|
|
197
|
-
log["DvcIpAddr"] = server["ip_address"]
|
|
198
|
-
|
|
199
|
-
print(json.dumps(log), flush=True)
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
log_authentication.Event = AuthenticationEvent
|
|
203
|
-
log_authentication.Result = Result
|
|
204
|
-
log_authentication.LoginMethod = AuthenticationLoginMethod
|
|
205
|
-
log_authentication.Severity = Severity
|
|
206
|
-
|
|
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
|
-
|
|
214
|
-
def _event_code(event: AuthenticationEvent, result: Result) -> str:
|
|
215
|
-
if event == AuthenticationEvent.Logon:
|
|
216
|
-
if result == Result.Success:
|
|
217
|
-
return "001a"
|
|
218
|
-
elif result == Result.Failure:
|
|
219
|
-
return "001b"
|
|
220
|
-
elif event == AuthenticationEvent.Logoff:
|
|
221
|
-
if result == Result.Success:
|
|
222
|
-
return "001c"
|
|
223
|
-
elif result == Result.Failure:
|
|
224
|
-
return "001d"
|
|
225
|
-
return "001"
|
|
61
|
+
class LogAuthentication(Activity):
|
|
62
|
+
Event = AuthenticationEvent
|
|
63
|
+
Result = Result
|
|
64
|
+
LoginMethod = AuthenticationLoginMethod
|
|
65
|
+
Severity = Severity
|
|
66
|
+
|
|
67
|
+
def __call__(
|
|
68
|
+
self,
|
|
69
|
+
request: HttpRequest,
|
|
70
|
+
event: AuthenticationEvent,
|
|
71
|
+
result: Result,
|
|
72
|
+
login_method: AuthenticationLoginMethod,
|
|
73
|
+
user: Optional[AuthenticationUser] = None,
|
|
74
|
+
server: Optional[Server] = None,
|
|
75
|
+
client: Optional[Client] = None,
|
|
76
|
+
severity: Optional[Severity] = None,
|
|
77
|
+
time_generated: Optional[datetime.datetime] = None,
|
|
78
|
+
result_details: Optional[str] = None,
|
|
79
|
+
message: Optional[str] = None,
|
|
80
|
+
):
|
|
81
|
+
"""
|
|
82
|
+
Log an ASIM Authentication Event to standard output.
|
|
83
|
+
|
|
84
|
+
:param request: django.http.HttpRequest object which initiated this Authentication request
|
|
85
|
+
from which the following data will be logged if available
|
|
86
|
+
- Django Authentication systems current username
|
|
87
|
+
- Django Session middlewares Session Key
|
|
88
|
+
- Client IP address
|
|
89
|
+
- URL requested by the client
|
|
90
|
+
- Server domain name
|
|
91
|
+
:param event: What authentication action was attempted, either "Logon" or "Logoff"
|
|
92
|
+
:param result: What outcome did the action have, either "Success", "Failure", "Partial", "NA"
|
|
93
|
+
:param login_method: What authentication mechanism was being used, one of:
|
|
94
|
+
- "Username & Password"
|
|
95
|
+
- "Staff-SSO"
|
|
96
|
+
- "UK.GOV-SSO"
|
|
97
|
+
- "External IdP"
|
|
98
|
+
:param user: Dictionary containing information on the subject of this Authentication event
|
|
99
|
+
see AuthenticationUser class for more details.
|
|
100
|
+
:param server: Dictionary containing information on the server servicing this Authentication event
|
|
101
|
+
see Server class for more details.
|
|
102
|
+
:param client: Dictionary containing information on the client performing this Authentication event
|
|
103
|
+
see Client class for more details.
|
|
104
|
+
:param severity: Optional severity of the event, defaults to "Informational", otherwise one of:
|
|
105
|
+
- "Informational"
|
|
106
|
+
- "Low"
|
|
107
|
+
- "Medium"
|
|
108
|
+
- "High"
|
|
109
|
+
:param time_generated: Optional datetime for when the event happened, otherwise datetime.now
|
|
110
|
+
:param result_details: Optional string describing any details associated with the events outcome.
|
|
111
|
+
This field is typically populated when the result is a failure.
|
|
112
|
+
:param message: Optional string describing the reason why the log was generated.
|
|
113
|
+
|
|
114
|
+
See also: https://learn.microsoft.com/en-us/azure/sentinel/normalization-schema-authentication
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
self._log_authentication(
|
|
118
|
+
request,
|
|
119
|
+
event,
|
|
120
|
+
result,
|
|
121
|
+
login_method,
|
|
122
|
+
user={} if user == None else user,
|
|
123
|
+
server={} if server == None else server,
|
|
124
|
+
client={} if client == None else client,
|
|
125
|
+
event_created=time_generated or datetime.datetime.now(tz=datetime.timezone.utc),
|
|
126
|
+
severity=severity,
|
|
127
|
+
result_details=result_details,
|
|
128
|
+
message=message,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
def _log_authentication(
|
|
132
|
+
self,
|
|
133
|
+
request: HttpRequest,
|
|
134
|
+
event: AuthenticationEvent,
|
|
135
|
+
result: Result,
|
|
136
|
+
login_method: AuthenticationLoginMethod,
|
|
137
|
+
user: AuthenticationUser,
|
|
138
|
+
server: Server,
|
|
139
|
+
client: Client,
|
|
140
|
+
event_created: datetime.datetime,
|
|
141
|
+
severity: Optional[Severity] = None,
|
|
142
|
+
result_details: Optional[str] = None,
|
|
143
|
+
message: Optional[str] = None,
|
|
144
|
+
):
|
|
145
|
+
log = {
|
|
146
|
+
"EventOriginalType": self._event_code(event, result),
|
|
147
|
+
"EventType": event,
|
|
148
|
+
"LogonMethod": login_method,
|
|
149
|
+
"EventSchema": "Authentication",
|
|
150
|
+
"EventSchemaVersion": "0.1.4",
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
log.update(
|
|
154
|
+
self._activity_fields(
|
|
155
|
+
request, event_created, result, server, client, severity, result_details, message
|
|
156
|
+
)
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
if "role" in user:
|
|
160
|
+
log["TargetUserType"] = user["role"]
|
|
161
|
+
|
|
162
|
+
if "sessionId" in user:
|
|
163
|
+
log["TargetSessionId"] = self._cryptographically_hash(user["sessionId"])
|
|
164
|
+
elif hasattr(request, "session") and request.session.session_key:
|
|
165
|
+
log["TargetSessionId"] = self._cryptographically_hash(request.session.session_key)
|
|
166
|
+
|
|
167
|
+
if "username" in user:
|
|
168
|
+
log["TargetUsername"] = user["username"]
|
|
169
|
+
elif hasattr(request, "user") and request.user.username:
|
|
170
|
+
log["TargetUsername"] = request.user.username
|
|
171
|
+
|
|
172
|
+
print(json.dumps(log), flush=True)
|
|
173
|
+
|
|
174
|
+
def _cryptographically_hash(self, data: Optional[str]) -> Optional[str]:
|
|
175
|
+
if data is None:
|
|
176
|
+
return None
|
|
177
|
+
return sha3_512(data.encode("UTF-8")).hexdigest()
|
|
178
|
+
|
|
179
|
+
def _event_code(self, event: AuthenticationEvent, result: Result) -> str:
|
|
180
|
+
if event == AuthenticationEvent.Logon:
|
|
181
|
+
if result == Result.Success:
|
|
182
|
+
return "001a"
|
|
183
|
+
elif result == Result.Failure:
|
|
184
|
+
return "001b"
|
|
185
|
+
elif event == AuthenticationEvent.Logoff:
|
|
186
|
+
if result == Result.Success:
|
|
187
|
+
return "001c"
|
|
188
|
+
elif result == Result.Failure:
|
|
189
|
+
return "001d"
|
|
190
|
+
return "001"
|
|
@@ -1,9 +1,23 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import os
|
|
1
3
|
from enum import Enum
|
|
2
4
|
from typing import Optional
|
|
3
5
|
from typing import TypedDict
|
|
4
6
|
|
|
5
7
|
from django.http import HttpRequest
|
|
6
8
|
|
|
9
|
+
from django_log_formatter_asim.ecs import _get_container_id
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class LoggedInUser(TypedDict, total=False):
|
|
13
|
+
"""
|
|
14
|
+
A unique identifier for the user.
|
|
15
|
+
|
|
16
|
+
Defaults to the logged in Django User.username if not provided.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
username: Optional[str]
|
|
20
|
+
|
|
7
21
|
|
|
8
22
|
class Result(str, Enum):
|
|
9
23
|
Success = "Success"
|
|
@@ -51,13 +65,67 @@ class Server(TypedDict, total=False):
|
|
|
51
65
|
service_name: Optional[str]
|
|
52
66
|
|
|
53
67
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
68
|
+
class Activity:
|
|
69
|
+
def _activity_fields(
|
|
70
|
+
self,
|
|
71
|
+
request: HttpRequest,
|
|
72
|
+
event_created: datetime.datetime,
|
|
73
|
+
result: Result,
|
|
74
|
+
server: Server,
|
|
75
|
+
client: Client,
|
|
76
|
+
severity: Optional[Severity],
|
|
77
|
+
result_details: Optional[str],
|
|
78
|
+
message: Optional[str],
|
|
79
|
+
):
|
|
80
|
+
log = {
|
|
81
|
+
"EventStartTime": event_created.isoformat(),
|
|
82
|
+
"EventSeverity": severity or self._default_severity(result),
|
|
83
|
+
"EventResult": result,
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if "domain_name" in server:
|
|
87
|
+
log["HttpHost"] = server["domain_name"]
|
|
88
|
+
elif "HTTP_HOST" in request.META:
|
|
89
|
+
log["HttpHost"] = request.get_host()
|
|
90
|
+
|
|
91
|
+
if "service_name" in server:
|
|
92
|
+
log["TargetAppName"] = server["service_name"]
|
|
93
|
+
elif os.environ.get("COPILOT_APPLICATION_NAME") and os.environ.get("COPILOT_SERVICE_NAME"):
|
|
94
|
+
app_name = (
|
|
95
|
+
f"{os.environ['COPILOT_APPLICATION_NAME']}-{os.environ['COPILOT_SERVICE_NAME']}"
|
|
96
|
+
)
|
|
97
|
+
log["TargetAppName"] = app_name
|
|
98
|
+
|
|
99
|
+
if container_id := _get_container_id():
|
|
100
|
+
log["TargetContainerId"] = container_id
|
|
101
|
+
|
|
102
|
+
if "ip_address" in client:
|
|
103
|
+
log["SrcIpAddr"] = client["ip_address"]
|
|
104
|
+
elif client_ip := self._get_client_ip_address(request):
|
|
105
|
+
log["SrcIpAddr"] = client_ip
|
|
106
|
+
|
|
107
|
+
if "requested_url" in client:
|
|
108
|
+
log["TargetUrl"] = client["requested_url"]
|
|
109
|
+
elif "HTTP_HOST" in request.META:
|
|
110
|
+
log["TargetUrl"] = request.scheme + "://" + request.get_host() + request.get_full_path()
|
|
111
|
+
|
|
112
|
+
if result_details:
|
|
113
|
+
log["EventResultDetails"] = result_details
|
|
114
|
+
|
|
115
|
+
if message:
|
|
116
|
+
log["EventMessage"] = message
|
|
117
|
+
|
|
118
|
+
if "ip_address" in server:
|
|
119
|
+
log["DvcIpAddr"] = server["ip_address"]
|
|
120
|
+
|
|
121
|
+
return log
|
|
122
|
+
|
|
123
|
+
def _default_severity(sef, result: Result) -> Severity:
|
|
124
|
+
return Severity.Informational if result == Result.Success else Severity.Medium
|
|
125
|
+
|
|
126
|
+
def _get_client_ip_address(self, request: HttpRequest) -> Optional[str]:
|
|
127
|
+
# Import here as ipware uses settings
|
|
128
|
+
from ipware import get_client_ip
|
|
129
|
+
|
|
130
|
+
client_ip, _ = get_client_ip(request)
|
|
131
|
+
return client_ip
|
|
@@ -7,14 +7,12 @@ from typing import TypedDict
|
|
|
7
7
|
|
|
8
8
|
from django.http import HttpRequest
|
|
9
9
|
|
|
10
|
-
from
|
|
11
|
-
|
|
10
|
+
from .common import Activity
|
|
12
11
|
from .common import Client
|
|
12
|
+
from .common import LoggedInUser
|
|
13
13
|
from .common import Result
|
|
14
14
|
from .common import Server
|
|
15
15
|
from .common import Severity
|
|
16
|
-
from .common import _default_severity
|
|
17
|
-
from .common import _get_client_ip_address
|
|
18
16
|
|
|
19
17
|
|
|
20
18
|
class FileActivityEvent(str, Enum):
|
|
@@ -71,186 +69,146 @@ class FileActivityFile(FileActivityFileBase, total=False):
|
|
|
71
69
|
size: Optional[int]
|
|
72
70
|
|
|
73
71
|
|
|
74
|
-
class
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
log
|
|
217
|
-
|
|
218
|
-
if message:
|
|
219
|
-
log["EventMessage"] = message
|
|
220
|
-
|
|
221
|
-
if "ip_address" in server:
|
|
222
|
-
log["DvcIpAddr"] = server["ip_address"]
|
|
223
|
-
|
|
224
|
-
print(json.dumps(log), flush=True)
|
|
225
|
-
|
|
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
|
-
|
|
254
|
-
log_file_activity.Event = FileActivityEvent
|
|
255
|
-
log_file_activity.Result = Result
|
|
256
|
-
log_file_activity.Severity = Severity
|
|
72
|
+
class LogFileActivity(Activity):
|
|
73
|
+
Event = FileActivityEvent
|
|
74
|
+
Result = Result
|
|
75
|
+
Severity = Severity
|
|
76
|
+
|
|
77
|
+
def __call__(
|
|
78
|
+
self,
|
|
79
|
+
request: HttpRequest,
|
|
80
|
+
event: FileActivityEvent,
|
|
81
|
+
result: Result,
|
|
82
|
+
file: FileActivityFile,
|
|
83
|
+
source_file: Optional[FileActivityFile] = None,
|
|
84
|
+
user: Optional[LoggedInUser] = None,
|
|
85
|
+
server: Optional[Server] = None,
|
|
86
|
+
client: Optional[Client] = None,
|
|
87
|
+
severity: Optional[Severity] = None,
|
|
88
|
+
time_generated: Optional[datetime.datetime] = None,
|
|
89
|
+
result_details: Optional[str] = None,
|
|
90
|
+
message: Optional[str] = None,
|
|
91
|
+
):
|
|
92
|
+
"""
|
|
93
|
+
Log an ASIM File Event to standard output.
|
|
94
|
+
|
|
95
|
+
:param request: django.http.HttpRequest object which initiated this Authentication request
|
|
96
|
+
from which the following data will be logged if available
|
|
97
|
+
- Django Authentication systems current username
|
|
98
|
+
- Client IP address
|
|
99
|
+
- URL requested by the client
|
|
100
|
+
- Server domain name
|
|
101
|
+
:param event: What File Event action was attempted, one of:
|
|
102
|
+
- FileAccessed
|
|
103
|
+
- FileCreated
|
|
104
|
+
- FileModified
|
|
105
|
+
- FileDeleted
|
|
106
|
+
- FileRenamed
|
|
107
|
+
- FileCopied
|
|
108
|
+
- FileMoved
|
|
109
|
+
- FolderCreated
|
|
110
|
+
- FolderDeleted
|
|
111
|
+
- FolderMoved
|
|
112
|
+
- FolderModified
|
|
113
|
+
:param result: What outcome did the action have, either "Success", "Failure", "Partial", "NA"
|
|
114
|
+
:param file: Dictionary containing information on the target of this File event see
|
|
115
|
+
FileActivityFile for more details.
|
|
116
|
+
:param source_file: Dictionary containing information on the source of this File event,
|
|
117
|
+
this MUST be used for a FileRenamed, FileMoved, FileCopied, FolderMoved
|
|
118
|
+
operation. See FileActivityFile for more details.
|
|
119
|
+
:param user: Dictionary containing information on the logged in users username.
|
|
120
|
+
:param server: Dictionary containing information on the server servicing this File event
|
|
121
|
+
see Server class for more details.
|
|
122
|
+
:param client: Dictionary containing information on the client performing this File event
|
|
123
|
+
see Client class for more details.
|
|
124
|
+
:param severity: Optional severity of the event, defaults to "Informational", otherwise one of:
|
|
125
|
+
- "Informational"
|
|
126
|
+
- "Low"
|
|
127
|
+
- "Medium"
|
|
128
|
+
- "High"
|
|
129
|
+
:param time_generated: Optional datetime for when the event happened, otherwise datetime.now
|
|
130
|
+
:param result_details: Optional string describing any details associated with the events outcome.
|
|
131
|
+
This field is typically populated when the result is a failure.
|
|
132
|
+
:param message: Optional string describing the reason why the log was generated.
|
|
133
|
+
|
|
134
|
+
See also: https://learn.microsoft.com/en-us/azure/sentinel/normalization-schema-file-event
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
self._log_file_activity(
|
|
138
|
+
request,
|
|
139
|
+
event,
|
|
140
|
+
result,
|
|
141
|
+
file,
|
|
142
|
+
source_file,
|
|
143
|
+
user={} if user == None else user,
|
|
144
|
+
server={} if server == None else server,
|
|
145
|
+
client={} if client == None else client,
|
|
146
|
+
event_created=time_generated or datetime.datetime.now(tz=datetime.timezone.utc),
|
|
147
|
+
severity=severity,
|
|
148
|
+
result_details=result_details,
|
|
149
|
+
message=message,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
def _log_file_activity(
|
|
153
|
+
self,
|
|
154
|
+
request: HttpRequest,
|
|
155
|
+
event: FileActivityEvent,
|
|
156
|
+
result: Result,
|
|
157
|
+
file: FileActivityFile,
|
|
158
|
+
source_file: Optional[FileActivityFile],
|
|
159
|
+
user: LoggedInUser,
|
|
160
|
+
server: Server,
|
|
161
|
+
client: Client,
|
|
162
|
+
event_created: datetime.datetime,
|
|
163
|
+
severity: Optional[Severity] = None,
|
|
164
|
+
result_details: Optional[str] = None,
|
|
165
|
+
message: Optional[str] = None,
|
|
166
|
+
):
|
|
167
|
+
log = {
|
|
168
|
+
"EventSchema": "FileEvent",
|
|
169
|
+
"EventSchemaVersion": "0.2.1",
|
|
170
|
+
"EventType": event,
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
log.update(
|
|
174
|
+
self._activity_fields(
|
|
175
|
+
request, event_created, result, server, client, severity, result_details, message
|
|
176
|
+
)
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
log.update(self._generate_file_attributes(file, "Target"))
|
|
180
|
+
if source_file:
|
|
181
|
+
log.update(self._generate_file_attributes(source_file, "Src"))
|
|
182
|
+
|
|
183
|
+
if "username" in user:
|
|
184
|
+
log["TargetUsername"] = user["username"]
|
|
185
|
+
elif hasattr(request, "user") and request.user.username:
|
|
186
|
+
log["TargetUsername"] = request.user.username
|
|
187
|
+
|
|
188
|
+
print(json.dumps(log), flush=True)
|
|
189
|
+
|
|
190
|
+
def _generate_file_attributes(self, file: FileActivityFile, prefix: str) -> dict:
|
|
191
|
+
log = {prefix + "FilePath": file["path"]}
|
|
192
|
+
|
|
193
|
+
if "name" in file:
|
|
194
|
+
log[prefix + "FileName"] = file["name"]
|
|
195
|
+
else:
|
|
196
|
+
log[prefix + "FileName"] = os.path.basename(file["path"])
|
|
197
|
+
|
|
198
|
+
if "extension" in file:
|
|
199
|
+
log[prefix + "FileExtension"] = file["extension"]
|
|
200
|
+
else:
|
|
201
|
+
file_name_parts = list(filter(None, log[prefix + "FileName"].split(".", 1)))
|
|
202
|
+
if len(file_name_parts) > 1:
|
|
203
|
+
log[prefix + "FileExtension"] = file_name_parts[1]
|
|
204
|
+
|
|
205
|
+
if "content_type" in file:
|
|
206
|
+
log[prefix + "FileMimeType"] = file["content_type"]
|
|
207
|
+
|
|
208
|
+
if "sha256" in file:
|
|
209
|
+
log[prefix + "FileSHA256"] = file["sha256"]
|
|
210
|
+
|
|
211
|
+
if "size" in file:
|
|
212
|
+
log[prefix + "FileSize"] = file["size"]
|
|
213
|
+
|
|
214
|
+
return log
|
{django_log_formatter_asim-1.2.0a0.dist-info → django_log_formatter_asim-1.2.0a1.dist-info}/METADATA
RENAMED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: django-log-formatter-asim
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.0a1
|
|
4
4
|
Summary: Formats Django logs in ASIM format.
|
|
5
5
|
License: MIT
|
|
6
|
+
License-File: LICENSE
|
|
6
7
|
Author: Department for Business and Trade Platform Team
|
|
7
8
|
Author-email: sre-team@digital.trade.gov.uk
|
|
8
9
|
Requires-Python: >=3.9,<4
|
|
@@ -13,7 +14,8 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
13
14
|
Classifier: Programming Language :: Python :: 3.11
|
|
14
15
|
Classifier: Programming Language :: Python :: 3.12
|
|
15
16
|
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
-
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Requires-Dist: ddtrace (>=3.2.1,<5)
|
|
17
19
|
Requires-Dist: django (>=3,<5) ; python_version == "3.9"
|
|
18
20
|
Requires-Dist: django (>=3,<6) ; python_version >= "3.10" and python_version < "4"
|
|
19
21
|
Requires-Dist: django-ipware (>=7.0.1,<8.0.0)
|
|
@@ -77,6 +79,101 @@ LOGGING = {
|
|
|
77
79
|
In this example we assign the ASIM formatter to a `handler` and ensure both `root` and `django` loggers use this `handler`.
|
|
78
80
|
We then set `propagate` to `False` on the `django` logger, to avoid duplicating logs at the root level.
|
|
79
81
|
|
|
82
|
+
### Settings
|
|
83
|
+
|
|
84
|
+
`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.
|
|
85
|
+
|
|
86
|
+
`DLFA_TRACE_HEADERS` - used for defining custom zipkin headers, the defaults is `("X-Amzn-Trace-Id")`, but for applications hosted in GOV.UK PaaS you should use `("X-B3-TraceId", "X-B3-SpanId")`. If you are running your application in both places side by side during migration, the following should work in your Django settings:
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
from dbt_copilot_python.utility import is_copilot
|
|
90
|
+
|
|
91
|
+
if is_copilot():
|
|
92
|
+
DLFA_TRACE_HEADERS = ("X-B3-TraceId", "X-B3-SpanId")
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
`DLFA_INCLUDE_RAW_LOG` - By default the original unformatted log is not included in the ASIM formatted log. You can enable that by setting this to `True` and it will be included in `AddidtionalFields.RawLog`.
|
|
96
|
+
|
|
97
|
+
> [!WARNING]
|
|
98
|
+
> Setting `DLFA_INCLUDE_RAW_LOG` to `True` will cause additional private fields to be output to your logs.
|
|
99
|
+
> This could include secrets, such as AWS Access Keys, private HTTP Request data, or personally identifiable information.
|
|
100
|
+
> This setting is not recommended for a production environment.
|
|
101
|
+
|
|
102
|
+
### Serialisation behaviour
|
|
103
|
+
|
|
104
|
+
The package provides one `logging.Formatter` class, `ASIMFormatter` which routes log messages to a serialiser
|
|
105
|
+
which generates a python dict which the formatter converts to a JSON string and prints to standard output.
|
|
106
|
+
|
|
107
|
+
It has a generic serialiser called `ASIMRootFormatter` and a custom serlializer for log messages where the
|
|
108
|
+
logger is `django.request`.
|
|
109
|
+
|
|
110
|
+
``` python
|
|
111
|
+
ASIM_FORMATTERS = {
|
|
112
|
+
"root": ASIMRootFormatter,
|
|
113
|
+
"django.request": ASIMRequestFormatter,
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
#### ASIMRootFormatter
|
|
118
|
+
|
|
119
|
+
This serialiser outputs the following ASIM fields.
|
|
120
|
+
|
|
121
|
+
- `EventSchema` = `ProcessEvent`
|
|
122
|
+
- `ActingAppType` = `Django`
|
|
123
|
+
- `AdditionalFields[DjangoLogFormatterAsimVersion]`
|
|
124
|
+
- `EventSchemaVersion`
|
|
125
|
+
- `EventMessage`
|
|
126
|
+
- `EventCount`
|
|
127
|
+
- `EventStartTime`
|
|
128
|
+
- `EventEndTime`
|
|
129
|
+
- `EventType`
|
|
130
|
+
- `EventResult`
|
|
131
|
+
- `EventSeverity`
|
|
132
|
+
- `EventOriginalSeverity`
|
|
133
|
+
|
|
134
|
+
Additionally, the following DataDog fields where available:
|
|
135
|
+
|
|
136
|
+
- `dd.trace_id`
|
|
137
|
+
- `dd.span_id`
|
|
138
|
+
- `env`
|
|
139
|
+
- `service`
|
|
140
|
+
- `version`
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
#### ASIMRequestFormatter
|
|
144
|
+
|
|
145
|
+
This serialiser outputs the following ASIM fields in addition to the ones from ASIMRootFormatter.
|
|
146
|
+
It is coupled to the datastructure provided by the `django.request` logger.
|
|
147
|
+
The `django.request` logger only outputs requests where the response code is 4xx/5xx.
|
|
148
|
+
|
|
149
|
+
- `SrcIpAddr` and `IpAddr`
|
|
150
|
+
- `SrcPortNumber`
|
|
151
|
+
- `SrcUserId` and `SrcUsername`
|
|
152
|
+
- `HttpUserAgent`
|
|
153
|
+
- `AdditionalFields["TraceHeaders"][trace_header_name]` - See `DLFA_TRACE_HEADERS` setting for more information.
|
|
154
|
+
|
|
155
|
+
#### Creating a custom serialiser
|
|
156
|
+
|
|
157
|
+
If you wish to create your own ASIM serialiser, you can inherit from `ASIMRootFormatter` and call
|
|
158
|
+
`super().get_log_dict()` to get the base level logging data for augmentation:
|
|
159
|
+
|
|
160
|
+
``` python
|
|
161
|
+
class MyASIMFormatter(ASIMRootFormatter):
|
|
162
|
+
def get_log_dict(self):
|
|
163
|
+
log_dict = super().get_log_dict()
|
|
164
|
+
|
|
165
|
+
# Customise logger event
|
|
166
|
+
|
|
167
|
+
return log_dict
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
This serialiser can then be added to `ASIM_FORMATTERS`...
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
ASIM_FORMATTERS["my_logger"] = MyASIMFormatter
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
|
|
80
177
|
### ASIM Events
|
|
81
178
|
|
|
82
179
|
The events mostly follow the Microsoft schema but have been tailored to Department of Business and Trade needs.
|
|
@@ -127,48 +224,114 @@ log_authentication(
|
|
|
127
224
|
}
|
|
128
225
|
```
|
|
129
226
|
|
|
130
|
-
|
|
227
|
+
#### File Activity event
|
|
131
228
|
|
|
132
|
-
|
|
229
|
+
Following the [ASIM File Event Schema](https://learn.microsoft.com/en-us/azure/sentinel/normalization-schema-file-event).
|
|
133
230
|
|
|
134
|
-
|
|
231
|
+
```python
|
|
232
|
+
# Example usage
|
|
233
|
+
from django_log_formatter_asim.events import log_file_activity
|
|
135
234
|
|
|
136
|
-
|
|
235
|
+
log_file_activity(
|
|
236
|
+
request,
|
|
237
|
+
event=log_file_activity.Event.FileCopied,
|
|
238
|
+
result=log_file_activity.Result.Success,
|
|
239
|
+
file={
|
|
240
|
+
"path": "/tmp/copied.txt",
|
|
241
|
+
"content_type": "text/plain",
|
|
242
|
+
"extension": "txt",
|
|
243
|
+
"name": "copied.txt",
|
|
244
|
+
"sha256": "6798b7a132f37a0474002dec538ec52bdcd5f7b76e49e52c8a3d2016ca8d1d18",
|
|
245
|
+
"size": 14,
|
|
246
|
+
},
|
|
247
|
+
# source_file is only necessary if the event is one of FileRenamed, FileMoved, FileCopied, FolderMoved
|
|
248
|
+
source_file={
|
|
249
|
+
"path": "/tmp/original.txt",
|
|
250
|
+
"content_type": "text/plain",
|
|
251
|
+
"extension": "txt",
|
|
252
|
+
"name": "original.txt",
|
|
253
|
+
"sha256": "6798b7a132f37a0474002dec538ec52bdcd5f7b76e49e52c8a3d2016ca8d1d18",
|
|
254
|
+
"size": 14,
|
|
255
|
+
},
|
|
256
|
+
)
|
|
137
257
|
|
|
138
|
-
|
|
139
|
-
|
|
258
|
+
# Example JSON printed to standard output
|
|
259
|
+
{
|
|
260
|
+
# Values provided as arguments
|
|
261
|
+
"EventType": "FileCopied",
|
|
262
|
+
"EventResult": "Success",
|
|
140
263
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
264
|
+
"TargetFilePath": "/tmp/copied.txt",
|
|
265
|
+
"TargetFileName": "copied.txt",
|
|
266
|
+
"TargetFileExtension": "txt",
|
|
267
|
+
"TargetFileMimeType": "text/plain",
|
|
268
|
+
"TargetFileSHA256": "6798b7a132f37a0474002dec538ec52bdcd5f7b76e49e52c8a3d2016ca8d1d18",
|
|
269
|
+
"TargetFileSize": 14,
|
|
144
270
|
|
|
145
|
-
|
|
271
|
+
"SrcFilePath": "/tmp/original.txt",
|
|
272
|
+
"SrcFileName": "original.txt",
|
|
273
|
+
"SrcFileExtension": "txt",
|
|
274
|
+
"SrcFileMimeType": "text/plain",
|
|
275
|
+
"SrcFileSHA256": "6798b7a132f37a0474002dec538ec52bdcd5f7b76e49e52c8a3d2016ca8d1d18",
|
|
276
|
+
"SrcFileSize": 14,
|
|
146
277
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
```
|
|
278
|
+
# Calculated / Hard coded fields
|
|
279
|
+
"EventStartTime": "2025-07-30T11:05:09.406460+00:00",
|
|
280
|
+
"EventSchema": "FileEvent",
|
|
281
|
+
"EventSchemaVersion": "0.2.1",
|
|
282
|
+
"EventSeverity": "Informational",
|
|
153
283
|
|
|
154
|
-
|
|
284
|
+
# Taken from Django HttpRequest object
|
|
285
|
+
"HttpHost": "WebServer.local",
|
|
286
|
+
"SrcIpAddr": "192.168.1.101",
|
|
287
|
+
"TargetUrl": "https://WebServer.local/steel",
|
|
288
|
+
"TargetUsername": "Adrian"
|
|
155
289
|
|
|
156
|
-
|
|
157
|
-
|
|
290
|
+
# Taken from DBT Platform environment variables
|
|
291
|
+
"TargetAppName": "export-analytics-frontend",
|
|
292
|
+
}
|
|
158
293
|
```
|
|
159
294
|
|
|
160
|
-
|
|
295
|
+
#### Account Management event
|
|
161
296
|
|
|
162
|
-
|
|
297
|
+
Following the [ASIM User Management Schema](https://learn.microsoft.com/en-us/azure/sentinel/normalization-schema-user-management).
|
|
163
298
|
|
|
164
|
-
``` python
|
|
165
|
-
class ASIMSystemFormatter(ASIMFormatterBase):
|
|
166
|
-
def get_event(self):
|
|
167
|
-
logger_event = self._get_event_base()
|
|
168
299
|
|
|
169
|
-
|
|
300
|
+
```python
|
|
301
|
+
# Example usage
|
|
302
|
+
from django_log_formatter_asim.events import log_account_management
|
|
303
|
+
|
|
304
|
+
log_account_management(
|
|
305
|
+
request,
|
|
306
|
+
event=log_account_management.Event.UserCreated,
|
|
307
|
+
result=log_account_management.Result.Success,
|
|
308
|
+
account={
|
|
309
|
+
"username": "Roger",
|
|
310
|
+
},
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
# Example JSON printed to standard output
|
|
314
|
+
{
|
|
315
|
+
# Values provided as arguments
|
|
316
|
+
"EventType": "UserCreated",
|
|
317
|
+
"EventResult": "Success",
|
|
318
|
+
"TargetUsername": "Roger",
|
|
170
319
|
|
|
171
|
-
|
|
320
|
+
# Calculated / Hard coded fields
|
|
321
|
+
"EventStartTime": "2025-07-30T11:05:09.406460+00:00",
|
|
322
|
+
"EventSchema": "UserManagement",
|
|
323
|
+
"EventSchemaVersion": "0.1.1",
|
|
324
|
+
"EventSeverity": "Informational",
|
|
325
|
+
|
|
326
|
+
# Taken from Django HttpRequest object
|
|
327
|
+
"HttpHost": "WebServer.local",
|
|
328
|
+
"SrcIpAddr": "192.168.1.101",
|
|
329
|
+
"TargetUrl": "https://WebServer.local/admin/create-user",
|
|
330
|
+
"ActorUsername": "Adrian"
|
|
331
|
+
|
|
332
|
+
# Taken from DBT Platform environment variables
|
|
333
|
+
"TargetAppName": "export-analytics-frontend",
|
|
334
|
+
}
|
|
172
335
|
```
|
|
173
336
|
|
|
174
337
|
## Dependencies
|
|
@@ -233,3 +396,4 @@ poetry publish
|
|
|
233
396
|
Check the [PyPI Release history](https://pypi.org/project/django-log-formatter-asim/#history) to make sure the package has been updated.
|
|
234
397
|
|
|
235
398
|
For an optional manual check, install the package locally and test everything works as expected.
|
|
399
|
+
|
|
@@ -0,0 +1,11 @@
|
|
|
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=K78BzhkNJpI2AdpaiMmf9FrBQTurEdBFX1Xx2hy-TGg,270
|
|
4
|
+
django_log_formatter_asim/events/account_management.py,sha256=k6uPs9XcchCW2G3lcbdeXviSVWjJ4pK52mrYekwSLVQ,3936
|
|
5
|
+
django_log_formatter_asim/events/authentication.py,sha256=8Kn5_8WF-DunmgpqWPVhaNh3k1qYVCwLbQ2I7WPmHj8,6966
|
|
6
|
+
django_log_formatter_asim/events/common.py,sha256=CWC6Bo3R7yOY7NElJb1jxcD1el72Hl4CBZhFYgT0tIs,3874
|
|
7
|
+
django_log_formatter_asim/events/file_activity.py,sha256=hTscMhGyaYVsLzqMcw-aQB5ks4G2VpB9Pkw63LwxqaI,7793
|
|
8
|
+
django_log_formatter_asim-1.2.0a1.dist-info/METADATA,sha256=f-pWjsHzFENa5TdBbEsWPTvr5rcbKsb15arW6n-my8k,12478
|
|
9
|
+
django_log_formatter_asim-1.2.0a1.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
|
|
10
|
+
django_log_formatter_asim-1.2.0a1.dist-info/licenses/LICENSE,sha256=dP79lN73--7LMApnankTGLqDbImXg8iYFqWgnExGkGk,1090
|
|
11
|
+
django_log_formatter_asim-1.2.0a1.dist-info/RECORD,,
|
|
@@ -1,10 +0,0 @@
|
|
|
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=WUHSllqTKTLnEdRlLn2SqCn3YR1_AKPA3y8Gm22CyIY,7808
|
|
5
|
-
django_log_formatter_asim/events/common.py,sha256=-QjR9QPkMDqD3nXPiukAJvm8qjUt2LwzhL_zN5ae_t0,1702
|
|
6
|
-
django_log_formatter_asim/events/file_activity.py,sha256=h8xlY0ABDCejAVnRMJ0aeWdNJZpu_54et1squMEfSM8,8755
|
|
7
|
-
django_log_formatter_asim-1.2.0a0.dist-info/LICENSE,sha256=dP79lN73--7LMApnankTGLqDbImXg8iYFqWgnExGkGk,1090
|
|
8
|
-
django_log_formatter_asim-1.2.0a0.dist-info/METADATA,sha256=ofAaVdlWggllH93apSQg16S0ZHaPmF1xd27APovOirc,7428
|
|
9
|
-
django_log_formatter_asim-1.2.0a0.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
|
10
|
-
django_log_formatter_asim-1.2.0a0.dist-info/RECORD,,
|
|
File without changes
|