Automating InsightVM Site Scope Updates

Today’s network environments are diverse and often constantly in flux. On top of that, the number of sources for host data on the network can also be just as varied. InsightVM provides capabilities to import assets from sources like Microsoft DHCP, Azure, and AWS; but maybe you have other sources to pull from. Or you’re looking to restructure your sites all together.

Whatever the case may be, the InsightVM API (v3) can provide the capabilities that you need to automate the configuration of site scopes to save you time and facilitate whatever workflow that you have in mind. Let’s walk through creating a Python script to update site scopes from a text file.

Getting started

To get things started, we’ll use the base of the script provided in Zac’s post on Extracting Bulk Data with the InsightVM Console API. The InsightVmApi class provided by that example is a part of the script at the end of this post. With a class for the connection to the API created, we can then get started on adding a couple methods. First we’ll add a method to read from our file and add all of the IP addresses/ranges to an array, then create our method to update a site by name. Note that in our example, we’re reading parameters for the script from the following environment variables:

Variable Description
INSIGHTVM_HOST Security Console host URL, format: <ip/hostname>:<port>
INSIGHTVM_USER API username
INSIGHTVM_PASS API user password
INSIGHTVM_SCOPE_FILE File from which IP address/ranges will be read
INSIGHTVM_SITE_ID ID of site that will have its scope updated. This can be retrieved from the URL of the site’s detail page or by using the /api/3/sites endpoint

Reading from a File

Python makes reading from a file very easy, but we can do one better and also validate the values in the file to ensure that they are valid IP addresses/ranges. To do this, we’ll use the following method:

def get_ips_from_file(file):
    # Use a set to get uniqueness for 'free'
    ips = set()
    if not os.path.isfile(file):
        exit(
            f"INSIGHTVM_SCOPE_FILE {file} does not exist, please create the file or update the environment variable.")
    with open(file, 'r') as f:
        for line in f:
            ip = line.strip()
            try:
                if '/' in ip:
                    # Allow for host bits to be set in the network
                    ipaddress.ip_network(ip, False)
                else:
                    ipaddress.ip_address(ip)
            except ValueError:
                print(
                    f"'{ip}' is not a valid IP address or range, omitting this value from the site scope")
            else:
                ips.add(ip)
    return list(ips)

Here we’re using the ipaddress library which is part of the Python 3 standard library to validate both networks and addresses in the file. If a value is not valid, it is simply not added to the scope.

This is just a function in our file. For updating the scope of a site, we’ll create an instance method on our InsightVmApi class.

Updating Site Scopes

With a list of IP addresses/ranges, we’re ready to roll with updating our site scope. All we need to do is send the payload to the right API endpoint. In this case, we’ll be sending a PUT request to the /api/3/sites/{id}/included_targets endpoint. It expects a JSON formatted array, so we’ll use the json library from the Python 3 standard library to format it properly. The instance method looks like this:

def update_site_scope(self, site_id, scope):
    endpoint = f"{self.base_resource}/sites/{site_id}/included_targets"
    json_scope = json.dumps(scope)
    self.conn.request('PUT', endpoint, body=json_scope,
                        headers=self.headers)
    resp = self.conn.getresponse()
    if resp.status != 200:
        body = json.dumps(resp.read().decode())
        print(f"Failed to update site scope, response: {body}")
    else:
        print(
            f"Successfully updated site ID {site_id} with {len(scope)} scope values")
    return

With a scope.txt file that looks like this:

192.168.1.1
10.0.0.1/24
999.999.999.999

Our results should look like the following:

~/Desktop $ python blog.py
'999.999.999.999' is not a valid IP address or range, omitting this value from the site scope
Successfully updated site ID 211 with 2 scope values


There are TONS of ways to expand on this use case for the API, but this example should give you a great starting point! Below you’ll find the full script used for this post. Note that you will need Python 3.6+ installed to run it.

Script
from base64 import b64encode
from datetime import datetime
import http.client
import ipaddress
import json
import os
import ssl
import sys
from time import sleep
import uuid


class InsightVmApi:
    def __init__(self, url, username, password, verify_ssl):
        # Craft basic authentication
        auth = f"{username}:{password}"
        auth = b64encode(auth.encode('ascii')).decode()

        self.base_resource = "/api/3"
        self.headers = {
            'Accept': "application/json",
            'Content-Type': "application/json",
            'Authorization': f"Basic {auth}"
        }
        self.conn = http.client.HTTPSConnection(url)

        if verify_ssl.lower() == 'false':
            # Ignore certificate verification for self-signed certificate; NOT to be used in production
            self.conn._context = ssl._create_unverified_context()

    def update_site_scope(self, site_id, scope):
        endpoint = f"{self.base_resource}/sites/{site_id}/included_targets"
        json_scope = json.dumps(scope)
        self.conn.request('PUT', endpoint, body=json_scope,
                          headers=self.headers)
        resp = self.conn.getresponse()
        if resp.status != 200:
            body = json.dumps(resp.read().decode())
            print(f"Failed to update site scope, response: {body}")
        else:
            print(
                f"Successfully updated site ID {site_id} with {len(scope)} scope values")
        return


def get_ips_from_file(file):
    # Use a set to get uniqueness for 'free'
    ips = set()
    if not os.path.isfile(file):
        exit(
            f"INSIGHTVM_SCOPE_FILE {file} does not exist, please create the file or update the environment variable.")
    with open(file, 'r') as f:
        for line in f:
            ip = line.strip()
            try:
                if '/' in ip:
                    # Allow for host bits to be set in the network
                    ipaddress.ip_network(ip, False)
                else:
                    ipaddress.ip_address(ip)
            except ValueError:
                print(
                    f"'{ip}' is not a valid IP address or range, omitting this value from the site scope")
            else:
                ips.add(ip)
    return list(ips)


if __name__ == '__main__':
    HOST = os.environ.get("INSIGHTVM_HOST", "")  # Format: <ip/hostname>:<port>
    # InsightVM Console user with permissions to generate reports
    USER = os.environ.get("INSIGHTVM_USER", "")
    PASS = os.environ.get("INSIGHTVM_PASS", "")
    FILE = os.environ.get("INSIGHTVM_SCOPE_FILE", "scope.txt")
    SITE_ID = os.environ.get("INSIGHTVM_SITE_ID", "")
    # Override to False to ignore certification verification
    SSL_VERIFY = os.environ.get("INSIGHTVM_SSL_VERIFY", "true")

    if any(v is None or v is "" for v in [HOST, USER, PASS, FILE, SITE_ID]):
        sys.exit(
            "Host, user, password, scope file, or site name not defined; check environment variables and try again!")

    scope = get_ips_from_file(FILE)
    insightvm = InsightVmApi(HOST, USER, PASS, SSL_VERIFY)
    insightvm.update_site_scope(SITE_ID, scope)

1 Like

I’ve done something similar here for ad-hoc scanning with SNOW tickets. It reads the assets from the ticket, verifies the assets are valid, and passes them on to included_targets to later be scanned.

It works like a charm and helps save this particular customer a lot of time by automating what was before a completely manual process.

4 Likes