- Post History
- Subscribe to RSS Feed
- Mark as New
- Mark as Read
- Bookmark
- Subscribe
- Printer Friendly Page
- Report Inappropriate Content
on 09-08-2022 11:52 AM
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".
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".
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".
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
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.
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)
};
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.
(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).
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.
- 2,798 Views
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
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?
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
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.