diff --git a/.github/workflows/github-actions-build.yml b/.github/workflows/github-actions-build.yml new file mode 100644 index 0000000..dd0f905 --- /dev/null +++ b/.github/workflows/github-actions-build.yml @@ -0,0 +1,92 @@ +name: Python package + +on: [push] + +jobs: + build_release: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.10"] + + steps: + - run: echo "job automatically triggered by a ${{ github.event_name }} event." + + - uses: actions/checkout@v3 + + - uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + + - name: Install Python requirements + run: | + pip install -r requirements.txt + + # + # Build release and run Appinspect + # + + - name: Build the release and submit to Splunk Appinspect vetting + env: + SPLUNK_BASE_LOGIN: ${{ vars.SPLUNK_BASE_LOGIN }} + SPLUNK_BASE_PASSWD: ${{ secrets.SPLUNK_BASE_PASSWD }} + run: | + cd build; python3 build.py --keep --submitappinspect --userappinspect "$SPLUNK_BASE_LOGIN" --passappinspect "$SPLUNK_BASE_PASSWD" + id: build_and_submit_appinspect + + # + # Archive artifacts + # + + - name: Archive Appinspect html report + if: always() + uses: actions/upload-artifact@v3 + with: + name: appinspect-report-html + path: output/report_appinspect.html + + - name: Archive Appinspect json report + if: always() + uses: actions/upload-artifact@v3 + with: + name: appinspect-report-json + path: output/report_appinspect.json + + - name: Archive tgz application + if: always() + uses: actions/upload-artifact@v3 + with: + path: output/*.tgz + + - name: Archive sha256sum + if: always() + uses: actions/upload-artifact@v3 + with: + name: release-sha256.txt + path: output/release-sha256.txt + + - name: Show output directory content + run: | + ls -ltr output/ + + - name: Retrieve version number + run: | + echo "VERSION_NUMBER=$(cat output/version.txt)" >> $GITHUB_ENV + + - name: Show version number + run: | + echo "Version number is ${{ env.VERSION_NUMBER }}" + + - name: Retrieve build number + run: | + echo "BUILD_NUMBER=$(cat output/build.txt)" >> $GITHUB_ENV + + - name: Show build number + run: | + echo "Build number is ${{ env.BUILD_NUMBER }}" + + # + # + # + + - run: echo "End of process, job status ${{ job.status }}." diff --git a/.readthedocs.yaml b/.readthedocs.yaml index de375d3..dc2eaa4 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -23,6 +23,6 @@ sphinx: # - pdf # Optionally declare the Python requirements required to build your docs -#python: -# install: -# - requirements: docs/requirements.txt +python: + install: + - requirements: docs/requirements.txt diff --git a/build/appinspect_manual.sh b/build/appinspect_manual.sh deleted file mode 100755 index 4a86611..0000000 --- a/build/appinspect_manual.sh +++ /dev/null @@ -1,120 +0,0 @@ -#!/bin/bash - -#set -x -unset username -unset uuid - -echo -n "Enter your Splunk Base login: " -read username - -echo "Attempting login to appinspect API..." - -export appinspect_token=$(curl -X GET \ - -u ${username} \ - --url "https://api.splunk.com/2.0/rest/login/splunk" -s | sed 's/%//g' | jq -r .data.token) - -case "$appinspect_token" in -"null") - echo "ERROR: login to appinspect API has failed, an authentication token could be not be generated." - exit 1 - ;; -*) - echo "SUCCESS: Authentication was successful and we got a token." - ;; -esac - -for app in $(ls *.tgz); do - - echo -n "RUN: Please confirm submitting the app ${app} to appinspect API vetting (yes / no) ? " - read submit - case ${submit} in - y | yes | Yes) - echo "RUN: Please wait while submitting to appinspect..." - uuid=$(curl -X POST \ - -H "Authorization: bearer ${appinspect_token}" \ - -H "Cache-Control: no-cache" \ - -s \ - -F "app_package=@${app}" \ - -F "included_tags=advanced_xml,alert_actions_conf,appapproval,cloud,custom_search_commands_v2,custom_search_commands,custom_visualizations,custom_workflow_actions,deprecated_feature,developer_guidance,django_bindings,inputs_conf,markdown,malicious,modular_input(s),offensive,packaging_standards,private_app,removed_feature,restmap_config,savedsearches,security,service,web_conf,splunk_5_0,splunk_6_0,splunk_6_1,splunk_6_2,splunk_6_3,splunk_6_4,splunk_6_5,splunk_6_6,splunk_7_0,splunk_7_1,splunk_7_2,splunk_7_3,splunk_8_0" \ - --url "https://appinspect.splunk.com/v1/app/validate" | jq -r .links | grep href | head -1 | awk -F\" '{print $4}' | awk -F\/ '{print $6}') - - if [ $? -eq 0 ]; then - echo "INFO: upload was successful, polling status..." - - status=$(curl -X GET \ - -s \ - -H "Authorization: bearer ${appinspect_token}" \ - --url https://appinspect.splunk.com/v1/app/validate/status/${uuid} | jq -r .status) - - while [ $status != "SUCCESS" ]; do - - echo -e "INFO: appinspect is currently running: \n" - echo "INFO: Sleeping 2 seconds..." - - curl -X GET \ - -s \ - -H "Authorization: bearer ${appinspect_token}" \ - --url https://appinspect.splunk.com/v1/app/validate/status/${uuid} | jq - sleep 2 - status=$(curl -X GET \ - -s \ - -H "Authorization: bearer ${appinspect_token}" \ - --url https://appinspect.splunk.com/v1/app/validate/status/${uuid} | jq -r .status) - - done - - case ${status} in - "SUCCESS") - echo "INFO: appinspect review was successfully proceeded:" - curl -X GET \ - -s \ - -H "Authorization: bearer ${appinspect_token}" \ - --url https://appinspect.splunk.com/v1/app/validate/status/${uuid} | jq . - echo -e "RUN: Download the HTML report in the current directory? (yes / no) " - read download - - case ${download} in - y | yes | Yes) - datetime=$(date '+%m%d%Y_%H%M%S') - filename="appinspect_report_${datetime}.html" - curl -X GET \ - -s \ - -H "Authorization: bearer ${appinspect_token}" \ - -H "Cache-Control: no-cache" \ - -H "Content-Type: text/html" \ - --url "https://appinspect.splunk.com/v1/app/report/${uuid}" \ - -o ${filename} - - echo "INFO: report downloaded to file ${filename} in the current directory." - - ;; - n | no | No) - echo "INFO: Operation completed for ${app} - thank you." - ;; - esac - - ;; - "*") - echo "ERROR: appinspect review was not successful!" - ;; - - esac - - else - echo "ERROR: upload has failed!" - break - - fi - - ;; - n | no | No) - - echo "INFO: Application was not submitted" - - ;; - - esac - -done - -exit 0 diff --git a/build/build.py b/build/build.py new file mode 100755 index 0000000..d27d0e5 --- /dev/null +++ b/build/build.py @@ -0,0 +1,589 @@ +#!/usr/bin/env python +# coding=utf-8 + +from __future__ import absolute_import, division, print_function, unicode_literals + +__author__ = "TrackMe Limited" +__version__ = "0.1.0" +__maintainer__ = "TrackMe Limited" +__status__ = "PRODUCTION" + +import os, sys +import time +import shutil +import tarfile +import json +import logging +import argparse +import glob +import subprocess +import configparser +import hashlib +import requests + +# load libs +sys.path.append("libs") +from tools import ( + cd, + login_appinspect, + submit_appinspect, + verify_appinspect, + download_htmlreport_appinspect, + download_jsonreport_appinspect, +) + +# Args +parser = argparse.ArgumentParser() +parser.add_argument("--debug", dest="debug", action="store_true") +parser.add_argument("--keep", dest="keep", action="store_true") +parser.add_argument("--submitappinspect", dest="submitappinspect", action="store_true") +parser.add_argument("--userappinspect", dest="userappinspect") +parser.add_argument("--passappinspect", dest="passappinspect") +parser.set_defaults(debug=False) +parser.set_defaults(keep=False) +parser.set_defaults(submitappinspect=False) +args = parser.parse_args() + +# Set debug boolean +if args.debug: + debug = True +else: + debug = False + +# Set keep boolean +if args.keep: + keep = True +else: + keep = False + +# Set appinspect_vetting +if args.submitappinspect: + submitappinspect = True +else: + submitappinspect = False + +# Set appinspect_username +if args.userappinspect: + userappinspect = args.userappinspect +else: + userappinspect = False + +# Set appinspect_password +if args.passappinspect: + passappinspect = args.passappinspect +else: + passappinspect = False + +# set logging +root = logging.getLogger() +root.setLevel(logging.DEBUG) +handler = logging.StreamHandler(sys.stdout) +handler.setLevel(logging.DEBUG) +formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s") +handler.setFormatter(formatter) +root.addHandler(handler) + +if debug: + root.setLevel(logging.DEBUG) + handler.setLevel(logging.DEBUG) +else: + root.setLevel(logging.INFO) + handler.setLevel(logging.INFO) + +# version file +version_file = "../version.json" + +# build_dir +build_dir = "../build" + +# package dir +package_dir = "../package" + +# output_dir +output_dir = "../output" + + +# +# functions +# + + +# get release number +def get_release_number(): + # Get the version release number + try: + with open(version_file) as f: + version_data = json.load(f) + version_release_number = version_data["version"] + logging.info( + '**** TrackMe package generation, version="{}" ****'.format( + version_release_number + ) + ) + + except Exception as e: + logging.error( + 'Failed to retrieve the version release number, exception="{}"'.format(e) + ) + version_release_number = "1.0.0" + + # return + return version_release_number + + +# get app id +def get_app_id(): + # Get the version release number + try: + with open(version_file) as f: + version_data = json.load(f) + logging.info('version_data="{}"'.format(version_data)) + appID = version_data["appID"] + logging.info('**** TrackMe app generation, appID="{}" ****'.format(appID)) + + except Exception as e: + logging.error('Failed to retrieve the appID, exception="{}"'.format(e)) + raise ValueError('Failed to retrieve the appID, exception="{}"'.format(e)) + + # return + return appID + + +# gen organisation applications +def gen_app(): + # get release number + version_release_number = get_release_number() + + # get app ID + appID = get_app_id() + + # Purge any existing tgz in the output directory + files = glob.glob(os.path.join(output_dir, "*.tgz")) + for file_name in files: + logging.debug( + 'Attempting to remove existing tgz archive="{}"'.format(file_name) + ) + if os.path.isfile(file_name): + try: + os.remove(file_name) + logging.debug('Archive="{}" was deleted successfully'.format(file_name)) + except Exception as e: + logging.error( + 'Archive="{}" could not be deleted, exception="{}"'.format( + file_name, e + ) + ) + + # Purge Appinspect previous reports + files = glob.glob(os.path.join(output_dir, "report_*.html")) + for file_name in files: + logging.debug('Attempting to remove report="{}"'.format(file_name)) + if os.path.isfile(file_name): + try: + os.remove(file_name) + logging.debug('Report="{}" was deleted successfully'.format(file_name)) + except Exception as e: + logging.error( + 'Report="{}" could not be deleted, exception="{}"'.format( + file_name, e + ) + ) + + files = glob.glob(os.path.join(output_dir, "report_*.json")) + for file_name in files: + logging.debug('Attempting to remove report="{}"'.format(file_name)) + if os.path.isfile(file_name): + try: + os.remove(file_name) + logging.debug('Report="{}" was deleted successfully'.format(file_name)) + except Exception as e: + logging.error( + 'Report="{}" could not be deleted, exception="{}"'.format( + file_name, e + ) + ) + + # Set app_root + app_root = os.path.join(output_dir, appID) + + # Remove current app if it exists + if os.path.isdir(app_root): + logging.debug( + 'appID="{}" purging existing directory app_root="{}"'.format( + appID, app_root + ) + ) + try: + shutil.rmtree(app_root) + except Exception as e: + logging.error( + 'appID="{}", failed to purge the existing build directory="{}" with exception="{}"'.format( + appID, app_root, e + ) + ) + raise ValueError( + 'appID="{}", failed to purge the existing build directory="{}" with exception="{}"'.format( + appID, app_root, e + ) + ) + + # Package + with cd("../"): + logging.info("Call ucc-gen") + subprocess.run(["ucc-gen", "build", "--ta-version", version_release_number]) + + # Package + with cd(output_dir): + # read app.conf to retrieve the current build + try: + config = configparser.ConfigParser() + config.read(os.path.join(app_root, "default", "app.conf")) + build_number = config["install"]["build"] + + except Exception as e: + logging.error(f'failed to retrieve the build number with exception="{e}"') + sys.exit(1) + + # save the version number to a simple text file for further usage + version_number_file = "version_full.txt" + with open(version_number_file, "w") as f: + f.write(f"{str(version_release_number)}\n") + + version_number_file = "version.txt" + with open(version_number_file, "w") as f: + f.write(f"{str(version_release_number).replace('.', '')}\n") + + # save the build number to a simple text file for further usage + build_number_file = "build.txt" + with open(build_number_file, "w") as f: + f.write(f"{build_number}\n") + + # Verify and delete any "*.pyc" files in the appID directory + for dirpath, dirnames, filenames in os.walk(appID): + for file in filenames: + if file.endswith(".pyc"): + os.remove(os.path.join(dirpath, file)) + + # Verify that there are no *.pyc file in app_root, and purge otherwise + purged_files = glob.glob(os.path.join(app_root, "**/*.pyc"), recursive=True) + for file_name in purged_files: + logging.debug('Attempting to remove pyc file="{}"'.format(file_name)) + if os.path.isfile(file_name): + try: + os.remove(file_name) + logging.debug( + 'pyc file="{}" was deleted successfully'.format(file_name) + ) + except Exception as e: + logging.error( + 'pyc file="{}" could not be deleted, exception="{}"'.format( + file_name, e + ) + ) + + # Verify and delete any hidden files (files starting with a dot) + hidden_files = glob.glob(os.path.join(app_root, "**/.*"), recursive=True) + for file_name in hidden_files: + logging.debug('Attempting to remove hidden file="{}"'.format(file_name)) + if os.path.isfile(file_name): + try: + os.remove(file_name) + logging.debug( + 'Hidden file="{}" was deleted successfully'.format(file_name) + ) + except Exception as e: + logging.error( + 'Hidden file="{}" could not be deleted, exception="{}"'.format( + file_name, e + ) + ) + + # Verify and delete any hidden directories (directories starting with a dot) + hidden_dirs = glob.glob(os.path.join(app_root, "**/.*"), recursive=True) + for dir_name in hidden_dirs: + if os.path.isdir(dir_name): + logging.debug( + 'Attempting to remove hidden directory="{}"'.format(dir_name) + ) + try: + shutil.rmtree(dir_name) + logging.debug( + 'Hidden directory="{}" was deleted successfully'.format( + dir_name + ) + ) + except Exception as e: + logging.error( + 'Hidden directory="{}" could not be deleted, exception="{}"'.format( + dir_name, e + ) + ) + + # gen tar + tar_file = f"{app_root}_v{str(version_release_number).replace('.', '')}_{build_number}.tgz" + out = tarfile.open(tar_file, mode="w:gz") + + try: + out.add(str(appID)) + except Exception as e: + logging.error( + f'appID="{appID}", archive file="{tar_file}" creation failed with exception="{e}"' + ) + raise ValueError( + f'appID="{appID}", archive file="{tar_file}" creation failed with exception="{e}"' + ) + finally: + logging.info( + f'appID="{appID}", Achive tar file creation, archive_file="{tar_file}"' + ) + out.close() + + # get sha256 + logging.info("Get and store the sha256 control sum") + + with open(tar_file, "rb") as f: + bytes = f.read() # read entire file as bytes + readable_hash = hashlib.sha256(bytes).hexdigest() + logging.info('sha256 control sum="{}"'.format(readable_hash)) + + sha256_file = "release-sha256.txt" + with open(sha256_file, "w") as f: + f.write( + readable_hash + + "\t" + + str(appID) + + "_v" + + str(version_release_number).replace(".", "") + + "_" + + str(build_number) + + ".tgz" + + "\n" + ) + + # log info + logging.info( + '**** TrackMe app generation terminated, appID="{}", build_number="{}", sha256="{}" ****'.format( + appID, build_number, readable_hash + ) + ) + + # Remove build directories + if not keep: + if os.path.isdir(app_root): + logging.debug( + 'appID="{}", purging existing directory app_root="{}"'.format( + appID, app_root + ) + ) + try: + shutil.rmtree(app_root) + except Exception as e: + logging.error( + 'appID="{}", failed to purge the build directory="{}" with exception="{}"'.format( + appID, app_root, e + ) + ) + raise ValueError( + 'appID="{}", failed to purge the build directory="{}" with exception="{}"'.format( + appID, app_root, e + ) + ) + + +# Generate the application release +gen_app() + +# If requested, perform the validation through Appinspect + +# get app ID +appID = get_app_id() + +if submitappinspect and (not userappinspect or not passappinspect): + logging.error( + "Appinspect vetting process request but login or password were not provided" + ) + sys.exit(1) + +if submitappinspect and userappinspect and passappinspect: + # login to Appinspect + appinspect_token = login_appinspect(userappinspect, passappinspect) + + if appinspect_token: + logging.info("Appsinspect: successfully logged in Appinspect API") + + # use session pooling + with requests.Session() as session: + # loop + with cd(output_dir): + appinspect_requestids = [] + + # Purge any existing tgz in the output directory + files = glob.glob(os.path.join(output_dir, appID + "*.tgz")) + for file_name in files: + if os.path.isfile(file_name): + logging.info( + 'Submitting to Appinspect API="{}"'.format(file_name) + ) + + # set None + appinspect_response = None + + # submit + appinspect_response = submit_appinspect( + session, appinspect_token, file_name + ) + + # append to the list + if appinspect_response: + appinspect_requestids.append( + json.loads(appinspect_response)["request_id"] + ) + + # Wait for all Appinspect vettings to be processed + logging.debug( + 'Appinspect request_ids="{}"'.format(appinspect_requestids) + ) + + # sleep 2 seconds + time.sleep(2) + + # loop per request id + for request_id in appinspect_requestids: + # check appinspect status + vetting_response = verify_appinspect( + session, appinspect_token, request_id + ) + + if not vetting_response: + raise Exception( + "Appinspect verification has permanently failed." + ) + else: + vetting_status = json.loads(vetting_response)["status"] + + # init counter + is_inprogress = True + attempts_counter = 0 + max_count = 900 + wait_time = 15 + + # Allow up to 150 attempts + while is_inprogress and attempts_counter < max_count: + attempts_counter += 1 + + try: + vetting_response = verify_appinspect( + session, appinspect_token, request_id + ) + if vetting_response: + vetting_status = json.loads(vetting_response)["status"] + + if vetting_status == "SUCCESS": + logging.info( + 'Appinspect request_id="{}" was successfully processed'.format( + request_id + ) + ) + is_inprogress = False + break + + elif vetting_status == "FAILURE": + logging.error( + 'Appinspect request_id="{}" reported failed, vetting was not accepted!'.format( + request_id + ) + ) + is_inprogress = False + break + + elif vetting_status == "PROCESSING": + logging.info( + 'Appinspect request_id="{}" is in progress, please wait.'.format( + request_id + ) + ) + is_inprogress = True + time.sleep(wait_time) + + else: + logging.error( + 'Appinspect request_id="{}" status is unknown or not expected, review the report if available'.format( + request_id + ) + ) + is_inprogress = False + break + + else: + # sleep 5 seconds + time.sleep(5) + + except Exception as e: + logging.warn( + f"temporary failure to retrieve Appinspect status, will sleep and try again" + ) + # sleep 5 seconds + time.sleep(5) + + # Download the HTML report + appinspect_report = download_htmlreport_appinspect( + session, appinspect_token, request_id + ) + + if appinspect_report: + f = open( + os.path.join(output_dir, "report_appinspect.html"), "w" + ) + f.write(appinspect_report) + f.close() + logging.info( + 'Appinspect written to report="{}"'.format( + os.path.join(output_dir, "report_appinspect.html") + ) + ) + + # Download the JSON report + appinspect_report = download_jsonreport_appinspect( + session, appinspect_token, request_id + ) + + if appinspect_report: + f = open( + os.path.join(output_dir, "report_appinspect.json"), "w" + ) + f.write(json.dumps(json.loads(appinspect_report), indent=4)) + f.close() + logging.info( + 'Appinspect written to report="{}"'.format( + os.path.join(output_dir, "report_appinspect.json") + ) + ) + + # Load the json dict + appinspect_report_dict = json.loads(appinspect_report) + + count_failure = int(appinspect_report_dict["summary"]["failure"]) + count_error = int(appinspect_report_dict["summary"]["failure"]) + + if count_failure == 0 and count_error == 0: + logging.info( + 'Appinspect request_id="{}" was successfully vetted, summary="{}"'.format( + request_id, + json.dumps(appinspect_report_dict["summary"], indent=4), + ) + ) + else: + logging.error( + 'Appinspect request_id="{}" could not be vetted, review the report for more information, summary="{}"'.format( + request_id, + json.dumps(appinspect_report_dict["summary"], indent=4), + ) + ) + raise ValueError( + 'Appinspect request_id="{}" could not be vetted, review the report for more information, summary="{}"'.format( + request_id, + json.dumps(appinspect_report_dict["summary"], indent=4), + ) + ) + +sys.exit(0) diff --git a/build/build.sh b/build/build.sh deleted file mode 100755 index 0371ce4..0000000 --- a/build/build.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env bash -set -x - -# for Mac OS X -export COPYFILE_DISABLE=true - -PWD=$(pwd) -OUTDIR="output" - -app="TA-ms-teams-alert-action" -version=$(grep 'version =' ../version.txt | head -1 | awk '{print $3}' | sed 's/\.//g') -ta_version=$(grep 'version =' ../version.txt | head -1 | awk '{print $3}') - -cd ../ -ucc-gen --ta-version "$ta_version" - -cd "${OUTDIR}" -find . -name "*.pyc" -type f -exec rm -f {} \; -rm -f *.tgz -if [ -f ${app}/metadata/local.meta ]; then - rm -f ${app}/metadata/local.meta -fi -tar -czf ${app}_${version}.tgz ${app} -echo "Wrote: ${app}_${version}.tgz" - -sha256=$(sha256sum ${app}_${version}.tgz) -echo "Wrote: ${sha256} in $OUTDIR" -echo ${sha256} > release-sha256.txt - -exit 0 \ No newline at end of file diff --git a/build/libs/tools.py b/build/libs/tools.py new file mode 100644 index 0000000..f3c1ccd --- /dev/null +++ b/build/libs/tools.py @@ -0,0 +1,212 @@ +#!/usr/bin/env python +# coding=utf-8 + +from __future__ import absolute_import, division, print_function, unicode_literals + +__author__ = "TrackMe Limited" +__version__ = "0.1.0" +__maintainer__ = "TrackMe Limited" +__status__ = "PRODUCTION" + +import os +import time +import json +import logging +import requests +from requests.auth import HTTPBasicAuth + + +# context manager +class cd: + """Context manager for changing the current working directory""" + + def __init__(self, newPath): + self.newPath = os.path.expanduser(newPath) + + def __enter__(self): + self.savedPath = os.getcwd() + os.chdir(self.newPath) + + def __exit__(self, etype, value, traceback): + os.chdir(self.savedPath) + + +# login to Appinspect API and return the token +def login_appinspect(username, password): + login_url = "https://api.splunk.com/2.0/rest/login/splunk" + + try: + response = requests.get( + login_url, auth=HTTPBasicAuth(username, password), verify=True + ) + if response.status_code not in (200, 201, 204): + logging.error( + "Authentication to Splunk Appinspect API has failed, url={}, HTTP Error={}, content={}".format( + login_url, response.status_code, response.text + ) + ) + else: + logging.debug( + 'Authentication to Splunk Appinspect API was successful, url="{}", token="{}"'.format( + login_url, response.text + ) + ) + + except Exception as e: + logging.error( + 'Authentication to Splunk Appinspect API has failed, url="{}", exception="{}"'.format( + login_url, e + ) + ) + + response_json = json.loads(response.text) + appinspect_token = response_json["data"]["token"] + + return appinspect_token + + +# submit an app to Appinspect +def submit_appinspect(session, token, app): + appinspect_headers = { + "Authorization": "bearer %s" % token, + } + + files = { + "app_package": open(app, "rb"), + "included_tags": (None, "cloud"), + } + + # submit + validate_url = "https://appinspect.splunk.com/v1/app/validate" + + # run + session = requests.Session() + + try: + response = session.post( + validate_url, headers=appinspect_headers, files=files, verify=True + ) + if response.status_code not in (200, 201, 204): + logging.error( + "Submission to Splunk Appinspect API has failed, url={}, HTTP Error={}, content={}".format( + validate_url, response.status_code, response.text + ) + ) + else: + logging.debug( + 'Submission to Splunk Appinspect API was successful, url="{}", response="{}"'.format( + validate_url, response.text + ) + ) + + except Exception as e: + logging.error( + 'Submission to Splunk Appinspect API has failed, url="{}", exception="{}"'.format( + validate_url, e + ) + ) + + return response.text + + +# verify an Appinspect vetting status +def verify_appinspect(session, token, request_id): + appinspect_headers = { + "Authorization": "bearer %s" % token, + } + + # submit + validate_url = f"https://appinspect.splunk.com/v1/app/validate/status/{request_id}" + + # run + try: + response = session.get(validate_url, headers=appinspect_headers, verify=True) + if response.status_code not in (200, 201, 204): + error_message = f"Request verification to Splunk Appinspect API has failed, url={validate_url}, HTTP Error={response.status_code}, content={response.text}" + logging.error(error_message) + raise Exception(error_message) + + else: + logging.debug( + 'Request verification to Splunk Appinspect API was successful, url="{}", response="{}"'.format( + validate_url, response.text + ) + ) + return response.text + + except Exception as e: + raise Exception(str(e)) + + +# download an Appinspect vetting report +def download_htmlreport_appinspect(session, token, request_id): + appinspect_headers = { + "Authorization": "bearer %s" % token, + "Content-Type": "text/html", + } + + # submit + validate_url = "https://appinspect.splunk.com/v1/app/report/" + str(request_id) + + # run + try: + response = session.get(validate_url, headers=appinspect_headers, verify=True) + if response.status_code not in (200, 201, 204): + logging.error( + "Report download request to Splunk Appinspect API has failed, url={}, HTTP Error={}, content={}".format( + validate_url, response.status_code, response.text + ) + ) + return None + else: + logging.debug( + 'Report download request to Splunk Appinspect API was successful, url="{}", response="{}"'.format( + validate_url, response.text + ) + ) + return response.text + + except Exception as e: + logging.error( + 'Report download request to Splunk Appinspect API has failed, url="{}", exception="{}"'.format( + validate_url, e + ) + ) + return None + + +# download an Appinspect vetting report +def download_jsonreport_appinspect(session, token, request_id): + appinspect_headers = { + "Authorization": "bearer %s" % token, + "Content-Type": "application/json", + } + + # submit + validate_url = "https://appinspect.splunk.com/v1/app/report/" + str(request_id) + + # run + try: + response = session.get(validate_url, headers=appinspect_headers, verify=True) + if response.status_code not in (200, 201, 204): + logging.error( + "Report download request to Splunk Appinspect API has failed, url={}, HTTP Error={}, content={}".format( + validate_url, response.status_code, response.text + ) + ) + return None + else: + logging.debug( + 'Report download request to Splunk Appinspect API was successful, url="{}", response="{}"'.format( + validate_url, response.text + ) + ) + return response.text + + except Exception as e: + logging.error( + 'Report download request to Splunk Appinspect API has failed, url="{}", exception="{}"'.format( + validate_url, e + ) + ) + return None diff --git a/docs/releasenotes.rst b/docs/releasenotes.rst index 4c6868e..189d1be 100644 --- a/docs/releasenotes.rst +++ b/docs/releasenotes.rst @@ -1,6 +1,17 @@ Release notes ############# +Version 1.1.6 +============= + +- Splunk Cloud vetting issues - SSL verification is now mandatory to satisfy with Splunk Cloud requirements +- Release refreshed + +Version 1.1.5 +============= + +- Release refreshed + Version 1.1.4 ============= diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..4701d7b --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,3 @@ +sphinx>=7.2.6 +sphinx-rtd-theme>=2.0.0 +jinja2>=3.1.4 \ No newline at end of file diff --git a/globalConfig.json b/globalConfig.json index 364a0c9..1fcf1bd 100644 --- a/globalConfig.json +++ b/globalConfig.json @@ -186,14 +186,6 @@ "errorMsg": "Max length of text input is 8192" } ] - }, - { - "field": "default_ms_teams_ssl_verification", - "label": "SSL certificate validation", - "type": "checkbox", - "help": "Check this box to perform SSL certificate validation", - "required": false, - "defaultValue": 1 } ] } @@ -415,8 +407,9 @@ "meta": { "name": "TA-ms-teams-alert-action", "restRoot": "ta_ms_teams_alert_action", - "version": "1.1.5", + "version": "1.1.6", "displayName": "MS Teams alert action", - "schemaVersion": "0.0.3" + "schemaVersion": "0.0.3", + "_uccVersion": "5.39.1" } -} \ No newline at end of file +} diff --git a/package/README.txt b/package/README.txt index 1a50a9f..1603551 100644 --- a/package/README.txt +++ b/package/README.txt @@ -3,3 +3,7 @@ Documentation is hosted online at: https://TA-ms-teams-alert-action.readthedocs.io + +# Binary File Declaration +lib/charset_normalizer/md__mypyc.cpython-310-x86_64-linux-gnu.so +lib/charset_normalizer/md.cpython-310-x86_64-linux-gnu.so diff --git a/package/bin/ta_ms_teams_alert_action/modalert_ms_teams_publish_to_channel_helper.py b/package/bin/ta_ms_teams_alert_action/modalert_ms_teams_publish_to_channel_helper.py index 28a6ef4..e85dfd6 100644 --- a/package/bin/ta_ms_teams_alert_action/modalert_ms_teams_publish_to_channel_helper.py +++ b/package/bin/ta_ms_teams_alert_action/modalert_ms_teams_publish_to_channel_helper.py @@ -11,7 +11,7 @@ def checkstr(i): # Manage tabs i = i.replace("\t", "\\t") # Manage breaking delimiters - i = i.replace("\"", "\\\"") + i = i.replace('"', '\\"') return i @@ -34,16 +34,21 @@ def process_event(helper, *args, **kwargs): session_key = helper.session_key # Get splunkd port - entity = splunk.entity.getEntity('/server', 'settings', namespace='TA-ms-teams-alert-action', - sessionKey=session_key, owner='-') + entity = splunk.entity.getEntity( + "/server", + "settings", + namespace="TA-ms-teams-alert-action", + sessionKey=session_key, + owner="-", + ) mydict = entity - splunkd_port = mydict['mgmtHostPort'] + splunkd_port = mydict["mgmtHostPort"] helper.log_debug("splunkd_port={}".format(splunkd_port)) # Retrieve default Webhook URL and optional per alert Webhook URL default_ms_teams_url = helper.get_global_setting("default_ms_teams_url") alert_ms_teams_url = helper.get_param("alert_ms_teams_url") - active_ms_teams_url = '' + active_ms_teams_url = "" if alert_ms_teams_url and alert_ms_teams_url is not None: helper.log_debug("alert_ms_teams_url={}".format(alert_ms_teams_url)) @@ -53,39 +58,38 @@ def process_event(helper, *args, **kwargs): active_ms_teams_url = default_ms_teams_url if not active_ms_teams_url: - helper.log_error("No Webhook URL have been configured nor for this alert, and neither globally, " - "cannot continue. Define a default URL in global addon configuration, or for this alert.") + helper.log_error( + "No Webhook URL have been configured nor for this alert, and neither globally, " + "cannot continue. Define a default URL in global addon configuration, or for this alert." + ) return False # Verify the URL target compliancy - if the URL does not comply, the call will not be proceeded - default_ms_teams_check_url_compliancy = helper.get_global_setting("default_ms_teams_check_url_compliancy") + default_ms_teams_check_url_compliancy = helper.get_global_setting( + "default_ms_teams_check_url_compliancy" + ) if default_ms_teams_check_url_compliancy in ("null", "None", None): default_ms_teams_check_url_compliancy = ".*" - helper.log_debug("default_ms_teams_check_url_compliancy={}".format(default_ms_teams_check_url_compliancy)) - - if not re.match(str(r"%s" % default_ms_teams_check_url_compliancy), active_ms_teams_url): - helper.log_error("The provided URL " + str(active_ms_teams_url) + " does not comply with the URL compliancy" \ - " check setting defined in the global configuration, therefore the operation cannot be proceeded.") + helper.log_debug( + "default_ms_teams_check_url_compliancy={}".format( + default_ms_teams_check_url_compliancy + ) + ) + + if not re.match( + str(r"%s" % default_ms_teams_check_url_compliancy), active_ms_teams_url + ): + helper.log_error( + "The provided URL " + + str(active_ms_teams_url) + + " does not comply with the URL compliancy" + " check setting defined in the global configuration, therefore the operation cannot be proceeded." + ) return 1 - # SSL verification (defaults to true) - # Version 1.0.15 notes: This param option was added in version 1.0.14, but customers upgrading to prior version - # would get failing action until a new configuration save is applied - # To avoid this, an addition verification step is performed - default_ms_teams_ssl_verification = helper.get_global_setting("default_ms_teams_ssl_verification") - if default_ms_teams_ssl_verification is not None: - default_ms_teams_ssl_verification = int(default_ms_teams_ssl_verification) - else: - default_ms_teams_ssl_verification = 1 - ssl_certificate_validation = True - helper.log_debug("default_ms_teams_ssl_verification={}".format(default_ms_teams_ssl_verification)) - if default_ms_teams_ssl_verification == 0: - ssl_certificate_validation = False - helper.log_debug("ssl_certificate_validation={}".format(ssl_certificate_validation)) - # For Splunk Cloud vetting, the URL must start with https:// if not active_ms_teams_url.startswith("https://"): - active_ms_teams_url = 'https://' + active_ms_teams_url + active_ms_teams_url = "https://" + active_ms_teams_url helper.log_debug("active_ms_teams_url={}".format(active_ms_teams_url)) # get proxy configuration @@ -104,29 +108,37 @@ def process_event(helper, *args, **kwargs): # defined alert_ms_teams_image_link = helper.get_param("alert_ms_teams_image_link") default_ms_teams_image_link = helper.get_global_setting( - "default_ms_teams_image_link") - active_ms_teams_image_link = '' + "default_ms_teams_image_link" + ) + active_ms_teams_image_link = "" if alert_ms_teams_image_link and alert_ms_teams_image_link is not None: active_ms_teams_image_link = alert_ms_teams_image_link helper.log_debug( - "active_ms_teams_image_link={}".format(active_ms_teams_image_link)) + "active_ms_teams_image_link={}".format(active_ms_teams_image_link) + ) elif default_ms_teams_image_link and default_ms_teams_image_link is not None: active_ms_teams_image_link = default_ms_teams_image_link helper.log_debug( - "active_ms_teams_image_link={}".format(active_ms_teams_image_link)) + "active_ms_teams_image_link={}".format(active_ms_teams_image_link) + ) else: - helper.log_debug("No image URL link were defined, neither globally of for this alert.") + helper.log_debug( + "No image URL link were defined, neither globally of for this alert." + ) # Get the message summary (required) alert_ms_teams_activity_title = helper.get_param("alert_ms_teams_activity_title") alert_ms_teams_activity_title = checkstr(alert_ms_teams_activity_title) if alert_ms_teams_activity_title and alert_ms_teams_activity_title is None: - helper.log_info("No activity title was provided, reverted to default: Splunk alert") + helper.log_info( + "No activity title was provided, reverted to default: Splunk alert" + ) alert_ms_teams_activity_title = "Splunk alert" else: helper.log_debug( - "alert_ms_teams_activity_title={}".format(alert_ms_teams_activity_title)) + "alert_ms_teams_activity_title={}".format(alert_ms_teams_activity_title) + ) # Theme color alert_ms_teams_theme_color = helper.get_param("alert_ms_teams_theme_color") @@ -135,33 +147,52 @@ def process_event(helper, *args, **kwargs): helper.log_debug("Theme color is not defined, reverted to default") else: helper.log_debug( - "alert_ms_teams_theme_color={}".format(alert_ms_teams_theme_color)) + "alert_ms_teams_theme_color={}".format(alert_ms_teams_theme_color) + ) # Start building the json object - data_json = '{\n' + '\"@type\": \"MessageCard\",\n' + '\"@context\": \"http://schema.org/extensions\",\n' + \ - '\"themeColor\": \"' + alert_ms_teams_theme_color + '\",\n' + data_json = ( + "{\n" + + '"@type": "MessageCard",\n' + + '"@context": "http://schema.org/extensions",\n' + + '"themeColor": "' + + alert_ms_teams_theme_color + + '",\n' + ) # Add the message title - data_json = data_json + '\n' + '\"summary\": \"' + alert_ms_teams_activity_title + '\",\n' + data_json = ( + data_json + "\n" + '"summary": "' + alert_ms_teams_activity_title + '",\n' + ) - data_json = data_json + '\"sections\": [{\n' - data_json = data_json + '\"activityTitle\": "' + alert_ms_teams_activity_title + '\",\n' - data_json = data_json + '\"activitySubtitle\": "\",\n' + data_json = data_json + '"sections": [{\n' + data_json = ( + data_json + '"activityTitle": "' + alert_ms_teams_activity_title + '",\n' + ) + data_json = data_json + '"activitySubtitle": "",\n' # Add the picture if any if active_ms_teams_image_link: - data_json = data_json + '\"activityImage\": \"' + active_ms_teams_image_link + '",\n' + data_json = ( + data_json + '"activityImage": "' + active_ms_teams_image_link + '",\n' + ) # data facts - data_json_facts = '\"facts\": [\n' + data_json_facts = '"facts": [\n' # Fields ordering in the message publication, defaults to alphabetical ordering alert_ms_teams_fields_order = helper.get_param("alert_ms_teams_fields_order") alert_ms_teams_fields_order_by_alpha = True - helper.log_debug("alert_ms_teams_fields_order={}".format(alert_ms_teams_fields_order)) + helper.log_debug( + "alert_ms_teams_fields_order={}".format(alert_ms_teams_fields_order) + ) if alert_ms_teams_fields_order in "order_by_list": alert_ms_teams_fields_order_by_alpha = False - helper.log_debug("alert_ms_teams_fields_order_by_alpha={}".format(alert_ms_teams_fields_order_by_alpha)) + helper.log_debug( + "alert_ms_teams_fields_order_by_alpha={}".format( + alert_ms_teams_fields_order_by_alpha + ) + ) # Iterate over the results to extract key values from the field list provided in input alert_ms_teams_fields_list = helper.get_param("alert_ms_teams_fields_list") @@ -178,30 +209,57 @@ def process_event(helper, *args, **kwargs): count = 0 # build a new dict to sort alphabetically - keys_dict_str = '' + keys_dict_str = "" keys_count = 0 for key, value in event.items(): if key in alert_ms_teams_fields_list: - helper.log_debug("key was found in data and is listed in fields list={}".format(key)) + helper.log_debug( + "key was found in data and is listed in fields list={}".format( + key + ) + ) helper.log_debug("value={}".format(value)) if keys_count != 0: - keys_dict_str = keys_dict_str + ', "' + checkstr(key) + '": ' + '"' + checkstr(value) + '"' + keys_dict_str = ( + keys_dict_str + + ', "' + + checkstr(key) + + '": ' + + '"' + + checkstr(value) + + '"' + ) else: - keys_dict_str = '"' + checkstr(key) + '": ' + '"' + checkstr(value) + '"' + keys_dict_str = ( + '"' + checkstr(key) + '": ' + '"' + checkstr(value) + '"' + ) keys_count += 1 helper.log_debug("keys_dict_str={}".format(keys_dict_str)) # Convert to a proper dict - keys_dict_str = '{' + keys_dict_str + '}' + keys_dict_str = "{" + keys_dict_str + "}" keys_dict = json.loads(keys_dict_str) - helper.log_debug("Before processing ordering, keys_dict_str={}".format(keys_dict_str)) + helper.log_debug( + "Before processing ordering, keys_dict_str={}".format(keys_dict_str) + ) # Ordering fields if alert_ms_teams_fields_order_by_alpha: - keys_ordered = OrderedDict(sorted(keys_dict.items(), key=lambda t: t[0])) + keys_ordered = OrderedDict( + sorted(keys_dict.items(), key=lambda t: t[0]) + ) else: - keys_ordered = OrderedDict(sorted(keys_dict.items(), key=lambda pair: alert_ms_teams_fields_list.index(pair[0]))) - helper.log_debug("After processing ordering as instructed, keys_ordered={}".format(keys_ordered)) + keys_ordered = OrderedDict( + sorted( + keys_dict.items(), + key=lambda pair: alert_ms_teams_fields_list.index(pair[0]), + ) + ) + helper.log_debug( + "After processing ordering as instructed, keys_ordered={}".format( + keys_ordered + ) + ) for key, value in keys_ordered.items(): if key in alert_ms_teams_fields_list: @@ -209,13 +267,13 @@ def process_event(helper, *args, **kwargs): # helper.log_debug("value={}".format(value)) if count != 0: - data_json_facts = data_json_facts + ',' + data_json_facts = data_json_facts + "," key = checkstr(key) value = checkstr(value) - data_json_facts = data_json_facts + '{\n' - data_json_facts = data_json_facts + '\"name\": \"' + key + '\",\n' - data_json_facts = data_json_facts + '\"value\": \"' + value + '\"\n' - data_json_facts = data_json_facts + '}\n' + data_json_facts = data_json_facts + "{\n" + data_json_facts = data_json_facts + '"name": "' + key + '",\n' + data_json_facts = data_json_facts + '"value": "' + value + '"\n' + data_json_facts = data_json_facts + "}\n" count += 1 # helper.log_debug("count={}".format(count)) @@ -226,39 +284,82 @@ def process_event(helper, *args, **kwargs): # MS teams action, this is optional # First OpenURI action - alert_ms_teams_potential_action_name = helper.get_param("alert_ms_teams_potential_action_name") - helper.log_debug("alert_ms_teams_potential_action_name={}".format(alert_ms_teams_potential_action_name)) - - alert_ms_teams_potential_action_url = helper.get_param("alert_ms_teams_potential_action_url") - helper.log_debug("alert_ms_teams_potential_action_url={}".format(alert_ms_teams_potential_action_url)) + alert_ms_teams_potential_action_name = helper.get_param( + "alert_ms_teams_potential_action_name" + ) + helper.log_debug( + "alert_ms_teams_potential_action_name={}".format( + alert_ms_teams_potential_action_name + ) + ) + + alert_ms_teams_potential_action_url = helper.get_param( + "alert_ms_teams_potential_action_url" + ) + helper.log_debug( + "alert_ms_teams_potential_action_url={}".format( + alert_ms_teams_potential_action_url + ) + ) # Second OpenURI action - alert_ms_teams_potential_action_name2 = helper.get_param("alert_ms_teams_potential_action_name2") - helper.log_debug("alert_ms_teams_potential_action_name2={}".format(alert_ms_teams_potential_action_name2)) - - alert_ms_teams_potential_action_url2 = helper.get_param("alert_ms_teams_potential_action_url2") - helper.log_debug("alert_ms_teams_potential_action_url2={}".format(alert_ms_teams_potential_action_url2)) + alert_ms_teams_potential_action_name2 = helper.get_param( + "alert_ms_teams_potential_action_name2" + ) + helper.log_debug( + "alert_ms_teams_potential_action_name2={}".format( + alert_ms_teams_potential_action_name2 + ) + ) + + alert_ms_teams_potential_action_url2 = helper.get_param( + "alert_ms_teams_potential_action_url2" + ) + helper.log_debug( + "alert_ms_teams_potential_action_url2={}".format( + alert_ms_teams_potential_action_url2 + ) + ) # HttpPOST action - alert_ms_teams_potential_postaction_name = helper.get_param("alert_ms_teams_potential_postaction_name") - helper.log_debug("alert_ms_teams_potential_postaction_name={}". - format(alert_ms_teams_potential_postaction_name)) - - alert_ms_teams_potential_postaction_target = helper.get_param("alert_ms_teams_potential_postaction_target") - helper.log_debug("alert_ms_teams_potential_postaction_target={}". - format(alert_ms_teams_potential_postaction_target)) - - alert_ms_teams_potential_postaction_body = helper.get_param("alert_ms_teams_potential_postaction_body") - helper.log_debug("alert_ms_teams_potential_postaction_body={}". - format(alert_ms_teams_potential_postaction_body)) - - alert_ms_teams_potential_postaction_bodycontenttype = helper.\ - get_param("alert_ms_teams_potential_postaction_bodycontenttype") - helper.log_debug("alert_ms_teams_potential_postaction_bodycontenttype={}". - format(alert_ms_teams_potential_postaction_bodycontenttype)) + alert_ms_teams_potential_postaction_name = helper.get_param( + "alert_ms_teams_potential_postaction_name" + ) + helper.log_debug( + "alert_ms_teams_potential_postaction_name={}".format( + alert_ms_teams_potential_postaction_name + ) + ) + + alert_ms_teams_potential_postaction_target = helper.get_param( + "alert_ms_teams_potential_postaction_target" + ) + helper.log_debug( + "alert_ms_teams_potential_postaction_target={}".format( + alert_ms_teams_potential_postaction_target + ) + ) + + alert_ms_teams_potential_postaction_body = helper.get_param( + "alert_ms_teams_potential_postaction_body" + ) + helper.log_debug( + "alert_ms_teams_potential_postaction_body={}".format( + alert_ms_teams_potential_postaction_body + ) + ) + + alert_ms_teams_potential_postaction_bodycontenttype = helper.get_param( + "alert_ms_teams_potential_postaction_bodycontenttype" + ) + helper.log_debug( + "alert_ms_teams_potential_postaction_bodycontenttype={}".format( + alert_ms_teams_potential_postaction_bodycontenttype + ) + ) # terminate the sections pattern - data_json = data_json + '\n' + '\"markdown\": false' + '\n' + '}]' + data_json = data_json + "\n" + '"markdown": false' + "\n" + "}]" # Actions statuses has_action1 = False @@ -266,194 +367,314 @@ def process_event(helper, *args, **kwargs): has_postaction = False # OpenURI action 1 - if ((alert_ms_teams_potential_action_name and alert_ms_teams_potential_action_name is not None) and - (alert_ms_teams_potential_action_url and alert_ms_teams_potential_action_url is not None)): + if ( + alert_ms_teams_potential_action_name + and alert_ms_teams_potential_action_name is not None + ) and ( + alert_ms_teams_potential_action_url + and alert_ms_teams_potential_action_url is not None + ): # https is enforced for certification compliance, action has to target https if not alert_ms_teams_potential_action_url.startswith("https://"): - helper.log_warn("alert_ms_teams_potential_action_url={}". - format(alert_ms_teams_potential_action_url)) - helper.log_warn("the potential action URL configured does not " - "target an https site, which is required for " - "compliance purpose, the potential action has been disabled automatically.") + helper.log_warn( + "alert_ms_teams_potential_action_url={}".format( + alert_ms_teams_potential_action_url + ) + ) + helper.log_warn( + "the potential action URL configured does not " + "target an https site, which is required for " + "compliance purpose, the potential action has been disabled automatically." + ) has_action1 = False else: has_action1 = True # OpenURI action 2 - if ((alert_ms_teams_potential_action_name2 and alert_ms_teams_potential_action_name2 is not None) and - (alert_ms_teams_potential_action_url2 and alert_ms_teams_potential_action_url2 is not None)): + if ( + alert_ms_teams_potential_action_name2 + and alert_ms_teams_potential_action_name2 is not None + ) and ( + alert_ms_teams_potential_action_url2 + and alert_ms_teams_potential_action_url2 is not None + ): # https is enforced for certification compliance, action has to target https if not alert_ms_teams_potential_action_url2.startswith("https://"): - helper.log_warn("alert_ms_teams_potential_action_url2={}". - format(alert_ms_teams_potential_action_url2)) + helper.log_warn( + "alert_ms_teams_potential_action_url2={}".format( + alert_ms_teams_potential_action_url2 + ) + ) helper.log_warn( "the potential action URL configured does not target an https site, which is required for " - "compliance purpose, the potential action has been disabled automatically.") + "compliance purpose, the potential action has been disabled automatically." + ) has_action2 = False else: has_action2 = True # HttpPOST action - if ((alert_ms_teams_potential_postaction_name and alert_ms_teams_potential_postaction_name is not None) and - (alert_ms_teams_potential_postaction_target and - alert_ms_teams_potential_postaction_target) is not None): + if ( + alert_ms_teams_potential_postaction_name + and alert_ms_teams_potential_postaction_name is not None + ) and ( + alert_ms_teams_potential_postaction_target + and alert_ms_teams_potential_postaction_target + ) is not None: # https is enforced for certification compliance, action has to target https - if not alert_ms_teams_potential_postaction_target.startswith("https://"): - helper.log_warn("alert_ms_teams_potential_postaction_target={}". - format(alert_ms_teams_potential_postaction_target)) + if not alert_ms_teams_potential_postaction_target.startswith( + "https://" + ): + helper.log_warn( + "alert_ms_teams_potential_postaction_target={}".format( + alert_ms_teams_potential_postaction_target + ) + ) helper.log_warn( "the potential HttpPOST action target configured does not target an " "https site, which is required for " - "compliance purpose, the potential action has been disabled automatically.") + "compliance purpose, the potential action has been disabled automatically." + ) has_postaction = False else: has_postaction = True if has_action1 or has_action2 or has_postaction: # Create the potentialAction section - data_json = data_json + '\n,\"potentialAction\": [' + '\n' + data_json = data_json + '\n,"potentialAction": [' + "\n" if has_action1: - data_json = data_json + '\n{' - data_json = data_json + '\"@type\": \"OpenUri\",' + '\n' - data_json = data_json + '\"name\": \"' + alert_ms_teams_potential_action_name + '\",' + '\n' - data_json = data_json + '\"targets\": [' + '\n' - data_json = data_json + '{\"os\": \"default\", \"uri\": \"' \ - + checkstr(alert_ms_teams_potential_action_url) + '\"}' + '\n' - data_json = data_json + ']\n' + '}\n' + data_json = data_json + "\n{" + data_json = data_json + '"@type": "OpenUri",' + "\n" + data_json = ( + data_json + + '"name": "' + + alert_ms_teams_potential_action_name + + '",' + + "\n" + ) + data_json = data_json + '"targets": [' + "\n" + data_json = ( + data_json + + '{"os": "default", "uri": "' + + checkstr(alert_ms_teams_potential_action_url) + + '"}' + + "\n" + ) + data_json = data_json + "]\n" + "}\n" if has_action1 and has_action2: - data_json = data_json + '\n,{' - data_json = data_json + '\"@type\": \"OpenUri\",' + '\n' - data_json = data_json + '\"name\": \"' + alert_ms_teams_potential_action_name2 + '\",' + '\n' - data_json = data_json + '\"targets\": [' + '\n' - data_json = data_json + '{\"os\": \"default\", \"uri\": \"' \ - + checkstr(alert_ms_teams_potential_action_url2) + '\"}' + '\n' - data_json = data_json + ']\n' + '}\n' + data_json = data_json + "\n,{" + data_json = data_json + '"@type": "OpenUri",' + "\n" + data_json = ( + data_json + + '"name": "' + + alert_ms_teams_potential_action_name2 + + '",' + + "\n" + ) + data_json = data_json + '"targets": [' + "\n" + data_json = ( + data_json + + '{"os": "default", "uri": "' + + checkstr(alert_ms_teams_potential_action_url2) + + '"}' + + "\n" + ) + data_json = data_json + "]\n" + "}\n" if has_action2 and not has_action1: helper.log_warn( "A second openURI action has been configured while the first openURI action has not." "Review your configuration to use the first action instead," - " so far this action is being ignored.") + " so far this action is being ignored." + ) if has_postaction and not has_action1: - data_json = data_json + '\n{' + data_json = data_json + "\n{" if has_postaction and has_action1: - data_json = data_json + '\n,{' - data_json = data_json + '\"@type\": \"HttpPOST\",' + '\n' - data_json = data_json + '\"name\": \"' + alert_ms_teams_potential_postaction_name + '\",' + '\n' - data_json = data_json + '\"target\": \"' + alert_ms_teams_potential_postaction_target + '\"' + data_json = data_json + "\n,{" + data_json = data_json + '"@type": "HttpPOST",' + "\n" + data_json = ( + data_json + + '"name": "' + + alert_ms_teams_potential_postaction_name + + '",' + + "\n" + ) + data_json = ( + data_json + + '"target": "' + + alert_ms_teams_potential_postaction_target + + '"' + ) if alert_ms_teams_potential_postaction_body is not None: - data_json = data_json + ',\n' - data_json = data_json + '\"body\": \"' + alert_ms_teams_potential_postaction_body + '\"' + data_json = data_json + ",\n" + data_json = ( + data_json + + '"body": "' + + alert_ms_teams_potential_postaction_body + + '"' + ) if alert_ms_teams_potential_postaction_bodycontenttype is not None: - data_json = data_json + ',\n' - data_json = data_json + '\"bodyContentType\": \"' +\ - alert_ms_teams_potential_postaction_bodycontenttype + '\"' + '\n' - data_json = data_json + '\n' + '}\n' + data_json = data_json + ",\n" + data_json = ( + data_json + + '"bodyContentType": "' + + alert_ms_teams_potential_postaction_bodycontenttype + + '"' + + "\n" + ) + data_json = data_json + "\n" + "}\n" # Terminate the block - data_json = data_json + ']\n' + data_json = data_json + "]\n" # Terminate the json - data_json = data_json + '\n' + '}' + data_json = data_json + "\n" + "}" # Properly load json try: data_json = json.dumps(json.loads(data_json, strict=False), indent=4) except Exception as e: - helper.log_error("json loads failed to accept some of the characters," - " raw json data before json.loads:={}".format(data_json)) + helper.log_error( + "json loads failed to accept some of the characters," + " raw json data before json.loads:={}".format(data_json) + ) raise e # log json in debug mode helper.log_debug("json data for final rest call:={}".format(data_json)) headers = { - 'Content-Type': 'application/json', + "Content-Type": "application/json", } # Try http post, catch exceptions and incorrect http return codes # Splunk Cloud vetting note, verify SSL is required for vetting purposes and enabled by default try: - response = helper.send_http_request(active_ms_teams_url, "POST", parameters=None, payload=data_json, - headers=headers, cookies=None, verify=ssl_certificate_validation, - cert=None, timeout=120, use_proxy=opt_use_proxy) + response = helper.send_http_request( + active_ms_teams_url, + "POST", + parameters=None, + payload=data_json, + headers=headers, + cookies=None, + verify=True, + cert=None, + timeout=120, + use_proxy=opt_use_proxy, + ) # No http exception, but http post was not successful if response.status_code not in (200, 201, 204): - helper.log_error("Microsoft Teams publish to channel has failed!. " - "url={}, data={}, HTTP Error={}, HTTP Reason={}, HTTP content={}" - .format(active_ms_teams_url, data_json, response.status_code, - response.reason, response.text)) + helper.log_error( + "Microsoft Teams publish to channel has failed!. " + "url={}, data={}, HTTP Error={}, HTTP Reason={}, HTTP content={}".format( + active_ms_teams_url, + data_json, + response.status_code, + response.reason, + response.text, + ) + ) # Store the failed publication in the replay KVstore - record_url = 'https://localhost:' + str( - splunkd_port) + '/servicesNS/nobody/' \ - 'TA-ms-teams-alert-action/storage/collections/data/kv_ms_teams_failures_replay' + record_url = ( + "https://localhost:" + str(splunkd_port) + "/servicesNS/nobody/" + "TA-ms-teams-alert-action/storage/collections/data/kv_ms_teams_failures_replay" + ) record_uuid = str(uuid.uuid1()) helper.log_error( - 'Microsoft Teams publish to channel failed message stored for next chance' - ' replay purposes in the ' - 'replay KVstore with uuid: ' + record_uuid) + "Microsoft Teams publish to channel failed message stored for next chance" + " replay purposes in the " + "replay KVstore with uuid: " + record_uuid + ) headers = { - 'Authorization': 'Splunk %s' % session_key, - 'Content-Type': 'application/json'} - - record = '{"_key": "' + record_uuid + '", "url": "' \ - + str(active_ms_teams_url) + '", "ctime": "' + str( - time.time()) \ - + '", "status": "temporary_failure", "no_attempts": "1", "data": "' + checkstr(data_json) \ - + '"}' + "Authorization": "Splunk %s" % session_key, + "Content-Type": "application/json", + } + + record = ( + '{"_key": "' + + record_uuid + + '", "url": "' + + str(active_ms_teams_url) + + '", "ctime": "' + + str(time.time()) + + '", "status": "temporary_failure", "no_attempts": "1", "data": "' + + checkstr(data_json) + + '"}' + ) # Splunk Cloud vetting note, this communication is a localhost communication to splunkd and # does not have to be verified - response = requests.post(record_url, headers=headers, data=record, - verify=False) + response = requests.post( + record_url, headers=headers, data=record, verify=False + ) if response.status_code not in (200, 201, 204): helper.log_error( - 'KVstore saving has failed!. url={}, data={}, HTTP Error={}, ' - 'content={}'.format(record_url, record, response.status_code, response.text)) + "KVstore saving has failed!. url={}, data={}, HTTP Error={}, " + "content={}".format( + record_url, record, response.status_code, response.text + ) + ) return 1 else: # http post was successful - helper.log_info('Microsoft Teams message successfully created. {}, ' - 'content={}'.format(active_ms_teams_url, response.text)) + helper.log_info( + "Microsoft Teams message successfully created. {}, " + "content={}".format(active_ms_teams_url, response.text) + ) return 0 # any exception such as proxy error, dns failure etc. will be catch here except Exception as e: - helper.log_error("Microsoft Teams publish to channel has failed!:{}".format(str(e))) helper.log_error( - 'message content={}'.format(data_json)) + "Microsoft Teams publish to channel has failed!:{}".format(str(e)) + ) + helper.log_error("message content={}".format(data_json)) # Store the failed publication in the replay KVstore - record_url = 'https://localhost:' + str( - splunkd_port) + '/servicesNS/nobody/' \ - 'TA-ms-teams-alert-action/storage/collections/data/kv_ms_teams_failures_replay' + record_url = ( + "https://localhost:" + str(splunkd_port) + "/servicesNS/nobody/" + "TA-ms-teams-alert-action/storage/collections/data/kv_ms_teams_failures_replay" + ) record_uuid = str(uuid.uuid1()) helper.log_error( - 'Microsoft Teams publish to channel failed message stored for next chance replay purposes in the ' - 'replay KVstore with uuid: ' + record_uuid) + "Microsoft Teams publish to channel failed message stored for next chance replay purposes in the " + "replay KVstore with uuid: " + record_uuid + ) headers = { - 'Authorization': 'Splunk %s' % session_key, - 'Content-Type': 'application/json'} - - record = '{"_key": "' + record_uuid + '", "url": "' + str(active_ms_teams_url) + '", "ctime": "' + str( - time.time()) \ - + '", "status": "temporary_failure", "no_attempts": "1", "data": "' + checkstr(data_json) \ - + '"}' + "Authorization": "Splunk %s" % session_key, + "Content-Type": "application/json", + } + + record = ( + '{"_key": "' + + record_uuid + + '", "url": "' + + str(active_ms_teams_url) + + '", "ctime": "' + + str(time.time()) + + '", "status": "temporary_failure", "no_attempts": "1", "data": "' + + checkstr(data_json) + + '"}' + ) # Splunk Cloud vetting note, this communication is a localhost communication to splunkd and # does not have to be verified - response = requests.post(record_url, headers=headers, data=record, - verify=False) + response = requests.post( + record_url, headers=headers, data=record, verify=False + ) if response.status_code not in (200, 201, 204): helper.log_error( - 'KVstore saving has failed!. url={}, data={}, HTTP Error={}, ' - 'content={}'.format(record_url, record, response.status_code, response.text)) + "KVstore saving has failed!. url={}, data={}, HTTP Error={}, " + "content={}".format( + record_url, record, response.status_code, response.text + ) + ) return 1 return 0 diff --git a/package/bin/ta_ms_teams_alert_action/modalert_ms_teams_publish_to_channel_replay_helper.py b/package/bin/ta_ms_teams_alert_action/modalert_ms_teams_publish_to_channel_replay_helper.py index 36f623c..33c0206 100755 --- a/package/bin/ta_ms_teams_alert_action/modalert_ms_teams_publish_to_channel_replay_helper.py +++ b/package/bin/ta_ms_teams_alert_action/modalert_ms_teams_publish_to_channel_replay_helper.py @@ -1,6 +1,6 @@ - # encoding = utf-8 + def checkstr(i): if i is not None: @@ -11,9 +11,9 @@ def checkstr(i): # Manage tabs i = i.replace("\t", "\\t") # Manage breaking delimiters - i = i.replace("\"", "\\\"") + i = i.replace('"', '\\"') return i - + def process_event(helper, *args, **kwargs): @@ -33,10 +33,15 @@ def process_event(helper, *args, **kwargs): session_key = helper.session_key # Get splunkd port - entity = splunk.entity.getEntity('/server', 'settings', namespace='TA-ms-teams-alert-action', - sessionKey=session_key, owner='-') + entity = splunk.entity.getEntity( + "/server", + "settings", + namespace="TA-ms-teams-alert-action", + sessionKey=session_key, + owner="-", + ) mydict = entity - splunkd_port = mydict['mgmtHostPort'] + splunkd_port = mydict["mgmtHostPort"] helper.log_debug("splunkd_port={}".format(splunkd_port)) # get proxy configuration @@ -51,14 +56,6 @@ def process_event(helper, *args, **kwargs): opt_use_proxy = False helper.log_debug("use_proxy set to False") - # SSL verification (defaults to true) - default_ms_teams_ssl_verification = int(helper.get_global_setting("default_ms_teams_ssl_verification")) - ssl_certificate_validation = True - helper.log_debug("default_ms_teams_ssl_verification={}".format(default_ms_teams_ssl_verification)) - if default_ms_teams_ssl_verification == 0: - ssl_certificate_validation = False - helper.log_debug("ssl_certificate_validation={}".format(ssl_certificate_validation)) - # Retrieve parameters message_uuid = helper.get_param("message_uuid") helper.log_debug("message_uuid={}".format(message_uuid)) @@ -68,7 +65,7 @@ def process_event(helper, *args, **kwargs): message_data = helper.get_param("message_data") helper.log_debug("message_data={}".format(message_data)) - #message_data = checkstr(message_data) + # message_data = checkstr(message_data) message_status = helper.get_param("message_status") helper.log_debug("message_status={}".format(message_status)) @@ -87,15 +84,19 @@ def process_event(helper, *args, **kwargs): # For Splunk Cloud vetting, the URL must start with https:// if not message_url.startswith("https://"): - helper.log_error("Non https rest calls are not allowed for vetting compliance purposes") + helper.log_error( + "Non https rest calls are not allowed for vetting compliance purposes" + ) return False # Properly load json try: message_data = json.dumps(json.loads(message_data, strict=False), indent=4) except Exception as e: - helper.log_error("json loads failed to accept some of the characters," - " raw json data before json.loads:={}".format(message_data)) + helper.log_error( + "json loads failed to accept some of the characters," + " raw json data before json.loads:={}".format(message_data) + ) raise e # log json in debug mode @@ -103,7 +104,7 @@ def process_event(helper, *args, **kwargs): # Build the header headers = { - 'Content-Type': 'application/json', + "Content-Type": "application/json", } helper.log_debug("message_no_attempts={}".format(message_no_attempts)) @@ -112,160 +113,250 @@ def process_event(helper, *args, **kwargs): if int(message_no_attempts) < int(message_max_attempts): - helper.log_info('Microsoft Teams creation attempting for record with uuid=' + message_uuid) + helper.log_info( + "Microsoft Teams creation attempting for record with uuid=" + message_uuid + ) # Try http post, catch exceptions and incorrect http return codes # Splunk Cloud vetting note, verify SSL is required for vetting purposes and enabled by default try: - response = helper.send_http_request(message_url, "POST", parameters=None, payload=message_data, - headers=headers, cookies=None, verify=ssl_certificate_validation, - cert=None, timeout=120, use_proxy=opt_use_proxy) + response = helper.send_http_request( + message_url, + "POST", + parameters=None, + payload=message_data, + headers=headers, + cookies=None, + verify=True, + cert=None, + timeout=120, + use_proxy=opt_use_proxy, + ) # No http exception, but http post was not successful if response.status_code not in (200, 201, 204): - helper.log_error("Microsoft Teams publish to channel has failed!. " - "url={}, data={}, HTTP Error={}, HTTP Reason={}, HTTP content={}" - .format(message_url, message_data, response.status_code, - response.reason, response.text)) - helper.log_info('Updating KVstore message record with uuid=' + message_uuid) - - record_url = 'https://localhost:' + str( - splunkd_port) + '/servicesNS/nobody/' \ - 'TA-ms-teams-alert-action/storage/collections/data/kv_ms_teams_failures_replay/' \ - + message_uuid + helper.log_error( + "Microsoft Teams publish to channel has failed!. " + "url={}, data={}, HTTP Error={}, HTTP Reason={}, HTTP content={}".format( + message_url, + message_data, + response.status_code, + response.reason, + response.text, + ) + ) + helper.log_info( + "Updating KVstore message record with uuid=" + message_uuid + ) + + record_url = ( + "https://localhost:" + str(splunkd_port) + "/servicesNS/nobody/" + "TA-ms-teams-alert-action/storage/collections/data/kv_ms_teams_failures_replay/" + + message_uuid + ) headers = { - 'Authorization': 'Splunk %s' % session_key, - 'Content-Type': 'application/json'} + "Authorization": "Splunk %s" % session_key, + "Content-Type": "application/json", + } message_no_attempts = int(message_no_attempts) + 1 # Update the KVstore record with the increment, and the new mtime - record = '{"_key": "' + str(message_uuid) + '", "url": "' + str(message_url) \ - + '", "ctime": "' + str(message_ctime) + '", "mtime": "' + str(time.time()) \ - + '", "status": "temporary_failure", "no_attempts": "' + str(message_no_attempts) \ - + '", "data": "' + checkstr(message_data) + '"}' + record = ( + '{"_key": "' + + str(message_uuid) + + '", "url": "' + + str(message_url) + + '", "ctime": "' + + str(message_ctime) + + '", "mtime": "' + + str(time.time()) + + '", "status": "temporary_failure", "no_attempts": "' + + str(message_no_attempts) + + '", "data": "' + + checkstr(message_data) + + '"}' + ) # Splunk Cloud vetting note, this communication is a localhost communication to splunkd # and does not have to be verified - response = requests.post(record_url, headers=headers, data=record, - verify=False) + response = requests.post( + record_url, headers=headers, data=record, verify=False + ) if response.status_code not in (200, 201, 204): helper.log_error( - 'KVstore saving has failed!. url={}, data={}, HTTP Error={}, ' - 'content={}'.format(record_url, record, response.status_code, response.text)) + "KVstore saving has failed!. url={}, data={}, HTTP Error={}, " + "content={}".format( + record_url, record, response.status_code, response.text + ) + ) return response.status_code else: # http post was successful message_creation_response = response.text - helper.log_info('Microsoft Teams message successfully created. {}, ' - 'content={}'.format(message_url, message_creation_response)) + helper.log_info( + "Microsoft Teams message successfully created. {}, " + "content={}".format(message_url, message_creation_response) + ) helper.log_info("Purging message in KVstore with uuid=" + message_uuid) # The JIRA ticket has been successfully created, and be safety removed from the KVstore - record_url = 'https://localhost:' + str( - splunkd_port) + '/servicesNS/nobody/' \ - 'TA-ms-teams-alert-action/storage/collections/data/' \ - 'kv_ms_teams_failures_replay/' + message_uuid + record_url = ( + "https://localhost:" + str(splunkd_port) + "/servicesNS/nobody/" + "TA-ms-teams-alert-action/storage/collections/data/" + "kv_ms_teams_failures_replay/" + message_uuid + ) headers = { - 'Authorization': 'Splunk %s' % session_key, - 'Content-Type': 'application/json'} + "Authorization": "Splunk %s" % session_key, + "Content-Type": "application/json", + } # Splunk Cloud vetting note, this communication is a localhost communication to splunkd # and does not have to be verified response = requests.delete(record_url, headers=headers, verify=False) if response.status_code not in (200, 201, 204): helper.log_error( - 'KVstore delete operation has failed!. url={}, HTTP Error={}, ' - 'content={}'.format(record_url, response.status_code, response.text)) + "KVstore delete operation has failed!. url={}, HTTP Error={}, " + "content={}".format( + record_url, response.status_code, response.text + ) + ) return response.status_code else: return 0 # any exception such as proxy error, dns failure etc. will be catch here except Exception as e: - helper.log_error("Microsoft Teams publish to channel has failed!:{}".format(str(e))) helper.log_error( - 'message content={}'.format(message_data)) + "Microsoft Teams publish to channel has failed!:{}".format(str(e)) + ) + helper.log_error("message content={}".format(message_data)) - helper.log_info('Updating KVstore message record with uuid=' + message_uuid) + helper.log_info("Updating KVstore message record with uuid=" + message_uuid) - record_url = 'https://localhost:' + str( - splunkd_port) + '/servicesNS/nobody/' \ - 'TA-ms-teams-alert-action/storage/collections/data/kv_ms_teams_failures_replay/' \ - + message_uuid + record_url = ( + "https://localhost:" + str(splunkd_port) + "/servicesNS/nobody/" + "TA-ms-teams-alert-action/storage/collections/data/kv_ms_teams_failures_replay/" + + message_uuid + ) headers = { - 'Authorization': 'Splunk %s' % session_key, - 'Content-Type': 'application/json'} + "Authorization": "Splunk %s" % session_key, + "Content-Type": "application/json", + } message_no_attempts = int(message_no_attempts) + 1 # Update the KVstore record with the increment, and the new mtime - record = '{"_key": "' + str(message_uuid) + '", "url": "' + str(message_url) \ - + '", "ctime": "' + str(message_ctime) + '", "mtime": "' + str(time.time()) \ - + '", "status": "temporary_failure", "no_attempts": "' + str(message_no_attempts) \ - + '", "data": "' + checkstr(message_data) + '"}' + record = ( + '{"_key": "' + + str(message_uuid) + + '", "url": "' + + str(message_url) + + '", "ctime": "' + + str(message_ctime) + + '", "mtime": "' + + str(time.time()) + + '", "status": "temporary_failure", "no_attempts": "' + + str(message_no_attempts) + + '", "data": "' + + checkstr(message_data) + + '"}' + ) # Splunk Cloud vetting note, this communication is a localhost communication to splunkd and # does not have to be verified - response = requests.post(record_url, headers=headers, data=record, - verify=False) + response = requests.post( + record_url, headers=headers, data=record, verify=False + ) if response.status_code not in (200, 201, 204): helper.log_error( - 'KVstore saving has failed!. url={}, data={}, HTTP Error={}, ' - 'content={}'.format(record_url, record, response.status_code, response.text)) + "KVstore saving has failed!. url={}, data={}, HTTP Error={}, " + "content={}".format( + record_url, record, response.status_code, response.text + ) + ) return response.status_code # the message has reached its maximal amount of attempts, this is a permanent failure - elif (int(message_no_attempts) >= int(message_max_attempts)) and str(message_status) in "temporary_failure": - - helper.log_info('KVstore Microsoft Teams message record with uuid=' + message_uuid - + " permanent failure!:={}".format(message_data)) - - record_url = 'https://localhost:' + str( - splunkd_port) + '/servicesNS/nobody/' \ - 'TA-ms-teams-alert-action/storage/collections/data/kv_ms_teams_failures_replay/' \ - + message_uuid + elif (int(message_no_attempts) >= int(message_max_attempts)) and str( + message_status + ) in "temporary_failure": + + helper.log_info( + "KVstore Microsoft Teams message record with uuid=" + + message_uuid + + " permanent failure!:={}".format(message_data) + ) + + record_url = ( + "https://localhost:" + str(splunkd_port) + "/servicesNS/nobody/" + "TA-ms-teams-alert-action/storage/collections/data/kv_ms_teams_failures_replay/" + + message_uuid + ) headers = { - 'Authorization': 'Splunk %s' % session_key, - 'Content-Type': 'application/json'} + "Authorization": "Splunk %s" % session_key, + "Content-Type": "application/json", + } # Update the KVstore record with the increment, and the new mtime - record = '{"_key": "' + str(message_uuid) + '", "url": "' + str(message_url) \ - + '", "ctime": "' + str(message_ctime) + '", "mtime": "' + str(time.time()) \ - + '", "status": "permanent_failure", "no_attempts": "' + str(message_no_attempts) \ - + '", "data": "' + checkstr(message_data) + '"}' + record = ( + '{"_key": "' + + str(message_uuid) + + '", "url": "' + + str(message_url) + + '", "ctime": "' + + str(message_ctime) + + '", "mtime": "' + + str(time.time()) + + '", "status": "permanent_failure", "no_attempts": "' + + str(message_no_attempts) + + '", "data": "' + + checkstr(message_data) + + '"}' + ) # Splunk Cloud vetting note, this communication is a localhost communication to splunkd and # does not have to be verified - response = requests.post(record_url, headers=headers, data=record, - verify=False) + response = requests.post(record_url, headers=headers, data=record, verify=False) if response.status_code not in (200, 201, 204): helper.log_error( - 'KVstore saving has failed!. url={}, data={}, HTTP Error={}, ' - 'content={}'.format(record_url, record, response.status_code, response.text)) + "KVstore saving has failed!. url={}, data={}, HTTP Error={}, " + "content={}".format( + record_url, record, response.status_code, response.text + ) + ) return response.status_code else: return 0 # manage permanent failure and removal - elif int(message_no_attempts) >= int(message_max_attempts) and str(message_status) in "tagged_for_removal": - - helper.log_info("Message in KVstore with uuid=" + message_uuid - + " has reached the maximal number of attempts and is tagged for removal," - " purging the record from the KVstore:={}".format(message_data)) + elif ( + int(message_no_attempts) >= int(message_max_attempts) + and str(message_status) in "tagged_for_removal" + ): + + helper.log_info( + "Message in KVstore with uuid=" + + message_uuid + + " has reached the maximal number of attempts and is tagged for removal," + " purging the record from the KVstore:={}".format(message_data) + ) # remove the object from the KVstore - record_url = 'https://localhost:' + str( - splunkd_port) + '/servicesNS/nobody/' \ - 'TA-ms-teams-alert-action/storage/collections/data/kv_ms_teams_failures_replay/' \ - + message_uuid + record_url = ( + "https://localhost:" + str(splunkd_port) + "/servicesNS/nobody/" + "TA-ms-teams-alert-action/storage/collections/data/kv_ms_teams_failures_replay/" + + message_uuid + ) headers = { - 'Authorization': 'Splunk %s' % session_key, - 'Content-Type': 'application/json'} + "Authorization": "Splunk %s" % session_key, + "Content-Type": "application/json", + } # Splunk Cloud vetting note, this communication is a localhost communication to splunkd and # does not have to be verified response = requests.delete(record_url, headers=headers, verify=False) if response.status_code not in (200, 201, 204): helper.log_error( - 'KVstore delete operation has failed!. url={}, HTTP Error={}, ' - 'content={}'.format(record_url, response.status_code, response.text)) + "KVstore delete operation has failed!. url={}, HTTP Error={}, " + "content={}".format(record_url, response.status_code, response.text) + ) return response.status_code else: return 0 @@ -273,10 +364,15 @@ def process_event(helper, *args, **kwargs): else: if str(message_status) in "permanent_failure": - helper.log_info("Ticket in KVstore with uuid=" + message_uuid - + " will be tagged for removal and purged upon expiration.") + helper.log_info( + "Ticket in KVstore with uuid=" + + message_uuid + + " will be tagged for removal and purged upon expiration." + ) else: - helper.log_info("Ticket in KVstore with uuid=" + message_uuid - + " has no action detected ?") + helper.log_info( + "Ticket in KVstore with uuid=" + + message_uuid + + " has no action detected ?" + ) return 0 - diff --git a/package/lib/requirements.txt b/package/lib/requirements.txt index 0b1cd8f..3d39428 100644 --- a/package/lib/requirements.txt +++ b/package/lib/requirements.txt @@ -1 +1 @@ -splunktaucclib>=5.0.4 \ No newline at end of file +splunktaucclib>=6.2.0 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..f4e50e7 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +splunk-add-on-ucc-framework>=5.44.0 \ No newline at end of file diff --git a/version.json b/version.json new file mode 100644 index 0000000..80261f9 --- /dev/null +++ b/version.json @@ -0,0 +1,4 @@ +{ + "version": "1.1.6", + "appID": "TA-ms-teams-alert-action" +} diff --git a/version.txt b/version.txt deleted file mode 100644 index bf503fc..0000000 --- a/version.txt +++ /dev/null @@ -1 +0,0 @@ -version = 1.1.5