Revealing the Unknown: Tenable.asm

Casey Reid a.k.a Packet Chaos
7 min readNov 14, 2022

--

You can’t protect what you can’t see!

Not only is this a popular idiom, it’s a very true statement. Often getting to market first with a new product or offering is enough to win the market over. This popular strategy is driving rapid change and expanding the attack surface. In most cases, this translates to an expanding number of blind spots.

To identify those blind spots you may consider using footprinting and reconnaissance since they are the first steps in an attack or breach. However, using tens of tools and techniques to gain open source information on your own attack surface can be quite tedious regardless of its effectiveness. The good news is you don’t have to; let Tenable.asm gather the data.

On June 26th 2022, Tenable completed the acquisition of Bit Discovery, Inc. (“Bit Discovery”); a leader in external attack surface management (EASM). This immediately added intrinsic value to Tenable’s portfolio and to their customers.

In the Tenable press release, “unseen risk” is the challenge EASM eases by:

  • Discovering previously unknown internet-connected assets
  • Applying rich context and attribution for domains, sub-domains and other exposed technologies
  • Continuously monitor your ever evolving external attack surface

Since there is no EASM panacea or easy button, let’s talk about how to operationalize this data now that Tenable makes it easy to obtain.

Challenge Brief

Finding the previously unknown assets takes more than turning on Tenable.asm. What is “known” are the assets scanned by Nessus. The assets “previously unknown” are any assets found by Tenable.asm but NOT scanned by Nessus. The goal is to find the unscanned IPs and scan them to close the blind spots!

There it is; that’s the million dollar question.

What IP addresses did Tenable.asm find that I have not scanned with Nessus?

The previously unknown assets represent a blind spot in a network and doing nothing will likely lead to a breach. It’s important to identify these assets and evaluate them with Nessus to get their full exposure and assumed risk. No solution can automatically answer this question today and if you have thousands of assets it an be extremely difficult to answer.

However, with a few lines of code and some creativity we can find the previously unknown assets and scan them while providing further insight to the Tenable.io with Tenable.asm information.

Solution Brief

At a very high level this is crazy simple. Export all “A records” from the Tenable.asm solution and import them into the Tenable.io Vulnerability Management solution.

Tenable.io has an asset import API that makes it easy to import assets from a new source and pytenable simplifies it to a single line of code! The IPs imported become Tenable assets which can be tagged for automatic scanning and reporting.

tio.assets.asset_import("ASM", host_dict)

Once we have all of the assets imported via our “ASM” source, we can use the Tenable.io filtering to find what public assets have the source ASM.

Looking at “Source is equal to ASM” is simplistic enough for my small lab. As the below screen shot confirms, I have two assets Tenable.asm found that I have NOT scanned with Nessus. Without Tenable.asm, I might not have identified these assets.

We can go as far as to automate the scanning of these ghost assets using Tenable’s tag rules. This will allow us to continuously capture these gaps as we expand our external footprint. Each time a new asset is stood up, Tenable.asm will capture it, the script will import it and Tenable will scan it with Nessus using the tag rule.

To be thorough, I used three inclusive “AND” filters to identify my Tenable.asm Ghost assets.

  • Public is equal to Yes
  • Source is NOT equal to Nessus Scan or Nessus Agent
  • Source is equal to ASM

Setting “Public” to yes ensures I won’t capture internal assets. To automate the scan, use the newly created tag as the scan target.

“Wait…there is more…”

What about all of the Websites found by Tenable.asm? How do we provide extra value to the asset owners with this information?

Somebody should be setting up Web application scans for the appropriate assets found by Tenable.asm. Above, you can see that there are a handful of websites found. The first step is ensuring you know what sites are hosted on what IPs in Tenable.io.

Enter Navi!

Navi is a free low-code open source solution that allows us to tag our new Tenable.asm assets with the URLs found by Tenable.asm; saving us 100s of lines of code and hours of coding.

In the screenshot above, 44.228.249.3, has 5 URLs associated with the single IP. Navi will allow us to tag this IP address with each Web application(URL) found in ASM. As the asset owner, I now have increased awareness on what URLs are being hosted on this IP.

To accomplish this we will use Navi’s built in query command to tag assets. Simply loop over each URL found and create a Tag using the Navi DB to grab the correct UUID. The navi command is quite simple, name your tag and query the DB for the current UUID using the IP address:

navi tag — c “ASM URL” — v “<URL found by ASM>” — query “select uuid from assets where ip_address==’<ip found by asm>’;”

The Solution

First we need to get the Authentication bits out of the way:

Notice: I’m only looking at “A Records” as you see described in the params variable.

import requests
import pprint
import time
from tenable.io import TenableIO
from os import system as sys

# ASM URL
api_url = "https://asm-demo.cloud.tenable.com/api/1.0/inventory?offset=0&limit=10000&sortorder=true&inventory=false"

# ASM API KEY
api_key = '<ASM API KEY>'

# ASM headers for Auth
headers = {"accept": "application/json", "Content-Type": "application/json", "Authorization": api_key}

# Limit ASM data to A records
params = [{"column": "bd.record_type",
"type": "starts with",
"value": "A"}]
# T.io API Keys
access_key = '<ACCESS KEY>'
secret_key = '<SECRET KEY>'

# Authenticate using Pytenable
tio = TenableIO(access_key, secret_key, vendor='Casey Reid', product='Import ASM Assets', build='0.0.1')

After we have setup authentication our first step is to export our “A records” from Tenable.asm and import them into Tenable.io as a new source called “ASM”.

def add_asm_assets_as_source():
# Request ASM A records
data = requests.request("POST", api_url, headers=headers, json=params)
hosts = []

# Cycle through each record
for asm_asset in data.json()['assets']:
ip = asm_asset["bd.ip_address"]
# Avoid duplicate records
if ip not in hosts:
hosts.append(ip)

# Create a dictionary for proper import
host_dict = {'ipv4': [ip]}
print("{} added as a ASM Source".format(ip))

# Import the ASM IP address using pytenable asset_import
tio.assets.asset_import("ASM", host_dict)

The code above is very simple. It grabs all of the IP addresses, asm_asset[“bd.ip_address”], from the available “A records” and imports them into Tenable.io. Pytenable makes the import a single line of code:

tio.assets.asset_import("ASM", host_dict)

Note that it takes a few seconds per asset that is imported. In order to get the tag relationships, Navi has to schedule an asset export with Tenable.io. Doing this immediately after import could result in missing the newly added IPs.

I highly suggest putting a 5 min break between adding a source and using the assets in any other workflow to mitigate this risk. For extreme scenarios, provide yourself 20 mins.

The below code utilized navi to tag each IP address.

def navi_tag_assets():
# Authenticate to Tenable.io using Navi
sys("navi keys --a {} --s {}".format(access_key, secret_key))
# Update the navi database
sys("navi update full --days 1")
# Request ASM data
data = requests.request("POST", api_url, headers=headers, json=params)

host_data = {}
# Organize the data into a clean dictionary to reduce API calls.
for asm_asset in data.json()['assets']:
try:
host_data[asm_asset['bd.ip_address']].append(asm_asset['bd.hostname'])
except KeyError:
host_data[asm_asset['bd.ip_address']] = [asm_asset['bd.hostname']]

print("\nTagging the following IPs with the corresponding URLs\n")
pprint.pprint(host_data)
# Cycle through the new dictionary
# For each IP, cycle through each URL and create a new Tag
for hosts in host_data.keys():
for urls in host_data[hosts]:
query = "select uuid from assets where ip_address=='{}';".format(hosts)
# Navi tag command.
sys("navi tag --c \"{}\" --v \"{}\" --query \"{}\"".format("ASM URL", urls, query))

As you see above, I’m taking advantage of the “OS” module in python to run commands as if I were at the command line. To ensure this works, you need to have navi installed. Follow the “Getting started with navi” if you’re not sure what to do.

These last few lines are what kick off the process. Again, it is important to provide at least 5 minute break between processes and in the worst scenario, 20 minutes.

add_asm_assets_as_source()
print("\nPausing for 20 mins to provide time for Tenable.io to update Assets")
time.sleep(1200)
navi_tag_assets()

The full code can be found here:

https://gist.github.com/packetchaos/05d1a415bfb6e07dcf7c574d2f533966

Next Challenge…

What URLs have I scanned with Tenable WAS that ASM has found?

What URLs have I missed?

Stay tuned…

--

--

Casey Reid a.k.a Packet Chaos

I'm a perpetually curious avid learner and athletic hacker/tinker who dabbles in python development, tenable integrations, philosophy, and writing