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 <deepika.e@tataelxsi.co.in>
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_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 @@
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 @@
: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 @@
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 @@
) # , 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 @@
# To be Confirmed
self.sess = None
+ self.cef_logger = cef_event_builder(config)
def validate_token(self, token):
"""
@@ -193,6 +195,18 @@
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 @@
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 @@
# 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.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 @@
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 @@
}
# 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 @@
# 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 @@
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 @@
"URL version '{}' not supported".format(version),
HTTPStatus.METHOD_NOT_ALLOWED,
)
+ if _id is not None:
+ url_id = _id
if (
kwargs
@@ -1744,6 +1781,36 @@
):
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 @@
"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 @@
"""
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 @@
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 @@
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 @@
# 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