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 }