diff --git a/.gitignore b/.gitignore index 0be1cd1e..52cbf26c 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,8 @@ lambda/mbj-newdwh2021-staging-NoticeToSlack/node_modules/* lambda/mbj-newdwh2021-staging-PublishFromLog/package-lock.json lambda/mbj-newdwh2021-staging-PublishFromLog/node_modules/* -# Pythonの仮想環境 +# ローカルの環境変数 +.env +# Python関連 .venv __pycache__ diff --git a/lambda/check-view-security-option/Dockerfile b/lambda/check-view-security-option/Dockerfile new file mode 100644 index 00000000..681c462a --- /dev/null +++ b/lambda/check-view-security-option/Dockerfile @@ -0,0 +1,14 @@ +FROM python:3.9 + +ENV WORKDIR /function/ +ENV TZ="Asia/Tokyo" +WORKDIR ${WORKDIR} + +COPY Pipfile Pipfile.lock ${WORKDIR} +RUN pip install pipenv --no-cache-dir && \ + pipenv install --system --deploy && \ + pip uninstall -y pipenv virtualenv-clone virtualenv +COPY check-view-option ./ + +ENTRYPOINT [ "/usr/local/bin/python", "-m", "awslambdaric" ] +CMD [ "main.handler" ] diff --git a/lambda/check-view-security-option/Pipfile b/lambda/check-view-security-option/Pipfile new file mode 100644 index 00000000..d6f3b1ee --- /dev/null +++ b/lambda/check-view-security-option/Pipfile @@ -0,0 +1,16 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +awslambdaric = "*" +boto3 = "*" +pymysql = "*" + +[dev-packages] +autopep8 = "*" +flake8 = "*" + +[requires] +python_version = "3.9" diff --git a/lambda/check-view-security-option/Pipfile.lock b/lambda/check-view-security-option/Pipfile.lock new file mode 100644 index 00000000..a93003e7 --- /dev/null +++ b/lambda/check-view-security-option/Pipfile.lock @@ -0,0 +1,209 @@ +{ + "_meta": { + "hash": { + "sha256": "9521eb0e33f733846811775b587cd94d7660f2e612b8efcbd622fd4d19122916" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.9" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "awslambdaric": { + "hashes": [ + "sha256:059c7a66d4470169e01620d93f07424b80d302e3736cd11e68373f293a41e396", + "sha256:0e90053614f0e5e5d6d6ae6d164412ce95b5d549c6fb0f6ff4290d77c5e9d3e5", + "sha256:11a365164efec105aa670259dfe473d9609da8f6f2e468790b2dfc24969bfff1", + "sha256:19da28e8c892b1c52a9db4d2b986af303932e3a4c4632eb0c5d5eb6a673c6022", + "sha256:2eb2fdb1ae0f84669d37f193f247fa115a282a7777e051ced3a33620d6280646", + "sha256:2efff2292fc8f8484eb094ffd77808a67815353be898a7f0b33ce51b841af691", + "sha256:387b94cb0358662ae2b203f0aa2af25e80c6a2019a6b569f733ecd993a4f53d2", + "sha256:38f8ae67ecb5b4e9f7fc42746ee39765dd7ddab359cb7e8ebfda1de0f0c0b059", + "sha256:3fd0e1b3891987fa7ebb0c08d24c76af5fc17466f6efdfa9a59848dfb23930ec", + "sha256:63a82d21d66146b3fde7eb6086abd058b75bdcab4a02b02afe0e8e4a45edfb5b", + "sha256:676a741ad8f3aa27d651bcf3a2b83d5cee815f99c8b2b9abef3cb22ca7b29698", + "sha256:9b0781bd41c20a2f2a0b018464a1daa376f663bd5eb7b0b6ba78f483681b1519", + "sha256:bad98f2f94cecc90b89ac4e1d4feed96eb664e13c29b7ce232444cc9358e0d36", + "sha256:d64dcba8da9dbea62644133a48c75376a37bfe0f84096ad73bf7fc5b2eb31fc7", + "sha256:d8f280b25d8a7ae6b6ff92a9bbc6567b984264be8ef3e0fcb0402a1247f6c75d", + "sha256:dad646f566aa7ec9b7179f16ca6741a2bea148abec6ed5947f86d00607e0a9a2", + "sha256:dc7072f642fdd215387d4921bbd5ac91b96a4a705bce5e7853622d09fe59f57d", + "sha256:fbbd24446ce2f876335b178f04aa4ec7ec480afc0f9621ebfdd5f55ad4b7c06e", + "sha256:fe76893a1b42bcee4c91c6456092d2a42455818756e8f62d50e8c5adb22fa9e7" + ], + "index": "pypi", + "version": "==2.0.4" + }, + "boto3": { + "hashes": [ + "sha256:4a7cf5fddb1626d25c5935c5a82afdff9c7fe2faac2a68d37edf0264b3a85127", + "sha256:bd0b94428ae7cc57904d3c903d9393bdf4dd2b1274d1c51749f27f5bd76953e1" + ], + "index": "pypi", + "version": "==1.24.18" + }, + "botocore": { + "hashes": [ + "sha256:20a866351f9f65cfe27edc21d755de60e17a1fbb1273d73fc0006ed0d6f8ef86", + "sha256:74426179c75debd77c6dcc2d66cfd506e52962e605d2b9f2dbca290474539c8b" + ], + "markers": "python_version >= '3.7'", + "version": "==1.27.18" + }, + "jmespath": { + "hashes": [ + "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", + "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe" + ], + "markers": "python_version >= '3.7'", + "version": "==1.0.1" + }, + "pymysql": { + "hashes": [ + "sha256:41fc3a0c5013d5f039639442321185532e3e2c8924687abe6537de157d403641", + "sha256:816927a350f38d56072aeca5dfb10221fe1dc653745853d30a216637f5d7ad36" + ], + "index": "pypi", + "version": "==1.0.2" + }, + "python-dateutil": { + "hashes": [ + "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", + "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.8.2" + }, + "s3transfer": { + "hashes": [ + "sha256:06176b74f3a15f61f1b4f25a1fc29a4429040b7647133a463da8fa5bd28d5ecd", + "sha256:2ed07d3866f523cc561bf4a00fc5535827981b117dd7876f036b0c1aca42c947" + ], + "markers": "python_version >= '3.7'", + "version": "==0.6.0" + }, + "simplejson": { + "hashes": [ + "sha256:034550078a11664d77bc1a8364c90bb7eef0e44c2dbb1fd0a4d92e3997088667", + "sha256:05b43d568300c1cd43f95ff4bfcff984bc658aa001be91efb3bb21df9d6288d3", + "sha256:0dd9d9c738cb008bfc0862c9b8fa6743495c03a0ed543884bf92fb7d30f8d043", + "sha256:10fc250c3edea4abc15d930d77274ddb8df4803453dde7ad50c2f5565a18a4bb", + "sha256:2862beabfb9097a745a961426fe7daf66e1714151da8bb9a0c430dde3d59c7c0", + "sha256:292c2e3f53be314cc59853bd20a35bf1f965f3bc121e007ab6fd526ed412a85d", + "sha256:2d3eab2c3fe52007d703a26f71cf649a8c771fcdd949a3ae73041ba6797cfcf8", + "sha256:2e7b57c2c146f8e4dadf84977a83f7ee50da17c8861fd7faf694d55e3274784f", + "sha256:311f5dc2af07361725033b13cc3d0351de3da8bede3397d45650784c3f21fbcf", + "sha256:344e2d920a7f27b4023c087ab539877a1e39ce8e3e90b867e0bfa97829824748", + "sha256:3fabde09af43e0cbdee407555383063f8b45bfb52c361bc5da83fcffdb4fd278", + "sha256:42b8b8dd0799f78e067e2aaae97e60d58a8f63582939af60abce4c48631a0aa4", + "sha256:4b3442249d5e3893b90cb9f72c7d6ce4d2ea144d2c0d9f75b9ae1e5460f3121a", + "sha256:55d65f9cc1b733d85ef95ab11f559cce55c7649a2160da2ac7a078534da676c8", + "sha256:5c659a0efc80aaaba57fcd878855c8534ecb655a28ac8508885c50648e6e659d", + "sha256:72d8a3ffca19a901002d6b068cf746be85747571c6a7ba12cbcf427bfb4ed971", + "sha256:75ecc79f26d99222a084fbdd1ce5aad3ac3a8bd535cd9059528452da38b68841", + "sha256:76ac9605bf2f6d9b56abf6f9da9047a8782574ad3531c82eae774947ae99cc3f", + "sha256:7d276f69bfc8c7ba6c717ba8deaf28f9d3c8450ff0aa8713f5a3280e232be16b", + "sha256:7f10f8ba9c1b1430addc7dd385fc322e221559d3ae49b812aebf57470ce8de45", + "sha256:8042040af86a494a23c189b5aa0ea9433769cc029707833f261a79c98e3375f9", + "sha256:813846738277729d7db71b82176204abc7fdae2f566e2d9fcf874f9b6472e3e6", + "sha256:845a14f6deb124a3bcb98a62def067a67462a000e0508f256f9c18eff5847efc", + "sha256:869a183c8e44bc03be1b2bbcc9ec4338e37fa8557fc506bf6115887c1d3bb956", + "sha256:8acf76443cfb5c949b6e781c154278c059b09ac717d2757a830c869ba000cf8d", + "sha256:8f713ea65958ef40049b6c45c40c206ab363db9591ff5a49d89b448933fa5746", + "sha256:934115642c8ba9659b402c8bdbdedb48651fb94b576e3b3efd1ccb079609b04a", + "sha256:9551f23e09300a9a528f7af20e35c9f79686d46d646152a0c8fc41d2d074d9b0", + "sha256:9a2b7543559f8a1c9ed72724b549d8cc3515da7daf3e79813a15bdc4a769de25", + "sha256:a55c76254d7cf8d4494bc508e7abb993a82a192d0db4552421e5139235604625", + "sha256:ad8f41c2357b73bc9e8606d2fa226233bf4d55d85a8982ecdfd55823a6959995", + "sha256:af4868da7dd53296cd7630687161d53a7ebe2e63814234631445697bd7c29f46", + "sha256:afebfc3dd3520d37056f641969ce320b071bc7a0800639c71877b90d053e087f", + "sha256:b59aa298137ca74a744c1e6e22cfc0bf9dca3a2f41f51bc92eb05695155d905a", + "sha256:bc00d1210567a4cdd215ac6e17dc00cb9893ee521cee701adfd0fa43f7c73139", + "sha256:c1cb29b1fced01f97e6d5631c3edc2dadb424d1f4421dad079cb13fc97acb42f", + "sha256:c94dc64b1a389a416fc4218cd4799aa3756f25940cae33530a4f7f2f54f166da", + "sha256:ceaa28a5bce8a46a130cd223e895080e258a88d51bf6e8de2fc54a6ef7e38c34", + "sha256:cff6453e25204d3369c47b97dd34783ca820611bd334779d22192da23784194b", + "sha256:d0b64409df09edb4c365d95004775c988259efe9be39697d7315c42b7a5e7e94", + "sha256:d4813b30cb62d3b63ccc60dd12f2121780c7a3068db692daeb90f989877aaf04", + "sha256:da3c55cdc66cfc3fffb607db49a42448785ea2732f055ac1549b69dcb392663b", + "sha256:e058c7656c44fb494a11443191e381355388443d543f6fc1a245d5d238544396", + "sha256:fed0f22bf1313ff79c7fc318f7199d6c2f96d4de3234b2f12a1eab350e597c06", + "sha256:ffd4e4877a78c84d693e491b223385e0271278f5f4e1476a4962dca6824ecfeb" + ], + "markers": "python_version >= '2.5' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==3.17.2" + }, + "six": { + "hashes": [ + "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", + "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.16.0" + }, + "urllib3": { + "hashes": [ + "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14", + "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", + "version": "==1.26.9" + } + }, + "develop": { + "autopep8": { + "hashes": [ + "sha256:44f0932855039d2c15c4510d6df665e4730f2b8582704fa48f9c55bd3e17d979", + "sha256:ed77137193bbac52d029a52c59bec1b0629b5a186c495f1eb21b126ac466083f" + ], + "index": "pypi", + "version": "==1.6.0" + }, + "flake8": { + "hashes": [ + "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d", + "sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d" + ], + "index": "pypi", + "version": "==4.0.1" + }, + "mccabe": { + "hashes": [ + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + ], + "version": "==0.6.1" + }, + "pycodestyle": { + "hashes": [ + "sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20", + "sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==2.8.0" + }, + "pyflakes": { + "hashes": [ + "sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c", + "sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.4.0" + }, + "toml": { + "hashes": [ + "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", + "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" + ], + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==0.10.2" + } + } +} diff --git a/lambda/check-view-security-option/check-view-option/aws/__init__.py b/lambda/check-view-security-option/check-view-option/aws/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/lambda/check-view-security-option/check-view-option/aws/s3.py b/lambda/check-view-security-option/check-view-option/aws/s3.py new file mode 100644 index 00000000..55c33c81 --- /dev/null +++ b/lambda/check-view-security-option/check-view-option/aws/s3.py @@ -0,0 +1,34 @@ +import boto3 +import environments +from constants import AWS_RESOURCE_S3, S3_RESPONSE_BODY, UTF8 + + +class S3Resource: + + def __init__(self, bucket_name: str) -> None: + self.__s3_resource = boto3.resource(AWS_RESOURCE_S3) + self.__s3_bucket = self.__s3_resource.Bucket(bucket_name) + + def get_object(self, object_key: str): + s3_object = self.__s3_bucket.Object(object_key) + response = s3_object.get() + return response[S3_RESPONSE_BODY].read().decode(UTF8) + + +class ConfigBucket: + __s3_resource: S3Resource = None + + def __init__(self) -> None: + self.__s3_resource = S3Resource(environments.CONFIG_BUCKET_NAME) + + @property + def check_target_schema_names(self): + return self.__s3_resource.get_object(environments.CHECK_TARGET_SCHEMA_NAMES_PATH) + + @property + def notice_mail_title_template(self): + return self.__s3_resource.get_object(environments.NOTICE_MAIL_TITLE_TEMPLATE_PATH) + + @property + def notice_mail_body_template(self): + return self.__s3_resource.get_object(environments.NOTICE_MAIL_BODY_TEMPLATE_PATH) diff --git a/lambda/check-view-security-option/check-view-option/aws/sns.py b/lambda/check-view-security-option/check-view-option/aws/sns.py new file mode 100644 index 00000000..d81e7307 --- /dev/null +++ b/lambda/check-view-security-option/check-view-option/aws/sns.py @@ -0,0 +1,31 @@ +import boto3 +import environments +from constants import AWS_RESOURCE_SNS + + +class SNSClient: + + def __init__(self) -> None: + self.__sns_client = boto3.client(AWS_RESOURCE_SNS) + + def publish(self, sns_topic_arn: str, subject: str, message: str) -> None: + publish_params = { + 'TopicArn': sns_topic_arn, + 'Subject': subject.rstrip('\n'), + 'Message': message + } + self.__sns_client.publish(**publish_params) + + +class SNSNotifier: + __sns_client: SNSClient = None + + def __init__(self) -> None: + self.__sns_client = SNSClient() + + def publish_to_mbj(self, subject: str, message: str): + self.__sns_client.publish(environments.MBJ_NOTICE_TOPIC, subject, message) + + def publish_to_nds(self, error_id: str, exception: Exception): + error_message = f'{error_id} のエラーが発生しました。ご確認ください\n詳細:{exception}' + self.__sns_client.publish(environments.NDS_NOTICE_TOPIC, environments.NDS_NOTICE_TITLE, error_message) diff --git a/lambda/check-view-security-option/check-view-option/aws/ssm.py b/lambda/check-view-security-option/check-view-option/aws/ssm.py new file mode 100644 index 00000000..7b618b59 --- /dev/null +++ b/lambda/check-view-security-option/check-view-option/aws/ssm.py @@ -0,0 +1,34 @@ +import boto3 +import environments +from constants import (AWS_RESOURCE_SSM, SSM_PARAMETER_RESPONSE, + SSM_PARAMETER_VALUE) + + +class SSMClient: + + def __init__(self) -> None: + self.__ssm_client = boto3.client(AWS_RESOURCE_SSM) + + def get_ssm_params(self, parameter_key: str, with_decryption: bool): + response = self.__ssm_client.get_parameter(Name=parameter_key, WithDecryption=with_decryption) + parameter_value = response[SSM_PARAMETER_RESPONSE][SSM_PARAMETER_VALUE] + return parameter_value + + +class SSMParameterStore: + __ssm_client: SSMClient = None + + def __init__(self) -> None: + self.__ssm_client = SSMClient() + + @property + def db_host(self): + return self.__ssm_client.get_ssm_params(environments.PARAM_NAME_DB_HOST, True) + + @property + def db_user_name(self): + return self.__ssm_client.get_ssm_params(environments.PARAM_NAME_DB_USER_NAME, True) + + @property + def db_user_password(self): + return self.__ssm_client.get_ssm_params(environments.PARAM_NAME_DB_USER_PASSWORD, True) diff --git a/lambda/check-view-security-option/check-view-option/constants.py b/lambda/check-view-security-option/check-view-option/constants.py new file mode 100644 index 00000000..ac6b2bfa --- /dev/null +++ b/lambda/check-view-security-option/check-view-option/constants.py @@ -0,0 +1,43 @@ +# logger +LOG_FORMAT = '[%(levelname)s]\t%(asctime)s\t%(message)s\n' +LOG_DATE_FORMAT = '%Y-%m-%d %H:%M:%S' +LOG_LEVEL_INFO = 'INFO' + +# environments +CHECK_TARGET_SCHEMA_NAMES_PATH = 'CHECK_TARGET_SCHEMA_NAMES_PATH' +CONFIG_BUCKET_NAME = 'CONFIG_BUCKET_NAME' +LOG_LEVEL = 'LOG_LEVEL' +MBJ_NOTICE_TOPIC = 'MBJ_NOTICE_TOPIC' +NDS_NOTICE_TOPIC = 'NDS_NOTICE_TOPIC' +NDS_NOTICE_TITLE = 'NDS_NOTICE_TITLE' +NOTICE_MAIL_BODY_TEMPLATE_PATH = 'NOTICE_MAIL_BODY_TEMPLATE_PATH' +NOTICE_MAIL_TITLE_TEMPLATE_PATH = 'NOTICE_MAIL_TITLE_TEMPLATE_PATH' +PARAM_NAME_DB_HOST = 'PARAM_NAME_DB_HOST' +PARAM_NAME_DB_USER_NAME = 'PARAM_NAME_DB_USER_NAME' +PARAM_NAME_DB_USER_PASSWORD = 'PARAM_NAME_DB_USER_PASSWORD' +TZ = 'TZ' + +# aws +AWS_RESOURCE_S3 = 's3' +AWS_RESOURCE_SSM = 'ssm' +AWS_RESOURCE_SNS = 'sns' +S3_RESPONSE_BODY = 'Body' +SSM_PARAMETER_RESPONSE = 'Parameter' +SSM_PARAMETER_NAME = 'Name' +SSM_PARAMETER_VALUE = 'Value' +RESPONSE_ERROR = 'Error' +RESPONSE_ERROR_CODE = 'Code' +RESPONSE_CODE_NO_SUCH_KEY = 'NoSuchKey' +RESPONSE_CODE_PARAMETER_NOT_FOUND = 'ParameterNotFound' + +# sql +DEFAULT_SCHEMA = 'INFORMATION_SCHEMA' +INFORMATION_SCHEMA_SECURITY_TYPE_INVOKER = 'INVOKER' +CONNECTION_TIMEOUT = 5 + +# system var +UTF8 = 'utf-8' +LAUNCH_ON_LOCAL = 'local' +CHECK_TARGET_SCHEMAS = 'check_target_schemas' +# メール本文に出力する不足ファイル名一覧のインデント +MAIL_INDENT = '\n  ' diff --git a/lambda/check-view-security-option/check-view-option/database.py b/lambda/check-view-security-option/check-view-option/database.py new file mode 100644 index 00000000..133ebbb4 --- /dev/null +++ b/lambda/check-view-security-option/check-view-option/database.py @@ -0,0 +1,40 @@ +import contextlib + +import pymysql +from pymysql.constants import CLIENT + +from constants import CONNECTION_TIMEOUT, DEFAULT_SCHEMA + + +class Database: + + __connection: pymysql.Connection = None + __host: str = None + __user: str = None + __password: str = None + __database: str = None + + def __init__(self, host: str, user: str, password: str) -> None: + self.__host = host + self.__user = user + self.__password = password + self.__database = DEFAULT_SCHEMA + + def connect(self): + connection = pymysql.connect(host=self.__host, user=self.__user, passwd=self.__password, + database=self.__database, connect_timeout=CONNECTION_TIMEOUT, + client_flag=CLIENT.MULTI_STATEMENTS) + self.__connection = connection + + @contextlib.contextmanager + def query(self, query: str): + if self.__connection is None: + raise Exception('データベースに接続されていません') + + with self.__connection.cursor() as cursor: + cursor.execute(query) + yield cursor + + def close(self): + self.__connection.close() + self.__connection = None diff --git a/lambda/check-view-security-option/check-view-option/dto/__init__.py b/lambda/check-view-security-option/check-view-option/dto/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/lambda/check-view-security-option/check-view-option/dto/no_security_option_view.py b/lambda/check-view-security-option/check-view-option/dto/no_security_option_view.py new file mode 100644 index 00000000..5a5fb39c --- /dev/null +++ b/lambda/check-view-security-option/check-view-option/dto/no_security_option_view.py @@ -0,0 +1,8 @@ +from dataclasses import dataclass + + +@dataclass +class NoSecurityOptionView: + schema_name: str + table_name: str + definer: str diff --git a/lambda/check-view-security-option/check-view-option/environments.py b/lambda/check-view-security-option/check-view-option/environments.py new file mode 100644 index 00000000..3a292a68 --- /dev/null +++ b/lambda/check-view-security-option/check-view-option/environments.py @@ -0,0 +1,24 @@ +import os + +from constants import (CHECK_TARGET_SCHEMA_NAMES_PATH, CONFIG_BUCKET_NAME, + LOG_LEVEL, LOG_LEVEL_INFO, MBJ_NOTICE_TOPIC, + NDS_NOTICE_TITLE, NDS_NOTICE_TOPIC, + NOTICE_MAIL_BODY_TEMPLATE_PATH, + NOTICE_MAIL_TITLE_TEMPLATE_PATH, PARAM_NAME_DB_HOST, + PARAM_NAME_DB_USER_NAME, PARAM_NAME_DB_USER_PASSWORD, + TZ) + +LOG_LEVEL = os.environ.get(LOG_LEVEL, LOG_LEVEL_INFO) +CHECK_TARGET_SCHEMA_NAMES_PATH = os.environ[CHECK_TARGET_SCHEMA_NAMES_PATH] +CONFIG_BUCKET_NAME = os.environ[CONFIG_BUCKET_NAME] +MBJ_NOTICE_TOPIC = os.environ[MBJ_NOTICE_TOPIC] +NDS_NOTICE_TOPIC = os.environ[NDS_NOTICE_TOPIC] +NDS_NOTICE_TITLE = os.environ[NDS_NOTICE_TITLE] +NOTICE_MAIL_BODY_TEMPLATE_PATH = os.environ[NOTICE_MAIL_BODY_TEMPLATE_PATH] +NOTICE_MAIL_TITLE_TEMPLATE_PATH = os.environ[NOTICE_MAIL_TITLE_TEMPLATE_PATH] + +PARAM_NAME_DB_HOST = os.environ[PARAM_NAME_DB_HOST] +PARAM_NAME_DB_USER_NAME = os.environ[PARAM_NAME_DB_USER_NAME] +PARAM_NAME_DB_USER_PASSWORD = os.environ[PARAM_NAME_DB_USER_PASSWORD] + +TZ = os.environ[TZ] diff --git a/lambda/check-view-security-option/check-view-option/exceptions.py b/lambda/check-view-security-option/check-view-option/exceptions.py new file mode 100644 index 00000000..aebd28c3 --- /dev/null +++ b/lambda/check-view-security-option/check-view-option/exceptions.py @@ -0,0 +1,34 @@ +from abc import ABCMeta + + +class MeDaCaException(Exception, metaclass=ABCMeta): + """MeDaCaシステム固有のカスタムエラークラス""" + + def __init__(self, error_id: str, message) -> None: + super().__init__(message) + self.error_id = error_id + + +class FileNotFoundException(MeDaCaException): + """S3のファイルが見つからない場合の例外""" + pass + + +class ParameterNotFoundException(MeDaCaException): + """パラメータストアのキーが見つからない場合の例外""" + pass + + +class DatabaseConnectionException(MeDaCaException): + """データベース接続に失敗した場合の例外""" + pass + + +class QueryExecutionException(MeDaCaException): + """クエリ実行に失敗した場合の例外""" + pass + + +class SNSPublishException(MeDaCaException): + """AmazonSNSへの通知に失敗した場合の例外""" + pass diff --git a/lambda/check-view-security-option/check-view-option/main.py b/lambda/check-view-security-option/check-view-option/main.py new file mode 100644 index 00000000..c40d8c51 --- /dev/null +++ b/lambda/check-view-security-option/check-view-option/main.py @@ -0,0 +1,282 @@ +""" +Viewセキュリティオプション付与チェック用Lambda関数のエントリーポイント +""" + +import json + +import botocore + +from aws.s3 import ConfigBucket +from aws.sns import SNSNotifier +from aws.ssm import SSMParameterStore +from constants import (CHECK_TARGET_SCHEMAS, + INFORMATION_SCHEMA_SECURITY_TYPE_INVOKER, MAIL_INDENT, + RESPONSE_CODE_NO_SUCH_KEY, + RESPONSE_CODE_PARAMETER_NOT_FOUND, RESPONSE_ERROR, + RESPONSE_ERROR_CODE) +from database import Database +from dto.no_security_option_view import NoSecurityOptionView +from environments import (CONFIG_BUCKET_NAME, MBJ_NOTICE_TOPIC, + NDS_NOTICE_TOPIC, NOTICE_MAIL_BODY_TEMPLATE_PATH, + NOTICE_MAIL_TITLE_TEMPLATE_PATH) +from exceptions import (DatabaseConnectionException, FileNotFoundException, + MeDaCaException, ParameterNotFoundException, + QueryExecutionException, SNSPublishException) +from medaca_logger import MeDaCaLogger + +logger = MeDaCaLogger.get_logger() + + +def handler(event, context): + try: + # ① 処理開始ログを出力する + logger.info('I-01-01', '処理開始 Viewセキュリティオプション付与チェック') + # ② 設定ファイル[チェック対象スキーマ名ファイル]を読み込む + logger.info('I-02-01', 'チェック対象スキーマ名ファイルを読み込み 開始') + check_target_schemas = read_check_target_schemas() + logger.info('I-02-02', f'チェック対象スキーマ名ファイルを読み込み 終了 チェック対象スキーマ名:{check_target_schemas}') + # ③ データベースに接続する + logger.info('I-03-01', 'データベースへの接続開始 開始') + # DB接続のためのパラメータ取得 + db_host, db_user_name, db_user_password = read_db_param_from_parameter_store() + connection = connection_database(db_host, db_user_name, db_user_password) + logger.info('I-03-02', 'データベースへの接続開始 成功') + # ④ Viewのオプションを確認するため、データを取得する + logger.info('I-04-01', 'Viewセキュリティオプション チェック開始') + check_result = fetch_view_security_options(connection, check_target_schemas) + logger.debug('D-04-01', f'取得データ:{check_result}') + if len(check_result) == 0: + logger.info('I-04-02', 'Viewセキュリティオプション 未設定のViewはありません。処理を終了します。') + return + logger.info('I-05-01', 'Viewセキュリティオプション 未設定のViewがあるため、メール送信処理を開始します。') + # ⑤ 取得できたデータをもとに、メール通知する文言を作成する + no_security_option_views = [NoSecurityOptionView(*row) for row in check_result] + logger.info( + 'I-05-02', f'通知メール(タイトル)テンプレートファイル読込 読込元:{CONFIG_BUCKET_NAME}/{NOTICE_MAIL_TITLE_TEMPLATE_PATH}') + mail_title = read_mail_title() + logger.info( + 'I-05-03', '通知メール(タイトル)テンプレートファイルを読み込みました') + logger.info( + 'I-05-04', f'通知メール(本文)テンプレートファイル読込 読込元:{CONFIG_BUCKET_NAME}/{NOTICE_MAIL_BODY_TEMPLATE_PATH}') + mail_body_template = read_mail_body_template() + logger.info( + 'I-05-05', '通知メール(本文)テンプレートファイルを読み込みました') + mail_body = make_notice_mail_body(no_security_option_views, mail_body_template) + + logger.info('I-05-06', f'メール送信指示をします 送信先トピック:{MBJ_NOTICE_TOPIC}') + notice_to_mbj(mail_title, mail_body) + logger.info('I-05-07', 'メール送信指示をしました') + + except MeDaCaException as e: + logger.exception(e.error_id, e) + logger.error('E-ERR-01', f'処理異常通知の送信指示をしました 通知先トピック:{NDS_NOTICE_TOPIC}') + notice_to_nds(e.error_id, e) + raise e + except Exception as e: + logger.exception('E-99', f'想定外のエラーが発生しました エラー内容:{e}') + logger.error('E-ERR-01', f'処理異常通知の送信指示をしました 通知先トピック:{NDS_NOTICE_TOPIC}') + notice_to_nds('E-99', e) + raise e + finally: + # ⑥ 処理終了ログを出力する + logger.info('I-06-01', '処理終了 Viewセキュリティオプション付与チェック') + + +def read_check_target_schemas() -> list: + """設定ファイル[チェック対象スキーマ名ファイル]を読み込む + + Raises: + FileNotFoundException: ファイルが読み込めなかったエラー + Exception: 想定外のエラー + + Returns: + list: チェック対象のスキーマ名のリスト + """ + try: + config_bucket = ConfigBucket() + check_target_schema_names = config_bucket.check_target_schema_names + return json.loads(check_target_schema_names)[CHECK_TARGET_SCHEMAS] + except botocore.exceptions.ClientError as e: + if e.response[RESPONSE_ERROR][RESPONSE_ERROR_CODE] == RESPONSE_CODE_NO_SUCH_KEY: + raise FileNotFoundException('E-02-01', f'チェック対象スキーマ名ファイルの読み込みに失敗しました エラー内容:{e}') + else: + raise Exception(e) + + +def read_db_param_from_parameter_store() -> tuple: + """パラメータストアからDB接続情報を取得する + + Raises: + ParameterNotFoundException: 指定されたパラメータが存在しないエラー + Exception: 想定外のエラー + + Returns: + tuple: DB接続情報 + """ + try: + parameter_store = SSMParameterStore() + db_host = parameter_store.db_host + db_user_name = parameter_store.db_user_name + db_user_password = parameter_store.db_user_password + return db_host, db_user_name, db_user_password + except botocore.exceptions.ClientError as e: + if e.response[RESPONSE_ERROR][RESPONSE_ERROR_CODE] == RESPONSE_CODE_PARAMETER_NOT_FOUND: + raise ParameterNotFoundException('E-03-01', f'パラメータストアの取得に失敗しました エラー内容:{e}') + else: + raise Exception(e) + + +def connection_database(host: str, user_name: str, password: str) -> Database: + """データベース接続 + + Args: + host (str): DBホスト + user_name (str): DBユーザー名 + password (str): DBパスワード + + Raises: + DatabaseConnectionException: データベースへの接続に失敗したエラー + + Returns: + Database: データベース操作クラス + """ + try: + database = Database(host, user_name, password) + database.connect() + return database + except Exception as e: + raise DatabaseConnectionException('E-03-02', f'データベースへの接続に失敗しました エラー内容:{e}') + + +def fetch_view_security_options(connection: Database, check_target_schemas: list) -> tuple: + """SECURITY INVOKERのついていないViewの一覧を取得する + + Args: + connection (Database): 接続済みDB操作クラス + check_target_schemas (str): チェック対象のスキーマ一覧 + + Raises: + QueryExecutionException: クエリ実行エラー + + Returns: + tuple: クエリ実行結果 + """ + + select_view_security_option_sql = f""" + SELECT + TABLE_SCHEMA, + TABLE_NAME, + DEFINER + FROM + INFORMATION_SCHEMA.VIEWS + WHERE + TABLE_SCHEMA IN ( + {','.join([f"'{schema_name}'" for schema_name in check_target_schemas])} + ) + AND SECURITY_TYPE <> '{INFORMATION_SCHEMA_SECURITY_TYPE_INVOKER}' + """ + try: + with connection.query(select_view_security_option_sql) as cursor: + result = cursor.fetchall() + return result + except Exception as e: + raise QueryExecutionException('E-04-01', f'Viewセキュリティオプションチェックに失敗しました エラー内容:{e}') + + +def make_notice_mail_body(no_security_option_views: list[NoSecurityOptionView], mail_body_template: str) -> tuple[str]: + """メール本文を生成します + + Args: + view_security_options (list[NoSecurityOptionView]): チェック対象のビュー一覧 + mail_body_template (str): メール本文のテンプレート + + Returns: + tuple[str]: メール本文 + """ + mail_message = MAIL_INDENT.join( + [f'{option.schema_name}.{option.table_name}' for option in no_security_option_views]) + mail_body = mail_body_template.format(no_option_views=mail_message) + return mail_body + + +def read_mail_title() -> str: + """メールタイトルを読み込む + + Raises: + FileNotFoundException: ファイルが読み込めなかったエラー + Exception: 想定外のエラー + + Returns: + str: メールタイトル + """ + + try: + config_bucket = ConfigBucket() + mail_title = config_bucket.notice_mail_title_template + except botocore.exceptions.ClientError as e: + if e.response[RESPONSE_ERROR][RESPONSE_ERROR_CODE] == RESPONSE_CODE_NO_SUCH_KEY: + raise FileNotFoundException('E-05-01', f'通知メール(タイトル)テンプレートファイルの読み込みに失敗しました エラー内容:{e}') + else: + raise Exception(e) + + return mail_title + + +def read_mail_body_template() -> str: + """メール本文を読み込む + + Raises: + FileNotFoundException: ファイルが読み込めなかったエラー + Exception: 想定外のエラー + + Returns: + str: メール本文 + """ + try: + config_bucket = ConfigBucket() + mail_body_template = config_bucket.notice_mail_body_template + except botocore.exceptions.ClientError as e: + if e.response[RESPONSE_ERROR][RESPONSE_ERROR_CODE] == RESPONSE_CODE_NO_SUCH_KEY: + raise FileNotFoundException('E-05-02', f'通知メール(本文)テンプレートファイルの読み込みに失敗しました エラー内容:{e}') + else: + raise Exception(e) + + return mail_body_template + + +def notice_to_mbj(mail_title: str, mail_body: str) -> None: + """MBJへ通知を行います + + Args: + mail_title (str): メールタイトル + mail_body (str): メール本文 + + Raises: + SNSPublishException: SNSでの通知失敗した場合のエラー + """ + try: + notifier = SNSNotifier() + notifier.publish_to_mbj(mail_title, mail_body) + except Exception as e: + raise SNSPublishException('E-98', f'通知の送信指示に失敗しました エラー内容:{e}') + + +def notice_to_nds(error_id: str, error_message: str) -> None: + """NDSに処理以上通知を行う + + Args: + error_id (str): エラーID + error_message (str): エラーメッセージ + Raises: + SNSPublishException: SNSでの通知失敗した場合のエラー + """ + try: + notifier = SNSNotifier() + notifier.publish_to_nds(error_id, error_message) + except Exception as e: + raise SNSPublishException('E-98', f'通知の送信指示に失敗しました エラー内容:{e}') + + +# ローカル実行用 +if __name__ == '__main__': + handler({}, {}) diff --git a/lambda/check-view-security-option/check-view-option/medaca_logger.py b/lambda/check-view-security-option/check-view-option/medaca_logger.py new file mode 100644 index 00000000..fac70c1c --- /dev/null +++ b/lambda/check-view-security-option/check-view-option/medaca_logger.py @@ -0,0 +1,60 @@ +import datetime +import logging +import sys +from zoneinfo import ZoneInfo + +from constants import LAUNCH_ON_LOCAL, LOG_DATE_FORMAT, LOG_FORMAT +from environments import LOG_LEVEL, TZ + + +class SingletonLogger: + __logger: logging.Logger = None + + def __init__(self) -> None: + # logger設定 + logger = logging.getLogger() + formatter = logging.Formatter( + LOG_FORMAT, + LOG_DATE_FORMAT + ) + formatter.converter = lambda *arg: datetime.datetime.now(ZoneInfo(TZ)).timetuple() + # ローカル環境で動かす場合、標準出力ハンドラーを追加する + # AWS Lambda上では`LambdaLoggerHandler`がデフォルトでセットされている + if len(sys.argv) == 2 and sys.argv[1] == LAUNCH_ON_LOCAL: + localHandler = logging.StreamHandler() + logger.addHandler(localHandler) + for handler in logger.handlers: + handler.setFormatter(formatter) + level = logging.getLevelName(LOG_LEVEL) + logger.setLevel(level) + + self.__logger = logger + + def debug(self, log_id: str, msg: str): + self._log(logging.DEBUG, log_id, msg) + + def info(self, log_id: str, msg: str): + self._log(logging.INFO, log_id, msg) + + def warning(self, log_id: str, msg: str): + self._log(logging.WARNING, log_id, msg) + + def error(self, log_id: str, msg: str): + self._log(logging.ERROR, log_id, msg) + + def exception(self, log_id: str, msg: str): + self._log(logging.ERROR, log_id, msg, exc_info=True) + + def _log(self, log_level: int, log_id: str, msg: str, exc_info=False): + self.__logger.log(log_level, f'{log_id} {msg}', exc_info=exc_info) + + +class MeDaCaLogger: + __unique_instance: logging.Logger = None + + @staticmethod + def get_logger() -> SingletonLogger: + # インスタンス未生成の場合、唯一のインスタンスを生成する + if not MeDaCaLogger.__unique_instance: + MeDaCaLogger.__unique_instance = SingletonLogger() + return MeDaCaLogger.__unique_instance diff --git a/s3/config/view_check/check_target_schemas.json b/s3/config/view_check/check_target_schemas.json new file mode 100644 index 00000000..4a7df05b --- /dev/null +++ b/s3/config/view_check/check_target_schemas.json @@ -0,0 +1,3 @@ +{ + "check_target_schemas": ["custom01", "custom02", "custom03"] +} diff --git a/s3/config/view_check/check_view_security_option_mail_body.config b/s3/config/view_check/check_view_security_option_mail_body.config new file mode 100644 index 00000000..5457eafc --- /dev/null +++ b/s3/config/view_check/check_view_security_option_mail_body.config @@ -0,0 +1,6 @@ +宛先各位 + customスキーマの以下のviewに「SQL SECURITY INVOKER」オプションが指定されておりません。viewを再作成しオプションを指定してください。 +  {no_option_views} + + 尚、本メールはシステム自動送信ですので、返信できません。 + 本件に関する問い合わせは、IT部門ゴザリ様にお願いいたします。 diff --git a/s3/config/view_check/check_view_security_option_mail_title.config b/s3/config/view_check/check_view_security_option_mail_title.config new file mode 100644 index 00000000..a85c0134 --- /dev/null +++ b/s3/config/view_check/check_view_security_option_mail_title.config @@ -0,0 +1 @@ +【MeDaCaシステム通知】view参照制限オプション指定漏れを検出しました \ No newline at end of file