Name

sn_em_tbac.EvtMgmtTagBasedTagEntity

Description

Object to represent an Alert Clustering Tag, with specific methods to enrich alert correlation rules.

Script

var EvtMgmtTagBasedTagEntity = Class.create();
EvtMgmtTagBasedTagEntity.prototype = {

  type: 'EvtMgmtTagBasedTagEntity',

  initialize: function(tagGr) {
      if (!tagGr) return;

      this.tbacUtils = new sn_em_tbac.EvtMgmtTagBasedAlertClusteringUtils();

      // Constants:
      this.ALERT_SOURCE = 'em_alert';
      this.ADD_INFO_SOURCE = 'additional_info';
      this.ALERT_CI_SOURCE = 'cmdb_ci';
      this.ALERT_CI_KEY_SOURCE = 'cmdb_key_value';
      this.ALERT_TAGS_SOURCE = 'alert_tags';

      this.EXACT_MATCH = 'exact';
      this.FUZZY_MATCH = 'fuzzy';
      this.PATTERN_MATCH = 'pattern';
      /* 
       * Use dot field references and NOT tagGr.getValue(FIELD).
       * When tagGr is a referenced object from M2M table then tagGr.getValue() throws error
       */

      // Escape the name of the tag entity before other scripts use it as it may break the script, see the attached image in DEF0229745 for example.
      this.name = this.tbacUtils.stripAndEscapeChars(tagGr.getValue('name'));

      // USE CAREFULLY! Not safe to be used in ACR script as it may contain characters that break it, see the attached image in DEF0229745 for example.
      this.unsafe_original_name = tagGr.getValue('name');

      this.description = tagGr.getValue('description');
      this.source = tagGr.getValue('source');
      this.selected_field = tagGr.getValue('selected_field');
      this.additional_info_key = tagGr.getValue('additional_info_key');
      this.match_method = tagGr.getValue('match_method');
      this.fuzzy_algorithm = tagGr.getValue('fuzzy_algorithm');
      this.fuzzy_similarity = this.getNumericalValue(tagGr.getValue('fuzzy_similarity'));
      this.pattern = this.getCleanPatternValue(tagGr.getValue('pattern'));
      this.sys_id = tagGr.getUniqueValue();
      this.sys_domain = tagGr.getValue('sys_domain');
      this.sys_domain_display_value = tagGr.sys_domain.getDisplayValue();
      this.sys_overrides = tagGr.getValue('sys_overrides');
      this.sys_overrides_display_value = tagGr.sys_overrides.getDisplayValue();
      this.cmdb_key = tagGr.getValue('cmdb_key');

      // Utility members

      // Dot walk of the selected field relatively to 'em_alert', e.g. "metric_name" or "cmdb_ci.ip_address".
      this.relativeSelectedField = this.computeRelativeSelectedField();

      // Value to extract from "currentAlert", through dot walk because getValue() doesn't work with dot walk and reference fields.
      // For example, getValue('metric_name') works, but the reference getValue('cmdb_ci.ip_address') doesn't work.
      this.currentAlertValue = 'String(currentAlert.' + this.relativeSelectedField + ')';


      // Value to extract from the compared alert (in turns) "gr", through dot walk because getValue() doesn't work with dot walk and reference fields.
      // For example, getValue('metric_name') works, but the reference getValue('cmdb_ci.ip_address') doesn't work.
      this.comparedAlertValue = 'String(gr.' + this.relativeSelectedField + ')';
  },

  // This JavaScript object is not a GlideRecord!!!
  // BUT, this function was added as a safety measure so values will return and errors won't be thrown if someone
  // confuses an instance of this entity with GlideRecord and accidently calls it with .getValue()
  getValue: function(fieldName) {
      if (!fieldName || !this[fieldName]) {
          return '';
      }
      return this[fieldName];
  },

  getNumericalValue: function(val, defaultVal, min, max) {
      var numVal = Number(val);

      if (isNaN(numVal)) {
          return defaultVal || 0; // 0 as fallback to defaultVal if it's undefined or empty
      }

      if (min && numVal < min) return min;
      if (max && numVal > max) return max;

      return numVal;
  },

  /**
   * This function replaces unsafe characters with pattern's match character symbol (?).
   */
  getCleanPatternValue: function(patternValue) {
      if (!patternValue) return '';
      patternValue = String(patternValue);

      // Replace the character / because it can break the regex expression in the script.
      // Replace the character " and \ because they can break the script.
      return patternValue.replaceAll('/', '?').replaceAll('"', '?').replaceAll('\\', '?');
  },

  /**
   * Computes the selected field name relatively to 'em_alert' table.
   * Compared field values are being retrieved relatively to em_alert records, 
   * so if the selected field is from 'cmdb_ci', we must dot walk there.
   * @returns: String - the selected field, relatively to 'em_alert', with dot walk if needed.
   */
  computeRelativeSelectedField: function() {
      // For cmdb ci - create 'cmdb_ci.' relative dot walk from the alert for the query.
      var dotWalkFromAlert = (this.source == 'em_alert') ? '' : (this.source + '.');
      var relativeSelectedField = dotWalkFromAlert + this.selected_field;
      return relativeSelectedField;
  },

  /**
   * Add logic before querying compared alerts.
   * currentAlert is available here.
   * @returns: String.
   */
  getBeforeQuery: function() {},

  /**
   * Add logic for querying compared alerts.
   * currentAlert is available here.
   * 'gr' GlideRecord variable of 'em_alert' is available here BEFORE its ".query()" method was called.
   * @returns: String - Query of alerts, e.g. "gr.addQuery(FIELD, OPERATOR, VALUE)"
   */
  getQuery: function() {
      var query = '';
      if (this.source) {
          switch (this.source) {
              case this.ADD_INFO_SOURCE:
                  query += 'gr.addQuery("additional_info", "CONTAINS", "' + this.additional_info_key + '");';
                  break;
              case this.ALERT_CI_KEY_SOURCE:
                  query += 'gr.addEncodedQuery("cmdb_ciISNOTEMPTY");\n';
                  break;
              case this.ALERT_TAGS_SOURCE:
                  query += 'gr.addQuery("' + this.tbacUtils.getAlertTagsField() + '", "CONTAINS", "' + this.additional_info_key + '");';
                  break;
              default:
                  if (this.match_method == this.EXACT_MATCH) {
                      // For exact match - filter by "equals" (=) only if a value exists.
                      // If a value doesn't exist ("else" condition) - we'd still like to filter anyway,
                      // as we'd want to narrow the amount of queried alerts, to reduce performance impact.
                      query += 'if (' + this.currentAlertValue + ') { \n';
                      query += 'gr.addEncodedQuery("' + this.relativeSelectedField + 'ISNOTEMPTY^' + this.relativeSelectedField + '=" + ' + this.currentAlertValue + ');\n';
                      query += '} else { \n';
                      query += 'gr.addEncodedQuery("' + this.relativeSelectedField + 'ISNOTEMPTY");\n';
                      query += '}';
                  } else {
                      query += 'gr.addEncodedQuery("' + this.relativeSelectedField + 'ISNOTEMPTY");';
                  }
                  break;
          }
      }

      return query;
  },

  /**
   * Add logic after the query result of compared alerts and before the while(gr.next) loop.
   * currentAlert is available here.
   * 'gr' is available here AFTER its ".query()" method was called and BEFORE its ".next()" method was called.
   * You can use 'gr' methods that DON'T affect the gr itself, e.g. gr.getRowCount() but NOT gr.next()
   * If the match method of the tag is fuzzy then tagBasedComparators variable for the script include is available here.
   * If the source field of the tag is additional_info then:
  	currentAddInfo - available here, contains the additional_info of the currentAlert as JavaScript object after parse.
   * @returns: String
   */
  getAfterQuery: function() {},

  /**
   * Add logic within and in the beginning of the the while(gr.next) { } loop.
   * currentAlert is available here.
   * 'gr' GlideRecord variable of an actual record in 'em_alert' is available here, after gr.next() was called.
   * You can use gr.getValue(FIELDNAME), and most other gr operations here.
   * If the match method of the tag is fuzzy then tagBasedComparators variable for the script include is available here.
   * If the source field of the tag is additional_info then:
  	currentAddInfo - available here, contains the additional_info of the currentAlert as JavaScript object after parse.
  	grAddInfo - available here, contains the additional_info of the gr as JavaScript object after parse.
   * This is basically for preparations of tag specific data, e.g. to retrieve data from a different table based on gr field value, before comparing the values.
   * @returns: String
   */
  getLoopStart: function() {},

  /**
  * Add logic that returns the condition by which tags are compared.
  * currentAlert is available here.
  * 'gr' GlideRecord variable of an actual record in 'em_alert' is available here, after gr.next() was called.
  * You can use gr.getValue(FIELDNAME), and most other gr operations here.
  * If the match method of the tag is fuzzy then tagBasedComparators variable for the script include is available here.
  * If the source field of the tag is additional_info then:
  	currentAddInfo - available here, contains the additional_info of the currentAlert as JavaScript object after parse.
  	grAddInfo - available here, contains the additional_info of the gr as JavaScript object after parse.
  * @returns: String - A condition within brackets, i.e: ( CONDITION ), that is evaluated within an if() clause.
  */
  getComparison: function() {
      var comparison = '';

      // Default values to 'em_alert', 'cmdb_ci' source, override in the switch-case ahead.
      var currentVal = this.currentAlertValue;
      var grVal = this.comparedAlertValue;
      var fieldKey = this.selected_field;

      // Extract the current alert and compared alert values by tag source.
      // Override defaults as needed.
      switch (this.source) {
          case this.ADD_INFO_SOURCE:
              currentVal = "currentAddInfo['" + this.additional_info_key + "']";
              grVal = "grAddInfo['" + this.additional_info_key + "']";
              fieldKey = this.additional_info_key;
              break;
          case this.ALERT_CI_KEY_SOURCE:
              currentVal = "currentCiKeyValues['" + this.cmdb_key + "']";
              grVal = "otherCiKeyValues['" + this.cmdb_key + "']";
              fieldKey = this.cmdb_key;
              break;
          case this.ALERT_TAGS_SOURCE:
              currentVal = "currentAlertTags['" + this.additional_info_key + "']";
              grVal = "grAlertTags['" + this.additional_info_key + "']";
              fieldKey = this.tbacUtils.getAlertTagsField();
              break;
          default:
              break;
      }

      // Use the extracted values and compare them.
      // the compared values MUST BE wrapped in brackets, e.g. ( x == y ), or ( tagBasedComparators.fuzzy(sim, s1, s2, alg) )
      //
      // Note:
      // The comparisons below are translated to: ((val1 && val2) && (val1 == val2)),
      // Which means (val1 && val2) evaluates to false and doesn't get to the comparison condition, when val1 or val2 are
      // equal to the boolean false, e.g. empty string (''), undefined, null, the number 0, NaN, boolean false.
      // Since the compared values are string values, "0" is a legitimate value to compare, in contrast to the number 0 or to the empty string "".
      // For booleans, we might compare "true" and "false" strings (or "0" and "1", and vice versa), which are always equal to the boolean true,
      // but in this case the comparison will act on (val1 == val2), and evaluate them to false if the values are not identical, as expected.
      switch (this.match_method) {
          case this.FUZZY_MATCH:
              comparison += '// Compare ' + this.source + ' field/key: "' + fieldKey + '" by Fuzzy method with similarity of ' + this.fuzzy_similarity + '%, from tag "' + this.name + '" with sys_id: ' + this.sys_id + ' from domain: ' + this.sys_domain_display_value + ' (sys_domain: ' + this.sys_domain + ') \n';
              comparison += '((' + currentVal + ' && ' + grVal + ') && tagBasedComparators.fuzzy(' + this.fuzzy_similarity + ', ' + currentVal + ', ' + grVal + ', "' + this.fuzzy_algorithm + '"))';
              break;
          case this.EXACT_MATCH:
              comparison += '// Compare ' + this.source + ' field/key: "' + fieldKey + '" by Exact method, from tag "' + this.name + '" with sys_id: ' + this.sys_id + ' from domain: ' + this.sys_domain_display_value + ' (sys_domain: ' + this.sys_domain + ') \n';
              comparison += '((' + currentVal + ' && ' + grVal + ') && tagBasedComparators.exact(' + currentVal + ', ' + grVal + '))'; // Check values exist (non-empty strings) and then compare them.
              break;
          case this.PATTERN_MATCH:
              comparison += '// Compare ' + this.source + ' field/key: "' + fieldKey + '" by Pattern method with pattern of ' + this.tbacUtils.stripAndEscapeChars(this.pattern) + ', from tag "' + this.name + '" with sys_id: ' + this.sys_id + ' from domain: ' + this.sys_domain_display_value + ' (sys_domain: ' + this.sys_domain + ') \n';

              // GlideFilter.checkRecord() can't check JSON fields (the additional_info or the sn_alert_tags of the alerts) so check it individually.
              // the additional_info check has supporting code in 
              // Also cmdb_key_value records need to be loaded into memory and regexed
              switch (this.source) {
                  case this.ADD_INFO_SOURCE:
                  case this.ALERT_TAGS_SOURCE:
                  case this.ALERT_CI_KEY_SOURCE:
                      var patternToRegex = this.pattern;
                      var lastIndex = (patternToRegex.length - 1);

                      // If the pattern doesn't begin with a wildcard (*) then prepend the regex start-of-line symbol (^)
                      if (patternToRegex[0] != '*') {
                          patternToRegex = '^' + patternToRegex;
                      }

                      // If the pattern doesn't end with a wildcard (*) then append the regex end-of-line symbol ($)
                      if (patternToRegex[lastIndex] != '*') {
                          patternToRegex = patternToRegex + '$';
                      }

                      // Convert SN's pattern identifiers to valid regex identifiers.
                      // (.*) - regex wildcard to replace the pattern's wildcard *
                      // . - regex matches any character symbol to replace the pattern's match any symbol ?
                      patternToRegex = patternToRegex.replaceAll('*', '(.*)').replaceAll('?', '.');

                      comparison += "( tagBasedComparators.pattern(" + currentVal + ", " + grVal + ", '" + patternToRegex + "') )";
                      break;

                  default:
                      var patternFieldComparison = this.relativeSelectedField + "MATCH_PAT" + this.pattern;
                      comparison += "( tagBasedComparators.checkRecord(currentAlert, \"" + patternFieldComparison + "\") && tagBasedComparators.checkRecord(gr, \"" + patternFieldComparison + "\") )";
                      break;
              }

              break;
          default:
              break;
      }

      return comparison;
  },

};

Sys ID

d6a74ffdb78530107c038229ce11a9ff

Offical Documentation

Official Docs: