django-log-formatter-asim 1.1.0a4__py3-none-any.whl → 1.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,2 +1,7 @@
1
- from .authentication import log_authentication
2
- from .file_activity import log_file_activity
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,21 +1,18 @@
1
1
  import datetime
2
2
  import json
3
- import os
4
3
  from enum import Enum
4
+ from hashlib import sha3_512
5
5
  from typing import Literal
6
6
  from typing import Optional
7
7
  from typing import TypedDict
8
8
 
9
9
  from django.http import HttpRequest
10
10
 
11
- from django_log_formatter_asim.ecs import _get_container_id
12
-
11
+ from .common import Activity
13
12
  from .common import Client
14
13
  from .common import Result
15
14
  from .common import Server
16
15
  from .common import Severity
17
- from .common import _default_severity
18
- from .common import _get_client_ip_address
19
16
 
20
17
 
21
18
  class AuthenticationEvent(str, Enum):
@@ -30,7 +27,7 @@ class AuthenticationLoginMethod(str, Enum):
30
27
  ExternalIDP = "External IdP"
31
28
 
32
29
 
33
- class AuthenticationUser(TypedDict):
30
+ class AuthenticationUser(TypedDict, total=False):
34
31
  """Dictionary to represent properties of the users session."""
35
32
 
36
33
  """What type of role best describes this Authentication event."""
@@ -61,138 +58,133 @@ class AuthenticationUser(TypedDict):
61
58
  sessionId: Optional[str]
62
59
 
63
60
 
64
- def log_authentication(
65
- request: HttpRequest,
66
- event: AuthenticationEvent,
67
- result: Result,
68
- login_method: AuthenticationLoginMethod,
69
- user: Optional[AuthenticationUser] = 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
- """
78
- Log an ASIM Authentication Event to standard output.
79
-
80
- :param request: django.http.HttpRequest object which initiated this Authentication request
81
- from which the following data will be logged if available
82
- - Django Authentication systems current username
83
- - Django Session middlewares Session Key
84
- - Client IP address
85
- - URL requested by the client
86
- - Server domain name
87
- :param event: What authentication action was attempted, either "Logon" or "Logoff"
88
- :param result: What outcome did the action have, either "Success", "Failure", "Partial", "NA"
89
- :param login_method: What authentication mechanism was being used, one of:
90
- - "Username & Password"
91
- - "Staff-SSO"
92
- - "UK.GOV-SSO"
93
- - "External IdP"
94
- :param user: Dictionary containing information on the subject of this Authentication event
95
- see AuthenticationUser class for more details.
96
- :param server: Dictionary containing information on the server servicing this Authentication event
97
- see Server class for more details.
98
- :param client: Dictionary containing information on the client performing this Authentication event
99
- see Client class for more details.
100
- :param severity: Optional severity of the event, defaults to "Informational", otherwise one of:
101
- - "Informational"
102
- - "Low"
103
- - "Medium"
104
- - "High"
105
- :param time_generated: Optional datetime for when the event happened, otherwise datetime.now
106
- :param result_details: Optional string describing any details associated with the events outcome.
107
- This field is typically populated when the result is a failure.
108
- :param message: Optional string describing the reason why the log was generated.
109
-
110
- See also: https://learn.microsoft.com/en-us/azure/sentinel/normalization-schema-authentication
111
- """
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
-
121
- log = {
122
- "EventCreated": event_created.isoformat(), # TODO: Should this really be EventCreated, or TimeGenerated
123
- "EventSeverity": severity or _default_severity(result),
124
- "EventOriginalType": _event_code(event, result),
125
- "EventType": event,
126
- "EventResult": result,
127
- "LogonMethod": login_method,
128
- "EventSchema": "Authentication",
129
- "EventSchemaVersion": "0.1.4",
130
- }
131
-
132
- if "domain_name" in server:
133
- log["HttpHost"] = server["domain_name"]
134
- elif "HTTP_HOST" in request.META:
135
- log["HttpHost"] = request.get_host()
136
-
137
- if "service_name" in server:
138
- log["TargetAppName"] = server["service_name"]
139
- elif os.environ.get("COPILOT_APPLICATION_NAME") and os.environ.get("COPILOT_SERVICE_NAME"):
140
- app_name = f"{os.environ['COPILOT_APPLICATION_NAME']}-{os.environ['COPILOT_SERVICE_NAME']}"
141
- log["TargetAppName"] = app_name
142
-
143
- if container_id := _get_container_id():
144
- log["ContainerId"] = container_id
145
-
146
- if "ip_address" in client:
147
- log["SrcIpAddr"] = client["ip_address"]
148
- elif client_ip := _get_client_ip_address(request):
149
- log["SrcIpAddr"] = client_ip
150
-
151
- if "requested_url" in client:
152
- log["TargetUrl"] = client["requested_url"]
153
- elif "HTTP_HOST" in request.META:
154
- log["TargetUrl"] = request.scheme + "://" + request.get_host() + request.get_full_path()
155
-
156
- if "role" in user:
157
- log["TargetUserType"] = user["role"]
158
-
159
- if "sessionId" in user:
160
- log["TargetSessionId"] = user["sessionId"]
161
- elif request.session.session_key:
162
- log["TargetSessionId"] = request.session.session_key
163
-
164
- if "username" in user:
165
- log["TargetUsername"] = user["username"]
166
- elif hasattr(request, "user") and request.user.username:
167
- log["TargetUsername"] = request.user.username
168
-
169
- if result_details:
170
- log["EventResultDetails"] = result_details
171
-
172
- if message:
173
- log["EventMessage"] = message
174
-
175
- if "ip_address" in server:
176
- log["DvcIpAddr"] = server["ip_address"]
177
-
178
- print(json.dumps(log), flush=True)
179
-
180
-
181
- log_authentication.Event = AuthenticationEvent
182
- log_authentication.Result = Result
183
- log_authentication.LoginMethod = AuthenticationLoginMethod
184
- log_authentication.Severity = Severity
185
-
186
-
187
- def _event_code(event: AuthenticationEvent, result: Result) -> str:
188
- if event == AuthenticationEvent.Logon:
189
- if result == Result.Success:
190
- return "001a"
191
- elif result == Result.Failure:
192
- return "001b"
193
- elif event == AuthenticationEvent.Logoff:
194
- if result == Result.Success:
195
- return "001c"
196
- elif result == Result.Failure:
197
- return "001d"
198
- 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"
@@ -19,7 +33,7 @@ class Severity(str, Enum):
19
33
  High = "High"
20
34
 
21
35
 
22
- class Client(TypedDict):
36
+ class Client(TypedDict, total=False):
23
37
  """Dictionary to represent properties of the HTTP Client."""
24
38
 
25
39
  """Internet Protocol Address of the client making the Authentication
@@ -29,7 +43,7 @@ class Client(TypedDict):
29
43
  requested_url: Optional[str]
30
44
 
31
45
 
32
- class Server(TypedDict):
46
+ class Server(TypedDict, total=False):
33
47
  """Dictionary to represent properties of the HTTP Server."""
34
48
 
35
49
  """
@@ -51,13 +65,67 @@ class Server(TypedDict):
51
65
  service_name: Optional[str]
52
66
 
53
67
 
54
- def _default_severity(result: Result) -> Severity:
55
- return Severity.Informational if result == Result.Success else Severity.Medium
56
-
57
-
58
- def _get_client_ip_address(request: HttpRequest) -> Optional[str]:
59
- # Import here as ipware uses settings
60
- from ipware import get_client_ip
61
-
62
- client_ip, _ = get_client_ip(request)
63
- return client_ip
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