Will Hallam
ServiceNow Employee
ServiceNow Employee

Disclaimer: the examples in this article come with no support or warranty, implied or explicit.  Caveat emptor!

 

One of the challenges with containerized workloads is that containers can be less accessible to several discovery methods which were mainstays for exploring server-based workloads.  There's no guarantee you can use SSH or PowerShell to remote into a container; in fact, it is often best practice for a container to have a highly-minimized set of software packages installed in it.  Here's one example of some tweaks which can provide visibility into the software packages installed in a Docker container, broken down by the pieces it requires.

Docker SBOM Plugin

Docker has a plugin, still classified "experimental", which extracts a Software Bill of Materials ("SBOM") from a given container image.  I installed it on my Docker hosts in order to pass that list of packages into the Docker discovery pattern.  The plugin is based on the Syft project (https://github.com/anchore/syft).  Syft can alternatively be installed on its own.

Uploaded File

For my experiment I chose to write a Python script to perform the heavy lifting of iterating through Docker containers, images, and packages on behalf of the Docker pattern. 

#!/bin/python3
import subprocess
tainerImgs=subprocess.run(["sudo","docker","ps","-a","--format","'{{.Image}}'"],stdout=subprocess.PIPE,stderr=subprocess.PIPE,encoding='UTF-8')
for tainerImg in tainerImgs.stdout.split():
    tainerImg=tainerImg.replace("'","")
    tainerImgInsp=subprocess.run(["sudo","docker","inspect",tainerImg,"--format","'{{.Id}}'"],stdout=subprocess.PIPE,stderr=subprocess.PIPE,encoding='UTF-8')
    tainerImgId=tainerImgInsp.stdout
    tainerImgId=tainerImgId.replace("'","").strip()
    imgPkgs=subprocess.run(["sudo","docker","sbom",tainerImgId],stdout=subprocess.PIPE,stderr=subprocess.DEVNULL,encoding='UTF-8')
    for line in imgPkgs.stdout.splitlines():
        print (tainerImgId+"\t"+line)

I attached the script to a new Uploaded File record (found under the "Pattern Designer" module).

Pattern Update

NOTE: for my experiment I modified the Docker pattern in place; for a real world implementation you may want to start with a copy of the Docker pattern and modify that.

Since my pattern updates depend on the Docker SBOM plugin, I added a "Parse Command Output" step to the Docker pattern which checks that the plugin is installed.  It runs "$sudo + "docker sbom --help> /dev/null 2>&1; echo $?" and stores the result of that in a temporary variable named "docker_sbom_installed".

find_real_file.png

Next I added a "Put File" step to copy my Uploaded File script to the Docker host.  I store the full path of the script in a temporary variable named "docker_pkg".

find_real_file.png

Then I added a "Parse Command Output" step immediately below the "inspect docker images" step, which executes the script and stores the output in a temporary table named "docker_pkgs".

find_real_file.png

The final pattern modification is to create a "Transform Table" step, located right after the "set docker global image table" step, to take the output of the script and store it in the existing "attributes" column of the cmdb_ci_docker_image table.  It does this using the following Groovy code snippet:

arry = ${docker_pkgs[*].pkg}
img_id=${cmdb_ci_docker_image[].image_digest}
str = ""

for (i=0;i<arry.size();i++) {
	pkgA=arry[i].split()
	if (pkgA[0]==img_id) {
		str = str + pkgA[1]+"@"+pkgA[2]+"@"+pkgA[3]+"\n"
	}
}

return str

find_real_file.png

With these pattern updates in place, after discovering a Docker host each container image record will have its "attributes" column populated with a list of installed packages.

find_real_file.png

Table

NOTE: the creation of a new table is something I did in order to work around what appears to be a permissions issue with writing directly to the existing cmdb_software_instance table; given more time I could probably have figured out the write sauce to enable writing directly into it, but I had time constraints.

I created a global table named "Container Software Instance (u_container_software_instance)" with the following two attributes: u_software (reference to cmdb_ci_spkg) and u_installed_on (reference to cmdb_ci).  I gave all application scopes full read/write/delete/create access to this new table.

Post Script

In order to take the package inventory data from the "attributes" column and populate cmdb_ci_spkg and cmdb_software_instance, I created a script under Pattern Designer->Pre Post Processing.  This script ingests the IRE result payload from the Docker pattern run.  For each cmdb_ci_docker_image object found, it looks for a value in the "attributes" column.  If a value is found, the script parses it into the corresponding cmdb_ci_spkg and cmdb_software_instance records.  Due to issues writing directly to cmdb_software_instance, the software instances are written to my custom u_container_software_instance table.

var rtrn = {};

//parsing the json string to a json object
var payloadObj = JSON.parse(payload);


for (var i in payloadObj.items) {
    item = payloadObj.items[i];

    if (item.className == "cmdb_ci_docker_image") {

        var contGr = new GlideRecord('cmdb_ci_docker_image');

        // check for discovered pkgs
        contGr.get(item.sysId);
        var pkgs = contGr.getValue('attributes');
        if (pkgs) {
            var pkgList = pkgs.split("\n");
            for (var ind1 = 0; ind1 < pkgList.length; ind1++) {
                gs.info("CONTAINER PKGS: got pkg " + pkgList[ind1]);
                pkgRec = pkgList[ind1].split("@");
                if (pkgRec[0] != "NAME" && pkgRec[0].length>0) {
                    gs.info("CONTAINER PKGS: valid pkg " + pkgRec[0]);
                    var sendToIre = {
                        "items": [{
                            "className": "cmdb_ci_spkg",
                            "values": {
                                "name": pkgRec[0],
                                "version": pkgRec[1],
                                "key": pkgRec[0] + "_:::_" + pkgRec[1]
                            }
                        }]
                    };
                    var ireOutput = sn_cmdb.IdentificationEngine.createOrUpdateCI("ServiceNow", JSON.stringify(sendToIre));
                    gs.info("CONTAINER PKGS: IRE returns " + ireOutput);

                    var ireOutputObj = JSON.parse(ireOutput);
                    if (ireOutputObj["items"][0].hasOwnProperty('sysId')) {
                        var swInstGr = new GlideRecord('cmdb_software_instance');
                        swInstGr.addQuery("installed_on", item.sysId);
                        swInstGr.addQuery("software", ireOutputObj["items"][0].sysId);
                        swInstGr.query();
                        if (!(swInstGr.hasNext())) {
                            var insertGr = new GlideRecord('u_container_software_instance');
                            insertGr.initialize();
                            insertGr.setValue("u_installed_on", item.sysId);
                            insertGr.setValue("u_software", ireOutputObj["items"][0].sysId);
							/*var insertGr = new GlideRecord('cmdb_software_instance');
                            insertGr.initialize();
                            insertGr.setValue("installed_on", item.sysId);
                            insertGr.setValue("software", ireOutputObj["items"][0].sysId);*/
                            gs.info("CONTAINER PKGS: adding software " + insertGr.getValue('u_software') + " for CI " + insertGr.getValue('u_installed_on') + ", canCreate is " + insertGr.canCreate());
							insertGr.setWorkflow(false);
                            var insResult = insertGr.insert();
                            gs.info("CONTAINER PKGS: added software " + insertGr.getValue('u_software') + " for CI " + insertGr.getValue('u_installed_on') + ", instance is " + insResult);
                        }
                    }
                }
            }
        }
    }
}


//you can return a message and a status, on top of the input variables that you MUST return.
//returning the payload as a Json String is mandatory in case of a pre sensor script, and optional in case of post sensor script.
//if you want to terminate the payload processing due to your business logic - you can set isSuccess to false.
rtrn = {
    'status': {
        'message': 'Message!',
        'isSuccess': true
    },
    'patternId': patternId,
    'payload': JSON.stringify(payloadObj)
};

find_real_file.png

Once this post script is in place, when the Docker pattern discovers a container image it will create requisite cmdb_ci_spkg and u_container_software_instance records.

Business Rule

Since the software instance data is initially written to a custom table, I created a business rule to populate cmdb_software_instance based on what is inserted into u_container_software_instance.

find_real_file.png

(function executeRule(current, previous /*null when async*/) {

var queryGr=new GlideRecord('cmdb_software_instance');
	var insertGr=new GlideRecord('cmdb_software_instance');
	queryGr.addQuery('software',current.u_software);
	queryGr.addQuery('installed_on',current.u_installed_on);
	queryGr.query();
	if (!(queryGr.hasNext())) {
		insertGr.initialize();
		insertGr.setValue('software',current.u_software);
		insertGr.setValue('installed_on',current.u_installed_on);
		insertGr.insert();
	}
	
})(current, previous);

Related List

Once the pieces I reviewed above were in place, I was able to add a "Software Installed" related list to my Docker Image form (Configuration->Docker->Global Images).

find_real_file.png

Conclusion

This specific example could be modified to support different container implementations, e.g., Kubernetes.  I worked on this Docker host scenario originally due to a customer question; the principles involved would largely work in a similar fashion though.  As long as the container image is being discovered and a utility like Syft can be present in the environment, we can get a software inventory and tie it to a container environment. 

Comments
Stefan Linthors
Giga Explorer

Looking forward to see Software inventory for containers as standard functionality in ServiceNow SAM Pro, enabling Software Asset Managers as well as Life Cycle Managers to be in control on software running on these platfroms as well. Any idea what the plans are?

Will Hallam
ServiceNow Employee
ServiceNow Employee

There may be something in the works -- you should ask your ServiceNow account team for a roadmap update, with a focus on discovering software in container images.

Version history
Last update:
‎09-08-2022 11:52 AM
Updated by: