Janel
Kilo Sage

We have a couple workflows that open change requests.  Part of our updated change management process is that Normal changes require a Risk Assessment be completed.  We didn't want to deviate from that rule no matter what was opening the CR, so we needed a way to script the risk assessment and attach it to the CR. And we figured APIs would probably use it so we wanted a way to pass a JSON payload of the metric questions and responses to it as well.

This is how we did it server side.  If you need a client side version you'll need to modify this script include or create a second one to pass over.

This does take some coordination with whomever is managing your Risk Assessments to not go in and change the name of the metric questions, the definition display value, or the definition value (asmt_metric_definition) without getting this script include updated.

Created a script include called "u_changeAPIUtils" (you can call yours whatever you want). 
Client callable: False
Accessible from: All application scopes (not super sure this is necessary)

  1. We select which risk assessment to take.
  2. If a JSON payload was passed in, we make sure we have matching values to any display_values otherwise we set the "default values" as the JSON payload (this makes it easier later).
  3. Create the assessment group (asmt_assessment).
  4. Create the assessment instance (asmt_assessment_instance).
  5. Lookup the risk assessment questions (asmt_metric).
  6. For each question on the risk assessment,
    1. Create a metric result (asmt_metric_result) record using the JSON payload.
      1. Remember, we normalized the JSON payload or set a default JSON payload earlier.
      2. If we have a metric question (asmt_metric) that matches an object in our payload, use the value we determined earlier as the actual_value.
      3. If we don't have a matching JSON object, we assume the smallest possible value.  This is just a cover if someone added a new question to the risk assessment that we did not have ready in our payload.
    2. Create an instance question record (asmt_assessment_instance_question), copying what we did for the metric result question.
  7. Go back to the assessment instance and change it to complete.
  8. Done!

 

var u_ChangeAPIUtils = Class.create();
u_ChangeAPIUtils.prototype = {
    initialize: function() {},

    /* Create and submit a risk assessment so risk condtiions will run and the assessment can be taken again if the CR goes back to New.  And so the state transitions won't prevent the CR from being submitted.
     * current: the GlideObject of the change request 
     * metricObj: an optional JSON object of the risk assessment questions and answers */
    submitRiskAssessment: function(cr, metricObj) {
        this.chg_risk_asmt = 'sys_id of the Risk Assessment to be take'; //The metric_type on change_risk_asmt.  You could pass this as an arguement to the function as well.

        /* Step 0) 
         * If a JSON payload of the metric names with the values was not passed, then we create a default version.
         * If a JSON payload was passed, we verify we have a value in the object to pair up with any display_value passed. */
        if (!metricObj) { //If a metricObj wasn't passed, we create a default one instead.
            metricObj = {
                //The object name needs to be the asmt_metric.name field (on the question).  You could change this to the exact question instead, but may encounter issues with special characters.  And you'll need to change the update below (the if (metricObj[metric_qs.name])) to go against the other field instead.  Name isn't ever displayed to users so that is why we picked that.

                //This list will need to be updated anytime a question is added, removed, or the display_value or actual_value is changed.  I suppose you could store this somewhere so you're not editing a script include every time.
                "Complexity": {
                    "display_value": 'Not complex',
                    "value": 1
                },
                "Critical CIs or services affected": {
                    "display_value": 'No',
                    "value": 1
                },
                "Outage required": {
                    "display_value": 'No outage',
                    "value": 1
                },
                "Verification": {
                    "display_value": 'Not difficult',
                    "value": 1
                },
                "Backout difficulty": {
                    "display_value": "Not difficult",
                    "value": 1
                },
                "Compliance and regulated data": {
                    "display_value": "No",
                    "value": 1
                }
            };
        } else {
            //We don't know for sure what was passed in.  It could contain only display_values, so we go find the values associated to the display value and assign them now.  Let's normalize!
            metricObj = this._assignRiskValuesToDisplay(metricObj);
        }


        /* Step 1) 
         * We need to create the asmt_assessment.  This is needed to link to the assessment instance.  It is also where the metric results (answered questiosns, I think) will get attached to. */
        var asmt_group = new GlideRecord('asmt_assessment'); //This is the Assessment group when looking at an assessment instance.
        asmt_group.initialize();
        asmt_group.metric_type = this.chg_risk_asmt;
        asmt_group.insert();

        /* Step 2)
         * Next we create the assessment instance that gets related to the assement group. */
        var instance = new GlideRecord('asmt_assessment_instance');
        instance.initialize();
        instance.metric_type = this.chg_risk_asmt;
        instance.assessment_group = asmt_group.sys_id;
        instance.user = cr.assigned_to.sys_id; //Using the sys_id causes a unique key violation for unknown reasons.

        //If these aren't completed it generates a unique key violation error.
        var laterDate = new GlideDate();
        laterDate.addDaysUTC(14);
        instance.expiration_date = laterDate;
        instance.due_date = new GlideDate();
        instance.taken_on = new GlideDateTime();
        instance.state = 'wip'; //We come back and update this later.  If we don't save it as Work in Progress first it generates a unique key violation.
        instance.task_id = cr.sys_id;
        instance.insert();

        /* Step 3) 
         * Now for the gross part.  We have to go all the way down into the questions that are on the change_risk_asmt (sys_id from earlier) and start working our way up with values.  Oh, and we need to do it in 2 spots or the questions won't be visible if someone were to retake the risk assessment. */

        //We create a blanket value of all of the questions set to 1.  This is incase an asmt_metric question get renamed (boo!) or a new one is added that we're not skipping questions.

        //Get the metric (the questions) where the type is the change_risk_asmt.
        var metric_qs = new GlideRecord('asmt_metric');
        metric_qs.addQuery('category.metric_type', this.chg_risk_asmt);
        metric_qs.query();

        while (metric_qs.next()) {
            /* Step 4)
             * For each metric question we need to create a matching metric result.  This is where we use the JSON payload to specify what value gets set for what question.  */
            var m_result = new GlideRecord('asmt_metric_result'); //You get to it through the asmt_group
            m_result.initialize();
            m_result.metric = metric_qs.sys_id;
            m_result.instance = instance.sys_id;
            m_result.assessment_group = asmt_group.sys_id;
            m_result.user = cr.assigned_to.sys_id;

            //We assume that at some point somoene will rename or add a question and the JSON above won't be updated.
            //If the JSON payload does have a matching value by name (or you could call something out specifically instead), we set it to the value we determined earlier.  
            //If the JSON payload does *not* have a match by name (meaning the question changes or is new), we just assume the lowest value instead.  Better to add a default value than to have an empty question.  And if it isn't in your case, then move this IF statement above the "var m_result" and wrap it to the end of the while prevent "undefined" from showing up (and probably a unique key violation)
            if (metricObj[metric_qs.name]) {
                m_result.actual_value = metricObj[metric_qs.name].value;
            } else {
                m_result.actual_value = 1; //We use 1 as the lowest value.  Adjust for your risk assessment as needed.  
            }
            m_result.insert();

            /* Step 5)
             * We need to create a record on asmt_assessment_instance_question too!  If we don't create this second one here then when you try to do the Risk Assesment again it doesn't show the questions. 
             * We just create the record and pretty much copy what we just did above. */
            var insta_question = new GlideRecord('asmt_assessment_instance_question');
            insta_question.initialize();
            insta_question.metric = metric_qs.sys_id;
            insta_question.instance = instance.sys_id;
            insta_question.category = metric_qs.category.sys_id;
            insta_question.source_table = 'change_request';
            insta_question.source_id = cr.sys_id;
            insta_question.value = m_result.actual_value;
            insta_question.insert();
        } //End of metric_qs while


        /* Step 6)
         * Update the instance to 'complete' and we're done! */
        instance.state = 'complete'; //If we don't come back later for this it results in errors.
        instance.update();
        return instance.number;
    },


    /* If a metricObj was passed for the Risk Assessment, make sure there is a value paired with the display_value. */
    _assignRiskValuesToDisplay: function(metricObj) {
        //If our JSON object only has display values and no values, assign the appropriate values now.  This is way easier to do now then attempting to pair them up once we start creating records.
        for (var v in metricObj) {
            if (metricObj[v].value) { //Skip it if there is a value.  We may update this later to verify the value is valid.
                continue;
            }
            var metric_def = new GlideRecord('asmt_metric_definition');
            metric_def.addQuery('metric.category.metric_type.sys_id', this.chg_risk_asmt);
            metric_def.addQuery('metric.name', v);
            metric_def.addQuery('display', metricObj[v].display_value);
            metric_def.query();
            if (metric_def.next()) {
                metricObj[v].value = metric_def.value.toString();
            }
        }
        return metricObj;
    },




    type: 'u_ChangeAPIUtils'
};

Then you just call it like this
new global.u_ChangeAPIUtils().submitRiskAssessment(cr, payload); //Or don't include a payload.

Hope this is helpful!

Comments
conanlloyd
Giga Guru

This is fantastic and answered one of my needs! We can now create and execute risk assessments programmatically during ATF, thank you. 

I did discover a typo that you may want to correct though.  In your _assignRiskValuesToDisplay function, you have the query as: "metric_def.addQuery('metric.category.asmt_metric_type.sys_id', this.chg_risk_asmt);" which generated an invalid query error.  when I dot walked to it, I discovered that the bolded part needed to be removed.  It should be: "metric_def.addQuery('metric.category.metric_type.sys_id', this.chg_risk_asmt);"

In my use case, I discovered that it was not executing the risk calculation and updating the risk of the change, so I did the following:

  1. added a variable at the beginning of step 1:
    /* Step 1) 
    * We need to create the asmt_assessment.  This is needed to link to the assessment instance.  It is also where the metric results (answered questiosns, I think) will get attached to.
    * We also need to create a variable to get the total value of metric ansers */
    var finalValue = '0';
    var asmt_group = new GlideRecord('asmt_assessment'); //This is the Assessment group when looking at an assessment instance.
  2. added the values of the metric results to it at the end of step 5, right after the m_result.insert();
    m_result.insert();
    //Sum total value of answers as we go
    finalValue = parseInt(finalValue) + parseInt(m_result.actual_value);
  3. Added in a step 5.5 to figure out the appropriate risk based on our thresholds and update the risk value on the change.  This is right before Step 6.  NOTE, these are the threshold values and risks for my environment and would need to be changed for anyone else
    } //End of metric_qs while
    
    /*Step 5.5)
    *Since the risk calculation doesn't run, caculate risk and update change*/
    
    var updateChangeRisk = new GlideRecord('change_request');
    updateChangeRisk.get(cr.sys_id);
    if (finalValue > 0 && finalValue < 10) {
        updateChangeRisk.risk = 4; //Low
        updateChangeRisk.update();
    }
    if (finalValue > 9 && finalValue < 26) {
        updateChangeRisk.risk = 3; //Moderate
        updateChangeRisk.update();
    }
    if (finalValue > 25 && finalValue < 51) {
        updateChangeRisk.risk = 2; //High
        updateChangeRisk.update();
    }
    if (finalValue > 50) {
        updateChangeRisk.risk = 1; //Very High
        updateChangeRisk.update();
    }
    /* Step 6)
    * Update the instance to 'complete' and we're done! */​

Don't know if that will help anyone else but thought I'd throw it out there.

Finally, I have a question. Does anyone know how I can call this from an API?  I have a group that want to generate normal changes through an API.  I have everything working up to this point but don't actually know how to call this script include through an API.  Any thoughts?

Thanks again for all this work, it really has helped my team out.

~Conan

Janel
Kilo Sage

Thanks for the info!  I fixed the original script.

We setup a whole scripted REST definition for doing risk assessments, trying to leverage the OOB change APIs and the scripted Risk Assessment.

Here is our POST.  PATCH is pretty similar. 

The top part is pretty much a copy of what we saw all the other OOB Change APIs were doing.

(function process( /*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) {
    var changeSysId = request.pathParams.change_sys_id || "";
    if (!changeSysId) {
        return new sn_ws_err.BadRequestError("Change Request id not provided.  Unable to continue.");
    }
    /**/
    var changeProcess = global.ChangeProcess.findById(changeSysId);
    if (!changeProcess)
        return new sn_ws_err.NotFoundError("Change Request not found");

    var changeRequestGr = new GlideRecordSecure(global.ChangeRequest.CHANGE_REQUEST);
    if (!changeRequestGr.get(changeSysId))
        return new sn_ws_err.NotFoundError("Change Request not found");

    //We don't need to check if one exists first.  It'll just take the latest one.
    var helper = new global.u_ChangeAPIRiskUtils().submitRiskAssessment(changeRequestGr, request.body.data);
    var responseBody = {};
    if (helper.error) {
        var sendError = new sn_ws_err.ServiceError();
        sendError.setStatus(helper.httpStatus);
        sendError.setMessage(helper.message);
        sendError.setDetail(helper.detail);
        return sendError;
    }

    responseBody.message = "Risk Assessment " + helper.number + " inserted on " + changeRequestGr.number;
    responseBody.number = helper.number;
    responseBody.sys_id = helper.sys_id;
    responseBody.change_request = {
        'number': changeRequestGr.number.toString(),
        'sys_id': changeRequestGr.sys_id.toString()
    };
    response.setStatus(201);
    response.setBody(responseBody);
    return response;


})(request, response);

Here is an example payload that pairs up with the... name...?(I forget the field name) of the question from the assessment.

/api/sn_chg_rest/change/{change_sys_id}/riskasmt

{
    "Complexity": {
        "display_value""Not complexy"
    },
    "Critical CIs or services affected": {
        "value"4
    },
    "Outage required": {
        "display_value""No outage"
    },

    "Compliance and regulated data": {
        "value"1
    }
}
HK11
Tera Contributor

Hello Conalloyd / Janel,
In our organisation we have  a related requirement to hide the Request for Approval UI button on the Change Form ( Normal / Emergency change) until the Risk Assessment is completed by the requester.

So once the Risk Assessment for change is completed, the Request for Approval button should appear for the change to progress ahead.

 

Can you please help with the UI action script to achieve above scenario?

 

Thanks very much

Janel
Kilo Sage

@HK11You may want to start a new topic for that question.  This thread is about scripting a risk assessment.

HK11
Tera Contributor

Thanks Janel.
Will do

chi123
Tera Explorer

Thanks you so much @Janel for your details answer which helped me to check my Risk Assessment Workflow I created in Python SNOW Rest API. Although In my case I had to pass the instance questions sys ID as an argument to each assessment metric results instance question and the corresponding metric definition as response to it. But I could figure that out.
Also, @conanlloyd you fix is the MOST IMPORTANT change I had to incorporate which is the Risk Calculation which is the part I was missing and I was able to fully automate my solution to my company after implement that.

Thank you so much both of you for this generous share! 

HARITHA KHAREED
Tera Contributor

Hi @Janel 

 

 

Required similar script as same like you mentioned

 

In my case whenever the change request is created from the catalog item the risk assessment in change request should create and submit automatically with the values(the answers which I want to fill). How can I modify your code as per my requirement.

 

Please help me

 

 

Can you please help me.

gautam_dhamija
Tera Contributor

Hi Janel,

I'm facing an issue where the risk assessment is being completed, but the calculated risk is always showing as "Low." Could you please help me resolve this?

Version history
Last update:
‎12-15-2021 06:09 AM
Updated by: