bosun.org@v0.0.0-20210513094433-e25bc3e69a1f/cmd/scollector/collectors/nexpose.go (about)

     1  package collectors
     2  
     3  import (
     4  	"bytes"
     5  	"crypto/tls"
     6  	"encoding/json"
     7  	"encoding/xml"
     8  	"fmt"
     9  	"io/ioutil"
    10  	"net/http"
    11  	"net/url"
    12  	"strings"
    13  	"time"
    14  
    15  	"bosun.org/cmd/scollector/conf"
    16  	"bosun.org/metadata"
    17  	"bosun.org/opentsdb"
    18  )
    19  
    20  func init() {
    21  	registerInit(func(c *conf.Conf) {
    22  		for _, n := range c.Nexpose {
    23  			collectors = append(collectors, &IntervalCollector{
    24  				F: func() (opentsdb.MultiDataPoint, error) {
    25  					return c_nexpose(n.Username, n.Password, n.Host, n.Insecure, false)
    26  				},
    27  				name: fmt.Sprintf("nexpose-scans-%s", n.Host),
    28  			})
    29  			collectors = append(collectors, &IntervalCollector{
    30  				F: func() (opentsdb.MultiDataPoint, error) {
    31  					return c_nexpose(n.Username, n.Password, n.Host, n.Insecure, true)
    32  				},
    33  				name:     fmt.Sprintf("nexpose-assets-%s", n.Host),
    34  				Interval: time.Minute * 30,
    35  			})
    36  		}
    37  	})
    38  }
    39  
    40  func c_nexpose(username, password, host string, insecure bool, collectAssets bool) (opentsdb.MultiDataPoint, error) {
    41  	const (
    42  		descScanRunning   = "Nexpose scan running."
    43  		descScanRunTime   = "Duration scan has been running, in seconds."
    44  		descRiskScore     = "Risk score for a given site/device."
    45  		descVulnCount     = "Number of known vulnerabilities."
    46  		descExploitCount  = "Number of vulnerabilities exploitable via Metasploit. Subset of vuln_count."
    47  		descMalwareCount  = "Number of vulnerabilities susceptible to malware attacks. Subset of vuln_count."
    48  		descAssetLastScan = "How many seconds ago this asset was last scanned."
    49  	)
    50  	tr := &http.Transport{
    51  		TLSClientConfig: &tls.Config{InsecureSkipVerify: insecure},
    52  	}
    53  	client := &http.Client{Transport: tr}
    54  	url1 := fmt.Sprintf("https://%s/api/1.1/xml", host)
    55  	url2 := fmt.Sprintf("https://%s/api/1.2/xml", host)
    56  
    57  	var md opentsdb.MultiDataPoint
    58  
    59  	c := nexposeConnection{Username: username, Password: password, URLv1: url1, URLv2: url2, Host: host, Client: client}
    60  
    61  	if err := c.login(); err != nil {
    62  		return nil, fmt.Errorf("Login failed: %s", err)
    63  	}
    64  
    65  	siteSummaries, err := c.siteListing()
    66  	if err != nil {
    67  		return nil, err
    68  	}
    69  
    70  	var siteNames = map[int]string{}
    71  
    72  	for _, site := range siteSummaries {
    73  		siteNames[site.ID] = site.Name
    74  		if !collectAssets {
    75  			continue
    76  		}
    77  		Add(&md, "nexpose.site.risk_score", site.RiskScore, opentsdb.TagSet{"site": site.Name}, metadata.Gauge, metadata.Score, descRiskScore)
    78  		tags := opentsdb.TagSet{"site": site.Name}
    79  
    80  		assets, err := c.getSiteAssets(site.ID)
    81  		if err != nil {
    82  			return nil, err
    83  		}
    84  		for _, asset := range assets {
    85  			now := time.Now()
    86  			last_scan := now.Unix() - (asset.LastScanDate / 1000)
    87  			var assetTags opentsdb.TagSet
    88  			// Set the AssetName to the hostname if we have it, otherwise the IP.
    89  			if asset.AssetName != "" {
    90  				assetTags = opentsdb.TagSet{"name": asset.shortName()}.Merge(tags)
    91  			} else {
    92  				assetTags = opentsdb.TagSet{"name": asset.AssetIP}.Merge(tags)
    93  			}
    94  			Add(&md, "nexpose.asset.risk_score", asset.RiskScore, assetTags, metadata.Gauge, metadata.Score, descRiskScore)
    95  			Add(&md, "nexpose.asset.vuln_count", asset.VulnCount, assetTags, metadata.Gauge, metadata.Vulnerabilities, descVulnCount)
    96  			Add(&md, "nexpose.asset.exploit_count", asset.ExploitCount, assetTags, metadata.Gauge, metadata.Vulnerabilities, descExploitCount)
    97  			Add(&md, "nexpose.asset.malware_count", asset.MalwareCount, assetTags, metadata.Gauge, metadata.Vulnerabilities, descMalwareCount)
    98  			Add(&md, "nexpose.asset.last_scan", last_scan, assetTags, metadata.Gauge, metadata.Second, descAssetLastScan)
    99  
   100  			site.VulnCount += asset.VulnCount
   101  			site.ExploitCount += asset.ExploitCount
   102  			site.MalwareCount += asset.MalwareCount
   103  		}
   104  
   105  		Add(&md, "nexpose.site.vuln_count", site.VulnCount, tags, metadata.Gauge, metadata.Vulnerabilities, descVulnCount)
   106  		Add(&md, "nexpose.site.exploit_count", site.ExploitCount, tags, metadata.Gauge, metadata.Vulnerabilities, descExploitCount)
   107  		Add(&md, "nexpose.site.malware_count", site.MalwareCount, tags, metadata.Gauge, metadata.Vulnerabilities, descMalwareCount)
   108  	}
   109  
   110  	assetGroupSummaries, err := c.assetGroupListing()
   111  	if err != nil {
   112  		return nil, err
   113  	}
   114  	for _, group := range assetGroupSummaries {
   115  		tags := opentsdb.TagSet{"asset_group": group.Name}
   116  		Add(&md, "nexpose.asset_group.risk_score", group.RiskScore, tags, metadata.Gauge, metadata.Score, descRiskScore)
   117  	}
   118  
   119  	const timeFmt = "20060102T150405"
   120  	activeScans, err := c.scanActivity()
   121  	if err != nil {
   122  		return nil, err
   123  	}
   124  	for _, scan := range activeScans {
   125  		t, err := time.Parse(timeFmt, scan.StartTime[0:15])
   126  		if err != nil {
   127  			return nil, err
   128  		}
   129  		runtime := int(time.Since(t).Seconds())
   130  		if scan.Status == "running" {
   131  			Add(&md, "nexpose.scan.running", 1, opentsdb.TagSet{"site": siteNames[scan.SiteID]}, metadata.Gauge, metadata.Bool, descScanRunning)
   132  			Add(&md, "nexpose.scan.runtime", runtime, opentsdb.TagSet{"site": siteNames[scan.SiteID]}, metadata.Gauge, metadata.Second, descScanRunTime)
   133  		}
   134  	}
   135  
   136  	return md, nil
   137  }
   138  
   139  type nexposeConnection struct {
   140  	Username  string
   141  	Password  string
   142  	Host      string
   143  	URLv1     string
   144  	URLv2     string
   145  	Client    *http.Client
   146  	SessionID string
   147  	Cookie    *http.Cookie
   148  }
   149  
   150  type apiResponse struct {
   151  	XMLName             xml.Name
   152  	Message             string              `xml:"Message"`
   153  	SessionID           string              `xml:"session-id,attr"`
   154  	SiteSummaries       []siteSummary       `xml:"SiteSummary"`
   155  	ScanSummaries       []scanSummary       `xml:"ScanSummary"`
   156  	AssetGroupSummaries []assetGroupSummary `xml:"AssetGroupSummary"`
   157  	Devices             []device            `xml:"SiteDevices>device"`
   158  }
   159  
   160  type loginRequest struct {
   161  	XMLName  xml.Name `xml:"LoginRequest"`
   162  	UserID   string   `xml:"user-id,attr"`
   163  	Password string   `xml:"password,attr"`
   164  }
   165  
   166  type siteRequest struct {
   167  	XMLName   xml.Name
   168  	SessionID string `xml:"session-id,attr"`
   169  	SiteID    int    `xml:"site-id,attr"`
   170  }
   171  
   172  type simpleRequest struct {
   173  	XMLName   xml.Name
   174  	SessionID string `xml:"session-id,attr"`
   175  }
   176  
   177  func (c *nexposeConnection) jsonRequest(method string, location string, v *url.Values) ([]byte, error) {
   178  	req, _ := http.NewRequest(method, "https://"+c.Host+location, strings.NewReader(v.Encode()))
   179  
   180  	// Some methods require the cookie, some require the header, so we set both.
   181  	req.AddCookie(c.Cookie)
   182  	req.Header.Add("nexposeCCSessionID", c.SessionID)
   183  	if method == "POST" {
   184  		req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
   185  	}
   186  
   187  	resp, err := c.Client.Do(req)
   188  	if err != nil {
   189  		return nil, err
   190  	}
   191  	defer resp.Body.Close()
   192  	body, err := ioutil.ReadAll(resp.Body)
   193  	if err != nil {
   194  		return nil, err
   195  	}
   196  	return body, nil
   197  }
   198  
   199  func (c *nexposeConnection) getJsonTable(location string, params map[string]string) (*jsonTable, error) {
   200  	v := url.Values{}
   201  	v.Set("dir", "ASC")
   202  	v.Set("startIndex", "0")
   203  	v.Set("results", "500")
   204  	for param, value := range params {
   205  		v.Set(param, value)
   206  	}
   207  
   208  	body, err := c.jsonRequest("POST", location, &v)
   209  	if err != nil {
   210  		return nil, err
   211  	}
   212  
   213  	var table jsonTable
   214  	if err = json.Unmarshal(body, &table); err != nil {
   215  		return nil, err
   216  	}
   217  	total := table.TotalRecords
   218  
   219  	for len(table.Records) < total {
   220  		v.Set("startIndex", fmt.Sprintf("%d", len(table.Records)))
   221  		if body, err = c.jsonRequest("POST", location, &v); err != nil {
   222  			return nil, err
   223  		}
   224  		err := table.concat(body)
   225  		if err != nil {
   226  			return nil, err
   227  		}
   228  	}
   229  	return &table, nil
   230  }
   231  
   232  func (c *nexposeConnection) xmlRequest(request interface{}, version int) (*apiResponse, error) {
   233  	buf, err := xml.Marshal(&request)
   234  	if err != nil {
   235  		return nil, err
   236  	}
   237  
   238  	var url string
   239  	if version == 1 {
   240  		url = c.URLv1
   241  	} else if version == 2 {
   242  		url = c.URLv2
   243  	} else {
   244  		return nil, fmt.Errorf("Unsupported API version requested.")
   245  	}
   246  
   247  	resp, err := c.Client.Post(url, "text/xml", bytes.NewBuffer(buf))
   248  	if err != nil {
   249  		return nil, err
   250  	}
   251  	defer resp.Body.Close()
   252  	response, err := ioutil.ReadAll(resp.Body)
   253  	if err != nil {
   254  		return nil, err
   255  	}
   256  
   257  	var data apiResponse
   258  	err = xml.Unmarshal(response, &data)
   259  	if err != nil {
   260  		return nil, err
   261  	}
   262  
   263  	if data.XMLName.Local == "Failure" {
   264  		return nil, fmt.Errorf(data.Message)
   265  	}
   266  
   267  	return &data, nil
   268  }
   269  
   270  func (c *nexposeConnection) login() error {
   271  	login := loginRequest{UserID: c.Username, Password: c.Password}
   272  	resp, err := c.xmlRequest(&login, 1)
   273  	if err != nil {
   274  		return err
   275  	}
   276  
   277  	if resp.SessionID == "" {
   278  		return fmt.Errorf("No SessionID in response.")
   279  	}
   280  	c.SessionID = resp.SessionID
   281  
   282  	// Set the cookie as well, in case we want to use the undocumented JSON API.
   283  	// Note that this cookie must not be sent to XML requests. It is only added
   284  	// for JSON requests, thus it is not added to the client's Jar.
   285  	c.Cookie = &http.Cookie{
   286  		Name:   "nexposeCCSessionID",
   287  		Value:  c.SessionID,
   288  		Path:   "/",
   289  		Domain: c.Host,
   290  		Secure: true}
   291  
   292  	return nil
   293  }
   294  
   295  type siteSummary struct {
   296  	ID           int     `xml:"id,attr"`
   297  	Name         string  `xml:"name,attr"`
   298  	Description  string  `xml:"description,attr"`
   299  	RiskFactor   float32 `xml:"riskfactor,attr"`
   300  	RiskScore    float64 `xml:"riskscore,attr"`
   301  	ExploitCount int
   302  	MalwareCount int
   303  	VulnCount    int
   304  }
   305  
   306  func (c *nexposeConnection) siteListing() ([]siteSummary, error) {
   307  	request := simpleRequest{XMLName: xml.Name{Local: "SiteListingRequest"}, SessionID: c.SessionID}
   308  	resp, err := c.xmlRequest(&request, 1)
   309  	if err != nil {
   310  		return nil, err
   311  	}
   312  
   313  	return resp.SiteSummaries, nil
   314  }
   315  
   316  type assetGroupSummary struct {
   317  	ID        int     `xml:"id,attr"`
   318  	Name      string  `xml:"name,attr"`
   319  	RiskScore float64 `xml:"riskscore,attr"`
   320  }
   321  
   322  func (c *nexposeConnection) assetGroupListing() ([]assetGroupSummary, error) {
   323  	request := simpleRequest{XMLName: xml.Name{Local: "AssetGroupListingRequest"}, SessionID: c.SessionID}
   324  	resp, err := c.xmlRequest(&request, 1)
   325  	if err != nil {
   326  		return nil, err
   327  	}
   328  
   329  	return resp.AssetGroupSummaries, nil
   330  }
   331  
   332  type device struct {
   333  	ID         int     `xml:"id,attr"`
   334  	Address    string  `xml:"address,attr"`
   335  	RiskFactor float32 `xml:"riskfactor,attr"`
   336  	RiskScore  float64 `xml:"riskscore,attr"`
   337  }
   338  
   339  func (c *nexposeConnection) deviceListing(siteID int) ([]device, error) {
   340  	request := siteRequest{XMLName: xml.Name{Local: "SiteDeviceListingRequest"}, SessionID: c.SessionID}
   341  	if siteID > 0 {
   342  		request.SiteID = siteID
   343  	}
   344  	resp, err := c.xmlRequest(&request, 1)
   345  	if err != nil {
   346  		return nil, err
   347  	}
   348  
   349  	return resp.Devices, nil
   350  }
   351  
   352  type scanSummary struct {
   353  	ID        int    `xml:"scan-id,attr"`
   354  	SiteID    int    `xml:"site-id,attr"`
   355  	EngineID  int    `xml:"engine-id,attr"`
   356  	Name      string `xml:"name,attr"`
   357  	StartTime string `xml:"startTime,attr"` // %Y%M%dT%H%M%s, with 3-digit millis added to the end
   358  	EndTime   string `xml:"endTime,attr"`   // same as above
   359  	Status    string `xml:"status,attr"`
   360  }
   361  
   362  func (c *nexposeConnection) scanHistory(siteID int) ([]scanSummary, error) {
   363  	request := siteRequest{SiteID: siteID}
   364  	request.XMLName = xml.Name{Local: "SiteScanHistoryRequest"}
   365  	request.SessionID = c.SessionID
   366  	resp, err := c.xmlRequest(&request, 1)
   367  	if err != nil {
   368  		return nil, err
   369  	}
   370  
   371  	return resp.ScanSummaries, nil
   372  }
   373  
   374  func (c *nexposeConnection) scanActivity() ([]scanSummary, error) {
   375  	request := simpleRequest{}
   376  	request.XMLName = xml.Name{Local: "ScanActivityRequest"}
   377  	request.SessionID = c.SessionID
   378  	resp, err := c.xmlRequest(&request, 1)
   379  	if err != nil {
   380  		return nil, err
   381  	}
   382  
   383  	return resp.ScanSummaries, nil
   384  }
   385  
   386  func (c *nexposeConnection) getSiteAssets(siteID int) ([]asset, error) {
   387  	params := make(map[string]string)
   388  
   389  	params["sort"] = "assetName"
   390  	params["table-id"] = "site-assets"
   391  	params["siteID"] = fmt.Sprintf("%d", siteID)
   392  
   393  	var assets []asset
   394  	table, err := c.getJsonTable("/data/asset/site", params)
   395  	if err != nil {
   396  		return nil, err
   397  	}
   398  
   399  	for _, record := range table.Records {
   400  		var newAsset asset
   401  		err = json.Unmarshal(record, &newAsset)
   402  		if err != nil {
   403  			return nil, err
   404  		}
   405  		assets = append(assets, newAsset)
   406  	}
   407  
   408  	return assets, nil
   409  }
   410  
   411  type asset struct {
   412  	AssetID      int
   413  	AssetIP      string
   414  	AssetName    string
   415  	AssetOSName  string
   416  	MacAddr      string
   417  	RiskScore    float64
   418  	ExploitCount int
   419  	MalwareCount int
   420  	VulnCount    int
   421  	Assessed     bool
   422  	LastScanDate int64 // 13-digit epoch with fractional seconds
   423  	HostType     string
   424  }
   425  
   426  func (a *asset) shortName() string {
   427  	if a.AssetName == "" {
   428  		return ""
   429  	}
   430  
   431  	return strings.Split(a.AssetName, ".")[0]
   432  }
   433  
   434  type jsonTable struct {
   435  	RowsPerPage  int
   436  	RecordOffset int
   437  	TotalRecords int
   438  	Records      []json.RawMessage
   439  }
   440  
   441  func (t *jsonTable) concat(jsonData []byte) error {
   442  	var newTable jsonTable
   443  	if err := json.Unmarshal(jsonData, &newTable); err != nil {
   444  		return err
   445  	}
   446  	t.Records = append(t.Records, newTable.Records...)
   447  
   448  	return nil
   449  }