From: elumalai Date: Mon, 24 Apr 2023 15:08:32 +0000 (+0530) Subject: Feature 10958: Audit Logs for OSM X-Git-Tag: release-v14.0-start~12 X-Git-Url: https://osm.etsi.org/gitweb/?p=osm%2FNBI.git;a=commitdiff_plain;h=7802ff80245ba7ba6055bc927b91e4f8b1f42542 Feature 10958: Audit Logs for OSM Added support for audit logs following CEF format in nbi.py, auth.py and authconn_internal.py. Change-Id: Ia4ef2fd7097a81d031d6e1bc68a779f536675fef Signed-off-by: elumalai --- diff --git a/osm_nbi/auth.py b/osm_nbi/auth.py index 0b3264f..ec33b1c 100644 --- a/osm_nbi/auth.py +++ b/osm_nbi/auth.py @@ -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 diff --git a/osm_nbi/authconn_internal.py b/osm_nbi/authconn_internal.py index d039f84..3f495d8 100644 --- a/osm_nbi/authconn_internal.py +++ b/osm_nbi/authconn_internal.py @@ -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"] = {} diff --git a/osm_nbi/nbi.cfg b/osm_nbi/nbi.cfg index 383b462..49f4985 100644 --- a/osm_nbi/nbi.cfg +++ b/osm_nbi/nbi.cfg @@ -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 diff --git a/osm_nbi/nbi.py b/osm_nbi/nbi.py index 8fa6152..343ac0d 100644 --- a/osm_nbi/nbi.py +++ b/osm_nbi/nbi.py @@ -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 diff --git a/osm_nbi/utils.py b/osm_nbi/utils.py index 73fc40f..9b48ee8 100644 --- a/osm_nbi/utils.py +++ b/osm_nbi/utils.py @@ -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] diff --git a/requirements.in b/requirements.in index 12fce5b..3cb40db 100644 --- a/requirements.in +++ b/requirements.in @@ -11,6 +11,7 @@ # under the License. aiohttp +cefevent CherryPy>=18.1.2 deepdiff jsonschema>=3.2.0 diff --git a/requirements.txt b/requirements.txt index 46d9b27..7de03f8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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