Feature 10958: Audit Logs for OSM 63/13263/6
authorelumalai <deepika.e@tataelxsi.co.in>
Mon, 24 Apr 2023 15:08:32 +0000 (20:38 +0530)
committerelumalai <deepika.e@tataelxsi.co.in>
Fri, 26 May 2023 11:24:21 +0000 (16:54 +0530)
Added support for audit logs following CEF format in nbi.py,
auth.py and authconn_internal.py.

Change-Id: Ia4ef2fd7097a81d031d6e1bc68a779f536675fef
Signed-off-by: elumalai <deepika.e@tataelxsi.co.in>
osm_nbi/auth.py
osm_nbi/authconn_internal.py
osm_nbi/nbi.cfg
osm_nbi/nbi.py
osm_nbi/utils.py
requirements.in
requirements.txt

index 0b3264f..ec33b1c 100644 (file)
@@ -44,6 +44,7 @@ from osm_nbi.authconn import AuthException, AuthconnException, AuthExceptionUnau
 from osm_nbi.authconn_keystone import AuthconnKeystone
 from osm_nbi.authconn_internal import AuthconnInternal
 from osm_nbi.authconn_tacacs import AuthconnTacacs
+from osm_nbi.utils import cef_event, cef_event_builder
 from osm_common import dbmemory, dbmongo, msglocal, msgkafka
 from osm_common.dbbase import DbException
 from osm_nbi.validation import is_valid_uuid
@@ -88,6 +89,7 @@ class Authenticator:
         self.valid_query_string = valid_query_string
         self.system_admin_role_id = None  # system_role id
         self.test_project_id = None  # test_project_id
+        self.cef_logger = None
 
     def start(self, config):
         """
@@ -98,6 +100,7 @@ class Authenticator:
         :param config: dictionary containing the relevant parameters for this object.
         """
         self.config = config
+        self.cef_logger = cef_event_builder(config["authentication"])
 
         try:
             if not self.db:
@@ -505,6 +508,18 @@ class Authenticator:
                     item_id,
                 )
                 self.logger.info("RBAC_auth: {}".format(RBAC_auth))
+                if RBAC_auth:
+                    cef_event(
+                        self.cef_logger,
+                        {
+                            "name": "System Access",
+                            "sourceUserName": token_info.get("username"),
+                            "message": "Accessing account with system privileges, Project={}".format(
+                                token_info.get("project_name")
+                            ),
+                        },
+                    )
+                    self.logger.info("{}".format(self.cef_logger))
                 token_info["allow_show_user_project_role"] = RBAC_auth
 
             return token_info
index d039f84..3f495d8 100644 (file)
@@ -40,6 +40,7 @@ from osm_nbi.authconn import (
 )  # , AuthconnOperationException
 from osm_common.dbbase import DbException
 from osm_nbi.base_topic import BaseTopic
+from osm_nbi.utils import cef_event, cef_event_builder
 from osm_nbi.validation import is_valid_uuid
 from time import time, sleep
 from http import HTTPStatus
@@ -68,6 +69,7 @@ class AuthconnInternal(Authconn):
 
         # To be Confirmed
         self.sess = None
+        self.cef_logger = cef_event_builder(config)
 
     def validate_token(self, token):
         """
@@ -193,6 +195,18 @@ class AuthconnInternal(Authconn):
         if user:
             user_content = self.validate_user(user, password)
             if not user_content:
+                cef_event(
+                    self.cef_logger,
+                    {
+                        "name": "User login",
+                        "sourceUserName": user,
+                        "message": "Invalid username/password Project={} Outcome=Failure".format(
+                            project
+                        ),
+                        "severity": "3",
+                    },
+                )
+                self.logger.exception("{}".format(self.cef_logger))
                 raise AuthException(
                     "Invalid username/password", http_code=HTTPStatus.UNAUTHORIZED
                 )
@@ -401,6 +415,16 @@ class AuthconnInternal(Authconn):
         if pswd and (
             len(pswd) != 64 or not re.match("[a-fA-F0-9]*", pswd)
         ):  # TODO: Improve check?
+            cef_event(
+                self.cef_logger,
+                {
+                    "name": "Change Password",
+                    "sourceUserName": user_data["username"],
+                    "message": "Changing Password for user, Outcome=Success",
+                    "severity": "2",
+                },
+            )
+            self.logger.info("{}".format(self.cef_logger))
             salt = uuid4().hex
             if "_admin" not in user_data:
                 user_data["_admin"] = {}
index 383b462..49f4985 100644 (file)
@@ -120,5 +120,10 @@ backend: "keystone"         # internal or keystone or tacacs
 # pwd_expiry_check: True      # Uncomment to enable the password expiry check
 # days: 30                    # Default value
 
+# CEF Configuration
+version: "0"
+deviceVendor: "OSM"
+deviceProduct: "OSM"
+
 [rbac]
 # roles_to_operations: "roles_to_operations.yml"  # initial role generation when database
index 8fa6152..343ac0d 100644 (file)
@@ -28,6 +28,7 @@ from osm_nbi.authconn import AuthException, AuthconnException
 from osm_nbi.auth import Authenticator
 from osm_nbi.engine import Engine, EngineException
 from osm_nbi.subscriptions import SubscriptionThread
+from osm_nbi.utils import cef_event, cef_event_builder
 from osm_nbi.validation import ValidationError
 from osm_common.dbbase import DbException
 from osm_common.fsbase import FsException
@@ -46,6 +47,7 @@ database_version = "1.2"
 auth_database_version = "1.0"
 nbi_server = None  # instance of Server class
 subscription_thread = None  # instance of SubscriptionThread class
+cef_logger = None
 
 """
 North Bound Interface  (O: OSM specific; 5,X: SOL005 not implemented yet; O5: SOL005 implemented)
@@ -1070,6 +1072,17 @@ class Server(object):
                 }
             # cherrypy.response.cookie["Authorization"] = outdata["id"]
             # cherrypy.response.cookie["Authorization"]['expires'] = 3600
+            cef_event(
+                cef_logger,
+                {
+                    "name": "User Login",
+                    "sourceUserName": token_info.get("username"),
+                    "message": "User Logged In, Project={} Outcome=Success".format(
+                        token_info.get("project_name")
+                    ),
+                },
+            )
+            cherrypy.log("{}".format(cef_logger))
         elif method == "DELETE":
             if not token_id and "id" in kwargs:
                 token_id = kwargs["id"]
@@ -1078,9 +1091,23 @@ class Server(object):
                 # for logging
                 self._format_login(token_info)
                 token_id = token_info["_id"]
+            token_details = self.engine.db.get_one("tokens", {"_id": token_id})
+            current_user = token_details.get("username")
+            current_project = token_details.get("project_name")
             outdata = self.authenticator.del_token(token_id)
             token_info = None
             cherrypy.session["Authorization"] = "logout"  # pylint: disable=E1101
+            cef_event(
+                cef_logger,
+                {
+                    "name": "User Logout",
+                    "sourceUserName": current_user,
+                    "message": "User Logged Out, Project={} Outcome=Success".format(
+                        current_project
+                    ),
+                },
+            )
+            cherrypy.log("{}".format(cef_logger))
             # cherrypy.response.cookie["Authorization"] = token_id
             # cherrypy.response.cookie["Authorization"]['expires'] = 0
         else:
@@ -1390,6 +1417,14 @@ class Server(object):
         engine_topic = None
         rollback = []
         engine_session = None
+        url_id = ""
+        log_mapping = {
+            "POST": "Creating",
+            "GET": "Fetching",
+            "DELETE": "Deleting",
+            "PUT": "Updating",
+            "PATCH": "Updating",
+        }
         try:
             if not main_topic or not version or not topic:
                 raise NbiException(
@@ -1416,6 +1451,8 @@ class Server(object):
                     "URL version '{}' not supported".format(version),
                     HTTPStatus.METHOD_NOT_ALLOWED,
                 )
+            if _id is not None:
+                url_id = _id
 
             if (
                 kwargs
@@ -1744,6 +1781,36 @@ class Server(object):
             ):
                 self.authenticator.remove_token_from_cache()
 
+            if item is not None:
+                cef_event(
+                    cef_logger,
+                    {
+                        "name": "User Operation",
+                        "sourceUserName": token_info.get("username"),
+                        "message": "Performing {} operation on {} {}, Project={} Outcome=Success".format(
+                            item,
+                            topic,
+                            url_id,
+                            token_info.get("project_name"),
+                        ),
+                    },
+                )
+                cherrypy.log("{}".format(cef_logger))
+            else:
+                cef_event(
+                    cef_logger,
+                    {
+                        "name": "User Operation",
+                        "sourceUserName": token_info.get("username"),
+                        "message": "{} {} {}, Project={} Outcome=Success".format(
+                            log_mapping[method],
+                            topic,
+                            url_id,
+                            token_info.get("project_name"),
+                        ),
+                    },
+                )
+                cherrypy.log("{}".format(cef_logger))
             return self._format_out(outdata, token_info, _format)
         except Exception as e:
             if isinstance(
@@ -1806,6 +1873,38 @@ class Server(object):
                 "status": http_code_value,
                 "detail": error_text,
             }
+            if item is not None and token_info is not None:
+                cef_event(
+                    cef_logger,
+                    {
+                        "name": "User Operation",
+                        "sourceUserName": token_info.get("username", None),
+                        "message": "Performing {} operation on {} {}, Project={} Outcome=Failure".format(
+                            item,
+                            topic,
+                            url_id,
+                            token_info.get("project_name", None),
+                        ),
+                        "severity": "2",
+                    },
+                )
+                cherrypy.log("{}".format(cef_logger))
+            elif token_info is not None:
+                cef_event(
+                    cef_logger,
+                    {
+                        "name": "User Operation",
+                        "sourceUserName": token_info.get("username", None),
+                        "message": "{} {} {}, Project={} Outcome=Failure".format(
+                            item,
+                            topic,
+                            url_id,
+                            token_info.get("project_name", None),
+                        ),
+                        "severity": "2",
+                    },
+                )
+                cherrypy.log("{}".format(cef_logger))
             return self._format_out(problem_details, token_info)
             # raise cherrypy.HTTPError(e.http_code.value, str(e))
         finally:
@@ -1828,6 +1927,7 @@ def _start_service():
     """
     global nbi_server
     global subscription_thread
+    global cef_logger
     cherrypy.log.error("Starting osm_nbi")
     # update general cherrypy configuration
     update_dict = {}
@@ -1929,6 +2029,8 @@ def _start_service():
         target_version=auth_database_version
     )
 
+    cef_logger = cef_event_builder(engine_config["authentication"])
+
     # start subscriptions thread:
     subscription_thread = SubscriptionThread(
         config=engine_config, engine=nbi_server.engine
index 73fc40f..9b48ee8 100644 (file)
@@ -21,6 +21,8 @@
 # For those usages not covered by the Apache License, Version 2.0 please
 # contact: fbravo@whitestack.com or agarcia@whitestack.com
 ##
+from cefevent import CEFEvent
+from osm_nbi import version
 
 
 def find_in_list(the_list, condition_lambda):
@@ -64,3 +66,29 @@ def deep_update_dict(data, updated_data):
         return data
 
     return data
+
+
+def cef_event(cef_logger, cef_fields):
+    for key, value in cef_fields.items():
+        cef_logger.set_field(key, value)
+
+
+def cef_event_builder(config):
+    cef_logger = CEFEvent()
+    cef_fields = {
+        "version": config["version"],
+        "deviceVendor": config["deviceVendor"],
+        "deviceProduct": config["deviceProduct"],
+        "deviceVersion": get_version(),
+        "message": "CEF Logger",
+        "sourceUserName": "admin",
+        "severity": 1,
+    }
+    cef_event(cef_logger, cef_fields)
+    cef_logger.build_cef()
+    return cef_logger
+
+
+def get_version():
+    osm_version = version.split("+")
+    return osm_version[0]
index 12fce5b..3cb40db 100644 (file)
@@ -11,6 +11,7 @@
 # under the License.
 
 aiohttp
+cefevent
 CherryPy>=18.1.2
 deepdiff
 jsonschema>=3.2.0
index 46d9b27..7de03f8 100644 (file)
@@ -26,6 +26,8 @@ attrs==23.1.0
     #   jsonschema
 autocommand==2.2.2
     # via jaraco-text
+cefevent==0.5.4
+    # via -r requirements.in
 certifi==2023.5.7
     # via requests
 charset-normalizer==3.1.0