Repository: apluslms/a-plus

Issue:
A+ Security logs, CEF format
After security audit in spring 2021, some new security-related log events were added, using SecurityLog class. The log output format should be converted to CEF format that can be exported to Aalto ITS logging systems. Also, the current log events should be reviewed: do they contain sufficient information, and should some additional events be added. Note that security log should contain only relevant events, that can be justified from security point of view.


Discussion:

Resolving patch:
diff --git a/lib/logging.py b/lib/logging.py
--- a/lib/logging.py
+++ b/lib/logging.py
@@ -3,6 +3,12 @@
 from django.contrib.auth.signals import user_logged_in, user_logged_out, user_login_failed
 from django.dispatch import receiver
 from django.http.request import HttpRequest
+from format_cef import format_cef
+from aplus import VERSION
+
+
+CEF_VENDOR = 'Aalto'
+CEF_PRODUCT = 'aplus'
 
 def skip_unreadable_post(record):
     """Skips log records of unfinished post requests."""
@@ -10,7 +16,8 @@ def skip_unreadable_post(record):
 
 class SecurityLog:
     """
-    Static singleton class used for A+ - wide security logging.
+    Static singleton class used for A+ - wide security logging,
+    to produce ArcSight Common Event Format (CEF) log.
     Django signals are used to track login/logout events.
     """
 
@@ -21,20 +28,32 @@ def logger() -> logging.Logger:
         return SecurityLog.seclogger
 
     @staticmethod
-    def logevent(request: HttpRequest, type: str, message: str) -> None:
-        # Unit tests do not have user defined in request object.
-        if request and hasattr(request, 'user'):
-            user=request.user
-            ip=request.META.get('REMOTE_ADDR')
-        else:
-            user='?'
-            ip='?'
-        SecurityLog.logger().info("({}/{}): {}: {}".format(
-            user,
-            ip,
-            type,
-            message
-        ))
+    def logevent(
+            request: HttpRequest,
+            type: str,
+            message: str,
+            severity: int = 5,
+            ) -> None:
+        extensions = {}
+        # Unit tests may not have user or source address defined.
+        if request:
+            if hasattr(request, 'user'):
+                extensions['sourceUserName'] = str(request.user)
+                extensions['sourceUserId'] = str(request.user.id)
+            if (addr := request.META.get('REMOTE_ADDR')):
+                extensions['sourceAddress'] = addr
+
+        SecurityLog.logger().info(
+            format_cef(
+                CEF_VENDOR,
+                CEF_PRODUCT,
+                VERSION,
+                type,
+                message,
+                severity,
+                extensions,
+            ).decode("utf-8")
+        )
 
 
 # This example was used as an inspiration for the following functions:
@@ -51,6 +70,8 @@ def user_logged_out_callback(sender, request, user, **kwargs):
 # so we cannot get the IP.
 @receiver(user_login_failed)
 def user_login_failed_callback(sender, credentials, **kwargs):
-    SecurityLog.logevent(None, "login-failed","{credentials}".format(
-        credentials=credentials)
-    )
+    try:
+        SecurityLog.logevent(None, "login-failed", f"username: {credentials['username']}")
+    except KeyError:
+        # Unit tests do not have 'username' in credentials, let's not fail them for that
+        pass


Test patch: