Exploring the Qualys API with golang


This past week I've been getting to know the Qualys API by writing an integration with Go. Along the way I've found some quirks that are worth mentioning for anyone getting to know the Qualys platform at the API level.

To jump straight to the sample code repository, you can go to the qualys-api-samples repo on GitHub.

References

Toy Project

Let's say you have a list of IP addresses of hosts which have been scanned by Qualys and you want to get a list of all the vulnerabilities that were found on the hosts via the API, how would you accomplish this? I found an approach that works, albeit in a roundabout fashion. There are several different 'APIs' offered on the platform and none of them do everything.

By pouring over the PDF documentation I settled on an approach that leverages the following APIs (in order):

  • Host List: "/api/2.0/fo/asset/host/?action=list&ips="+ipAddresses
  • Search Hosts Assets: "/qps/rest/2.0/search/am/hostasset"
  • Get Host Asset Info: "/qps/rest/2.0/get/am/hostasset/"+assetIdentifier
  • KnowledgeBase: "/api/2.0/fo/knowledge_base/vuln/?action=list&details=All&ids="+qids

Note: Because this is a 'toy project', it does not (as of this writing) include an example for how to paginate results

API Breakdown

Let me preface this by stating that there may be ( Hopefully? Likely is, somewhere?) an easier way of accomplishing this. I'm very new the Qualys API and may have missed something important. With that said, here is what each API call nets me with respect to my toy project:

Host List API

This API can take a number of parameters, including IP addresses. I can pass in a comma-delimited set of IPv4 addresses and get a host listing back:

<HOST_LIST_OUTPUT>
  <RESPONSE>
  <DATETIME>2018-04-26T11:22:56Z</DATETIME>
  <HOST_LIST>
      <HOST>
        <ID>3572461</ID>
        <IP>192.168.0.110</IP>
      </HOST>
      <HOST>
        <ID>2872568</ID>
        <IP>192.168.0.200</IP>
      </HOST>
    </HOST_LIST>
  </RESPONSE>
</HOST_LIST_OUTPUT>

The key data collected during this step is the Host ID (noted as just id in the XML response)

Search Hosts Assets API

With the list of Host IDs from the previous call I can use this API to get the list of Asset IDs back. To do this I send a POST request with an XML payload that includes a comma delimited list of host identifiers I want to search for and returns information about the hosts along with the much needed Asset ID.

Here's what the go code looks like to make the post request. Note that I selected the IN operator which allows me to pass in a comma-separated list of host IDs. Also note that Host ID is called qwebHostId for some reason (this caused me to go down a rabbit hole):

//
// Get a ServiceResponse which includes host details
func searchHostsQps(hostIds string) qualys.ServiceResponse{
    searchResults := qApiCallXml("POST", qauth64, "/qps/rest/2.0/search/am/hostasset", `<ServiceRequest>
  <filters>
    <Criteria field="qwebHostId" operator="IN">`+ hostIds + `</Criteria>
  </filters>
</ServiceRequest>`)

    var response qualys.ServiceResponse
    xml.Unmarshal(searchResults, &response)

    return response
}

The response from this API call looks something like this (truncated for this post):

<responseCode>SUCCESS</responseCode>
  <count>1</count>
  <hasMoreRecords>false</hasMoreRecords>
  <data>
    <HostAsset>
      <id>84021</id>
      <qwebHostId>2872568</qwebHostId>
      <os>Microsoft Windows XP Professional 5.1.2600 Service
Pack 3 Build 2600</os>
      <dnsHostName>XPSP2-32-27-145</dnsHostName>
      <address>192.168.0.200</address>
      <software>
        <list>
          <HostAssetSoftware>
            <name>Security Update for Windows XP (KB2347290)</name>
            <version>1</version>
          </HostAssetSoftware>
        </list>
      </software>
      <vuln>
        <list>
          <HostAssetVuln>
            <qid>118956</qid>
            <hostInstanceVulnId>296963</hostInstanceVulnId>
            <firstFound>2016-02-12T08:42:43Z</firstFound>
            <lastFound>2016-02-13T01:13:04Z</lastFound>
          </HostAssetVuln>
        </list>
      </vuln>

This example response illustrates something else that is a minor 'irk' to me: casing is inconsistent with XML tag names. It's a small thing, just bugs me... It also returns a list of installed software and a listing of vulnerabilities and their Qualys identifier (known as a qid). You will hear about qids a LOT if you get in depth into this platform and if you do you have my sympathy...

Note in the output here that the id field is really an Asset ID. Also note that further down you will see the qwebHostId listed separately (which is what we passed into the API to get the Asset ID.

Get Host Asset Info API

Looking at this now, I don't think I actually needed it in my toy project since the previous API includes the QIDs for the asset. This call can return detailed information about an Asset (once you have the Asset ID)

KnowledgeBase API

This API lets you pass in a list of qualys vulnerability identifiers (qids) and get more detailed information back. You can see the generated struct on github to get a sense of the XML response that gets returned from the API. I lost my raw output sample from this one so can't easily show the xml for the response.

A Note on Generating Go Structs

I wanted to quickly get up to speed and not spend a lot of time manually crafting structures in golang to support interacting with the qualys APIs. To speed this up I leveraged Zek to take the XML sample responses from the documentation and turn them into structs. While the output is not the prettiest, it's solid and gets the job done reliably. With 3 easy steps I had my structs:

  • Copy the XML response from the documentation into a file
  • Cat the file into Zek and capture the generated code:

    cat raw_output.xml | ./zek > golangstructname.go

  • Tweak the output to extract sub-structures that should be their own struct

Final Note (and Sample Code Repo)

Finally, you can see my toy project in my qualys-api-samples GitHub repository.