├── .github └── workflows │ └── go.yml ├── .gitignore ├── LICENSE ├── README.md ├── cli └── nessie.go ├── go.mod ├── go.sum ├── nessie.go ├── nessie_test.go ├── requests.go ├── resources.go └── responses.go /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | on: [push, pull_request] 3 | jobs: 4 | 5 | build: 6 | name: Build and Test 7 | runs-on: ubuntu-latest 8 | steps: 9 | 10 | - name: Set up Go 11 | uses: actions/setup-go@v1 12 | with: 13 | go-version: 1.15 14 | id: go 15 | 16 | - name: Check out code into the Go module directory 17 | uses: actions/checkout@v1 18 | 19 | - name: Get dependencies 20 | run: | 21 | go get -v -t -d ./... 22 | if [ -f Gopkg.toml ]; then 23 | curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh 24 | dep ensure 25 | fi 26 | 27 | - name: Build 28 | run: go build -v . 29 | 30 | - name: Test 31 | run: go test -v ./... 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Vim 7 | *.swp 8 | 9 | # Folders 10 | _obj 11 | _test 12 | 13 | # Architecture specific extensions/prefixes 14 | *.[568vq] 15 | [568vq].out 16 | 17 | *.cgo1.go 18 | *.cgo2.c 19 | _cgo_defun.c 20 | _cgo_gotypes.go 21 | _cgo_export.* 22 | 23 | _testmain.go 24 | 25 | *.exe 26 | *.test 27 | *.prof 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 attwad 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/attwad/nessie.svg?branch=master)](https://travis-ci.org/attwad/nessie) 2 | [![GoDoc](https://godoc.org/github.com/attwad/nessie?status.png)](https://godoc.org/github.com/attwad/nessie) 3 | 4 | Nessie 5 | ====== 6 | 7 | Tenable Nessus 6 API client in Go. 8 | 9 | Usage 10 | ----- 11 | 12 | Have a look at [the client example](https://github.com/attwad/nessie/blob/master/cli/nessie.go) for how to start a scan, wait until it finishes and exports the results to a CSV file. 13 | 14 | Status 15 | ------ 16 | 17 | Here are the resources accessible via the official API and their current implementation status in this client: 18 | 19 | - AgentGroups 20 | - Add agent 21 | - Add agents 22 | - Configure 23 | - Create 24 | - Delete group 25 | - Delete groups 26 | - Delete agent 27 | - Delete agents 28 | - List groups ✓ 29 | - Editor 30 | - Details 31 | - Edit 32 | - List policy templates ✓ 33 | - List scan templates ✓ 34 | - Plugin description 35 | - File 36 | - Upload ✓ 37 | - Folders ✓ 38 | - Create ✓ 39 | - Delete ✓ 40 | - Edit ✓ 41 | - List ✓ 42 | - Groups 43 | - Add user 44 | - Create ✓ 45 | - Delete 46 | - Delete user 47 | - Edit 48 | - List ✓ 49 | - List users 50 | - Permissions 51 | - Change 52 | - List ✓ 53 | - Plugins ✓ 54 | - Families ✓ 55 | - Family details ✓ 56 | - Plugin details ✓ 57 | - Plugin rules 58 | - Create 59 | - Delete 60 | - Edit 61 | - List 62 | - Policies 63 | - Configure ✓ 64 | - Copy 65 | - Create ✓ 66 | - Delete ✓ 67 | - Details 68 | - Import 69 | - Export 70 | - List ✓ 71 | - Scanners ✓ 72 | - List ✓ 73 | - Scans 74 | - Configure ✓ 75 | - Create ✓ 76 | - Delete ✓ 77 | - Delete history 78 | - Details ✓ 79 | - Download ✓ 80 | - Export ✓ 81 | - Export status ✓ 82 | - Host details 83 | - Import 84 | - Launch ✓ 85 | - List ✓ 86 | - Pause ✓ 87 | - Plugin output 88 | - Read status 89 | - Resume ✓ 90 | - Stop ✓ 91 | - Timezones ✓ 92 | - Server ✓ 93 | - Properties ✓ 94 | - Status ✓ 95 | - Sessions 96 | - Create ✓ 97 | - Destroy ✓ 98 | - Edit 99 | - Get ✓ 100 | - Password 101 | - Users ✓ 102 | - Create ✓ 103 | - Delete ✓ 104 | - Edit ✓ 105 | - List ✓ 106 | - Password ✓ 107 | 108 | Some methods are not part of the API but are implemented by this client to make life easier: 109 | 110 | - Get all plugin details 111 | -------------------------------------------------------------------------------- /cli/nessie.go: -------------------------------------------------------------------------------- 1 | // Package main implements a test client that starts a scan, wait until it finishes and exports its results to a csv file. 2 | package main 3 | 4 | import ( 5 | "flag" 6 | "github.com/attwad/nessie" 7 | "io/ioutil" 8 | "log" 9 | "strings" 10 | "time" 11 | ) 12 | 13 | var apiURL, username, password, fingerprints string 14 | 15 | func init() { 16 | flag.StringVar(&apiURL, "api_url", "", "") 17 | flag.StringVar(&username, "username", "", "Username to login with, in production read that from a file, do not set from the command line or it will end up in your history.") 18 | flag.StringVar(&password, "password", "", "Password that matches the provided username, in production read that from a file, do not set from the command line or it will end up in your history.") 19 | flag.StringVar(&fingerprints, "fingerprints", "", "Comma-separated list of SPKI Fingerprints for the Nessus server using SHA-256 encoded in base64.") 20 | flag.Parse() 21 | } 22 | 23 | func main() { 24 | var err error 25 | var nessus nessie.Nessus 26 | if len(fingerprints) > 0 { 27 | nessus, err = nessie.NewFingerprintedNessus(apiURL, strings.Split(fingerprints, ",")) 28 | } else { 29 | nessus, err = nessie.NewInsecureNessus(apiURL) 30 | } 31 | if err != nil { 32 | panic(err) 33 | } 34 | 35 | if err := nessus.Login(username, password); err != nil { 36 | log.Println(err) 37 | return 38 | } 39 | log.Println("Logged-in") 40 | defer nessus.Logout() 41 | 42 | var scanID int64 = 13 43 | // We only care about the last scan, so no use for the scan UUID here. 44 | if _, err = nessus.StartScan(scanID); err != nil { 45 | panic(err) 46 | } 47 | for { 48 | details, err := nessus.ScanDetails(scanID) 49 | if err != nil { 50 | panic(err) 51 | } 52 | if strings.ToLower(details.Info.Status) == "completed" { 53 | log.Println("Scan completed") 54 | break 55 | } 56 | log.Println("Scan is", details.Info.Status) 57 | time.Sleep(5 * time.Second) 58 | } 59 | 60 | exportID, err := nessus.ExportScan(scanID, nessie.ExportCSV) 61 | if err != nil { 62 | panic(err) 63 | } 64 | for { 65 | if finished, err := nessus.ExportFinished(scanID, exportID); err != nil { 66 | panic(err) 67 | } else if finished { 68 | log.Println("Scan export finished") 69 | break 70 | } 71 | log.Println("Scan export ongoing...") 72 | time.Sleep(5 * time.Second) 73 | } 74 | csv, err := nessus.DownloadExport(scanID, exportID) 75 | if err != nil { 76 | panic(err) 77 | } 78 | if err := ioutil.WriteFile("report.csv", csv, 0600); err != nil { 79 | panic(err) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/attwad/nessie 2 | 3 | go 1.15 4 | 5 | require github.com/gorilla/schema v1.2.0 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/gorilla/schema v1.2.0 h1:YufUaxZYCKGFuAq3c96BOhjgd5nmXiOY9NGzF247Tsc= 2 | github.com/gorilla/schema v1.2.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU= 3 | -------------------------------------------------------------------------------- /nessie.go: -------------------------------------------------------------------------------- 1 | // Package nessie implements a client for the Tenable Nessus 6 API. 2 | package nessie 3 | 4 | import ( 5 | "bytes" 6 | "crypto/sha256" 7 | "crypto/tls" 8 | "crypto/x509" 9 | "encoding/base64" 10 | "encoding/json" 11 | "fmt" 12 | "io" 13 | "io/ioutil" 14 | "log" 15 | "mime/multipart" 16 | "net" 17 | "net/http" 18 | "net/http/httputil" 19 | "net/url" 20 | "os" 21 | "path/filepath" 22 | "strings" 23 | "sync" 24 | ) 25 | 26 | // Nessus exposes the resources offered via the Tenable Nessus RESTful API. 27 | type Nessus interface { 28 | SetVerbose(bool) 29 | AuthCookie() string 30 | Request(method string, resource string, js interface{}, wantStatus []int) (resp *http.Response, err error) 31 | Login(username, password string) error 32 | Logout() error 33 | Session() (Session, error) 34 | 35 | ServerProperties() (*ServerProperties, error) 36 | ServerStatus() (*ServerStatus, error) 37 | 38 | CreateUser(username, password, userType, permissions, name, email string) (*User, error) 39 | ListUsers() ([]User, error) 40 | DeleteUser(userID int) error 41 | SetUserPassword(userID int, password string) error 42 | EditUser(userID int, permissions, name, email string) (*User, error) 43 | 44 | PluginFamilies() ([]PluginFamily, error) 45 | FamilyDetails(ID int64) (*FamilyDetails, error) 46 | PluginDetails(ID int64) (*PluginDetails, error) 47 | AllPlugins() (chan PluginDetails, error) 48 | 49 | Scanners() ([]Scanner, error) 50 | Policies() ([]Policy, error) 51 | CreatePolicy(policySettings CreatePolicyRequest) (CreatePolicyResp, error) 52 | ConfigurePolicy(id int64, policySettings CreatePolicyRequest) error 53 | DeletePolicy(id int64) error 54 | 55 | Upload(filePath string) error 56 | AgentGroups() ([]AgentGroup, error) 57 | 58 | NewScan(editorTmplUUID, settingsName string, outputFolderID, policyID, scannerID int64, launch string, targets []string) (*Scan, error) 59 | CreateScan(newScanRequest NewScanRequest) (*Scan, error) 60 | Scans() (*ListScansResponse, error) 61 | ScanTemplates() ([]Template, error) 62 | PolicyTemplates() ([]Template, error) 63 | StartScan(scanID int64) (string, error) 64 | PauseScan(scanID int64) error 65 | ResumeScan(scanID int64) error 66 | StopScan(scanID int64) error 67 | DeleteScan(scanID int64) error 68 | ScanDetails(scanID int64) (*ScanDetailsResp, error) 69 | ConfigureScan(scanID int64, scanSetting NewScanRequest) (*Scan, error) 70 | 71 | Timezones() ([]TimeZone, error) 72 | 73 | Folders() ([]Folder, error) 74 | CreateFolder(name string) error 75 | EditFolder(folderID int64, newName string) error 76 | DeleteFolder(folderID int64) error 77 | 78 | ExportScan(scanID int64, format string) (int64, error) 79 | ExportFinished(scanID, exportID int64) (bool, error) 80 | DownloadExport(scanID, exportID int64) ([]byte, error) 81 | 82 | Permissions(objectType string, objectID int64) ([]Permission, error) 83 | } 84 | 85 | type nessusImpl struct { 86 | // client is the HTTP client to use to issue requests to nessus. 87 | client *http.Client 88 | // authCookie is the login token returned by nessus upon successful login. 89 | authCookie string 90 | apiURL string 91 | // verbose will log requests and responses amongst other helpful debugging things. 92 | verbose bool 93 | } 94 | 95 | // NewNessus will return a new Nessus instance, if caCertPath is empty, the host certificate roots will be used to check for the validity of the nessus server API certificate. 96 | func NewNessus(apiURL, caCertPath string) (Nessus, error) { 97 | return newNessus(apiURL, caCertPath, false, false, nil) 98 | } 99 | 100 | // NewInsecureNessus will return a nessus instance which does not check for the api certificate validity, do not use in production environment. 101 | func NewInsecureNessus(apiURL string) (Nessus, error) { 102 | return newNessus(apiURL, "", true, false, nil) 103 | } 104 | 105 | // NewFingerprintedNessus will return a nessus instance which verifies the api server's certificate by its SHA256 fingerprint (on the RawSubjectPublicKeyInfo and base64 encoded) against a whitelist of good certFingerprints. Fingerprint verification will enable InsecureSkipVerify. 106 | func NewFingerprintedNessus(apiURL string, certFingerprints []string) (Nessus, error) { 107 | return newNessus(apiURL, "", true, true, certFingerprints) 108 | } 109 | 110 | func newNessus(apiURL, caCertPath string, ignoreSSLCertsErrors bool, verifyCertFingerprint bool, certFingerprints []string) (Nessus, error) { 111 | var ( 112 | dialTLS func(network, addr string) (net.Conn, error) 113 | roots *x509.CertPool 114 | ) 115 | config := &tls.Config{ 116 | InsecureSkipVerify: ignoreSSLCertsErrors, 117 | RootCAs: roots, 118 | } 119 | if len(caCertPath) != 0 { 120 | roots = x509.NewCertPool() 121 | rootPEM, err := ioutil.ReadFile(caCertPath) 122 | if err != nil { 123 | return nil, err 124 | } 125 | ok := roots.AppendCertsFromPEM(rootPEM) 126 | if !ok { 127 | return nil, fmt.Errorf("could not append certs from PEM %s", caCertPath) 128 | } 129 | } else if verifyCertFingerprint == true { 130 | if len(certFingerprints) == 0 { 131 | return nil, fmt.Errorf("fingerprint verification enabled, fingerprint must not be empty") 132 | } 133 | dialTLS = createDialTLSFuncToVerifyFingerprint(certFingerprints, config) 134 | } 135 | return &nessusImpl{ 136 | apiURL: apiURL, 137 | client: &http.Client{ 138 | Transport: &http.Transport{ 139 | TLSClientConfig: config, 140 | DialTLS: dialTLS, 141 | }, 142 | }, 143 | }, nil 144 | } 145 | 146 | func sha256Fingerprint(data []byte) string { 147 | h := sha256.New() 148 | h.Write(data) 149 | return base64.StdEncoding.EncodeToString(h.Sum(nil)) 150 | } 151 | 152 | func createDialTLSFuncToVerifyFingerprint(certFingerprints []string, config *tls.Config) func(network, addr string) (net.Conn, error) { 153 | return func(network, addr string) (net.Conn, error) { 154 | conn, err := tls.Dial(network, addr, config) 155 | if err != nil { 156 | return nil, err 157 | } 158 | state := conn.ConnectionState() 159 | // Only check the first cert in the chain. The TLS server must send its cert first (RFC5246), and this first cert is authenticated with a check for proof of private key possesion. 160 | peerFingerprint := sha256Fingerprint(state.PeerCertificates[0].RawSubjectPublicKeyInfo) 161 | for _, fingerprint := range certFingerprints { 162 | if peerFingerprint == fingerprint { 163 | return conn, nil 164 | } 165 | } 166 | conn.Close() 167 | return nil, fmt.Errorf("no server certificate with fingerprints %v was found", certFingerprints) 168 | } 169 | } 170 | 171 | func (n *nessusImpl) SetVerbose(verbosity bool) { 172 | n.verbose = verbosity 173 | } 174 | 175 | func (n *nessusImpl) AuthCookie() string { 176 | return n.authCookie 177 | } 178 | 179 | // Request make a request to Nessus 180 | func (n *nessusImpl) Request(method string, resource string, js interface{}, wantStatus []int) (resp *http.Response, err error) { 181 | u, err := url.ParseRequestURI(n.apiURL) 182 | if err != nil { 183 | return nil, err 184 | } 185 | // Note: resource doesn't support '?' due to https://golang.org/pkg/net/url/#PathEscape 186 | // In order to not break this API, we'll split the resource by '?' here 187 | // when resource = "/agents?filter.0.filter=status&filter.0.quality=neq&filter.0.value=online" 188 | // split resource to: 189 | // 1. Path = "/agents" 190 | // 2. RawQuery = "filter.0.filter=status&filter.0.quality=neq&filter.0.value=online" 191 | if idx := strings.IndexByte(resource, '?'); -1 != idx { 192 | u.Path = resource[:idx] 193 | u.RawQuery = resource[idx+1:] 194 | } else { 195 | u.Path = resource 196 | } 197 | urlStr := fmt.Sprintf("%v", u) 198 | 199 | jb, err := json.Marshal(js) 200 | if err != nil { 201 | return nil, err 202 | } 203 | req, err := http.NewRequest(method, urlStr, bytes.NewBufferString(string(jb))) 204 | if err != nil { 205 | return nil, err 206 | } 207 | req.Header.Add("Content-Type", "application/json") 208 | req.Header.Add("Accept", "application/json") 209 | if n.authCookie != "" { 210 | req.Header.Add("X-Cookie", fmt.Sprintf("token=%s", n.authCookie)) 211 | } 212 | 213 | if n.verbose { 214 | db, err := httputil.DumpRequest(req, true) 215 | if err != nil { 216 | return nil, err 217 | } 218 | log.Println("sending data:", string(db)) 219 | } 220 | resp, err = n.client.Do(req) 221 | if err != nil { 222 | return nil, err 223 | } 224 | if n.verbose { 225 | if body, err := httputil.DumpResponse(resp, true); err == nil { 226 | log.Println(string(body)) 227 | } 228 | } 229 | var statusFound bool 230 | for _, status := range wantStatus { 231 | if resp.StatusCode == status { 232 | statusFound = true 233 | break 234 | } 235 | } 236 | if !statusFound { 237 | body, err := ioutil.ReadAll(resp.Body) 238 | if err != nil { 239 | return nil, err 240 | } 241 | return nil, fmt.Errorf("Unexpected status code, got %d wanted %v (%s)", resp.StatusCode, wantStatus, body) 242 | } 243 | return resp, nil 244 | } 245 | 246 | // Login will log into nessus with the username and passwords given from the command line flags. 247 | func (n *nessusImpl) Login(username, password string) error { 248 | if n.verbose { 249 | log.Printf("Login into %s\n", n.apiURL) 250 | } 251 | data := loginRequest{ 252 | Username: username, 253 | Password: password, 254 | } 255 | 256 | resp, err := n.Request("POST", "/session", data, []int{http.StatusOK}) 257 | if err != nil { 258 | return err 259 | } 260 | defer resp.Body.Close() 261 | reply := &loginResp{} 262 | if err = json.NewDecoder(resp.Body).Decode(&reply); err != nil { 263 | return err 264 | } 265 | n.authCookie = reply.Token 266 | return nil 267 | } 268 | 269 | // Logout will invalidate the current session token. 270 | func (n *nessusImpl) Logout() error { 271 | if n.authCookie == "" { 272 | log.Println("Not logged in, nothing to do to logout...") 273 | return nil 274 | } 275 | if n.verbose { 276 | log.Println("Logout...") 277 | } 278 | 279 | if _, err := n.Request("DELETE", "/session", nil, []int{http.StatusOK}); err != nil { 280 | return err 281 | } 282 | n.authCookie = "" 283 | return nil 284 | } 285 | 286 | // Session will return the details for the current session. 287 | func (n *nessusImpl) Session() (Session, error) { 288 | if n.verbose { 289 | log.Printf("Getting details for current session...") 290 | } 291 | 292 | resp, err := n.Request("GET", "/session", nil, []int{http.StatusOK}) 293 | if err != nil { 294 | return Session{}, err 295 | } 296 | defer resp.Body.Close() 297 | var reply Session 298 | if err = json.NewDecoder(resp.Body).Decode(&reply); err != nil { 299 | return Session{}, err 300 | } 301 | return reply, nil 302 | } 303 | 304 | // ServerProperties will return the current state of the nessus instance. 305 | func (n *nessusImpl) ServerProperties() (*ServerProperties, error) { 306 | if n.verbose { 307 | log.Println("Server properties...") 308 | } 309 | 310 | resp, err := n.Request("GET", "/server/properties", nil, []int{http.StatusOK}) 311 | if err != nil { 312 | return nil, err 313 | } 314 | defer resp.Body.Close() 315 | reply := &ServerProperties{} 316 | if err = json.NewDecoder(resp.Body).Decode(&reply); err != nil { 317 | return nil, err 318 | } 319 | return reply, nil 320 | } 321 | 322 | // ServerStatus will return the current status of the nessus instance. 323 | func (n *nessusImpl) ServerStatus() (*ServerStatus, error) { 324 | if n.verbose { 325 | log.Println("Server status...") 326 | } 327 | 328 | resp, err := n.Request("GET", "/server/status", nil, []int{http.StatusOK, http.StatusServiceUnavailable}) 329 | if err != nil { 330 | return nil, err 331 | } 332 | defer resp.Body.Close() 333 | reply := &ServerStatus{} 334 | if err = json.NewDecoder(resp.Body).Decode(&reply); err != nil { 335 | return nil, err 336 | } 337 | if resp.StatusCode == http.StatusServiceUnavailable { 338 | reply.MustDestroySession = true 339 | } 340 | return reply, nil 341 | } 342 | 343 | const ( 344 | UserTypeLocal = "local" 345 | UserTypeLDAP = "ldap" 346 | 347 | Permissions0 = "0" 348 | Permissions16 = "16" 349 | Permissions32 = "32" 350 | Permissions64 = "64" 351 | Permissions128 = "128" 352 | ) 353 | 354 | // CreateUser will register a new user with the nessus instance. 355 | // Name and email can be empty. 356 | func (n *nessusImpl) CreateUser(username, password, userType, permissions, name, email string) (*User, error) { 357 | if n.verbose { 358 | log.Println("Creating new user...") 359 | } 360 | data := createUserRequest{ 361 | Username: username, 362 | Password: password, 363 | Permissions: permissions, 364 | Type: userType, 365 | } 366 | if name != "" { 367 | data.Name = name 368 | } 369 | if email != "" { 370 | data.Email = email 371 | } 372 | 373 | resp, err := n.Request("POST", "/users", data, []int{http.StatusOK}) 374 | if err != nil { 375 | return nil, err 376 | } 377 | defer resp.Body.Close() 378 | reply := &User{} 379 | if err = json.NewDecoder(resp.Body).Decode(&reply); err != nil { 380 | return nil, err 381 | } 382 | return reply, nil 383 | } 384 | 385 | // ListUsers will return the list of users on this nessus instance. 386 | func (n *nessusImpl) ListUsers() ([]User, error) { 387 | if n.verbose { 388 | log.Println("Listing users...") 389 | } 390 | 391 | resp, err := n.Request("GET", "/users", nil, []int{http.StatusOK}) 392 | if err != nil { 393 | return nil, err 394 | } 395 | defer resp.Body.Close() 396 | reply := &listUsersResp{} 397 | if err = json.NewDecoder(resp.Body).Decode(&reply); err != nil { 398 | return nil, err 399 | } 400 | return reply.Users, nil 401 | } 402 | 403 | // DeleteUser will remove a user from this nessus instance. 404 | func (n *nessusImpl) DeleteUser(userID int) error { 405 | if n.verbose { 406 | log.Println("Deleting user...") 407 | } 408 | 409 | _, err := n.Request("DELETE", fmt.Sprintf("/users/%d", userID), nil, []int{http.StatusOK}) 410 | return err 411 | } 412 | 413 | // SetUserPassword will change the password for the given user. 414 | func (n *nessusImpl) SetUserPassword(userID int, password string) error { 415 | if n.verbose { 416 | log.Println("Changing password of user...") 417 | } 418 | data := setUserPasswordRequest{ 419 | Password: password, 420 | } 421 | 422 | _, err := n.Request("PUT", fmt.Sprintf("/users/%d/chpasswd", userID), data, []int{http.StatusOK}) 423 | return err 424 | } 425 | 426 | // EditUser will edit certain information about a user. 427 | // Any non empty parameter will be set. 428 | func (n *nessusImpl) EditUser(userID int, permissions, name, email string) (*User, error) { 429 | if n.verbose { 430 | log.Println("Editing user...") 431 | } 432 | data := editUserRequest{} 433 | 434 | if permissions != "" { 435 | data.Permissions = permissions 436 | } 437 | if name != "" { 438 | data.Name = name 439 | } 440 | if email != "" { 441 | data.Email = email 442 | } 443 | 444 | resp, err := n.Request("PUT", fmt.Sprintf("/users/%d", userID), data, []int{http.StatusOK}) 445 | if err != nil { 446 | return nil, err 447 | } 448 | defer resp.Body.Close() 449 | reply := &User{} 450 | if err = json.NewDecoder(resp.Body).Decode(&reply); err != nil { 451 | return nil, err 452 | } 453 | return reply, nil 454 | } 455 | 456 | func (n *nessusImpl) PluginFamilies() ([]PluginFamily, error) { 457 | if n.verbose { 458 | log.Println("Getting list of plugin families...") 459 | } 460 | 461 | resp, err := n.Request("GET", "/plugins/families", nil, []int{http.StatusOK}) 462 | if err != nil { 463 | return nil, err 464 | } 465 | defer resp.Body.Close() 466 | var reply PluginFamilies 467 | if err = json.NewDecoder(resp.Body).Decode(&reply); err != nil { 468 | return nil, err 469 | } 470 | return reply.Families, nil 471 | } 472 | 473 | func (n *nessusImpl) FamilyDetails(ID int64) (*FamilyDetails, error) { 474 | if n.verbose { 475 | log.Println("Getting details of family...") 476 | } 477 | 478 | resp, err := n.Request("GET", fmt.Sprintf("/plugins/families/%d", ID), nil, []int{http.StatusOK}) 479 | if err != nil { 480 | return nil, err 481 | } 482 | defer resp.Body.Close() 483 | reply := &FamilyDetails{} 484 | if err = json.NewDecoder(resp.Body).Decode(&reply); err != nil { 485 | return nil, err 486 | } 487 | return reply, nil 488 | } 489 | 490 | func (n *nessusImpl) PluginDetails(ID int64) (*PluginDetails, error) { 491 | if n.verbose { 492 | log.Println("Getting details plugin...") 493 | } 494 | 495 | resp, err := n.Request("GET", fmt.Sprintf("/plugins/plugin/%d", ID), nil, []int{http.StatusOK}) 496 | if err != nil { 497 | return nil, err 498 | } 499 | defer resp.Body.Close() 500 | reply := &PluginDetails{} 501 | if err = json.NewDecoder(resp.Body).Decode(&reply); err != nil { 502 | return nil, err 503 | } 504 | return reply, nil 505 | } 506 | 507 | func (n *nessusImpl) Scanners() ([]Scanner, error) { 508 | if n.verbose { 509 | log.Println("Getting scanners list...") 510 | } 511 | 512 | resp, err := n.Request("GET", "/scanners", nil, []int{http.StatusOK}) 513 | if err != nil { 514 | return nil, err 515 | } 516 | defer resp.Body.Close() 517 | var reply struct { 518 | Scanners []Scanner `json:"scanners"` 519 | } 520 | if err = json.NewDecoder(resp.Body).Decode(&reply); err != nil { 521 | return nil, err 522 | } 523 | return reply.Scanners, nil 524 | } 525 | 526 | // AllPlugin wil hammer nessus asking for details of every plugins available and feeding them in 527 | // the returned channel. 528 | // Getting all the plugins is slow (usually takes a few minutes on a decent machine). 529 | func (n *nessusImpl) AllPlugins() (chan PluginDetails, error) { 530 | plugChan := make(chan PluginDetails, 20) 531 | 532 | families, err := n.PluginFamilies() 533 | if err != nil { 534 | return nil, err 535 | } 536 | idChan := make(chan int64, 20) 537 | var wgf sync.WaitGroup 538 | var wgp sync.WaitGroup 539 | // Launch a goroutine per family to get all the plugins of those families. 540 | for _, family := range families { 541 | wgf.Add(1) 542 | go func(famID int64) { 543 | defer wgf.Done() 544 | famDetails, err := n.FamilyDetails(famID) 545 | if err != nil { 546 | return 547 | } 548 | for _, plugin := range famDetails.Plugins { 549 | wgp.Add(1) 550 | idChan <- plugin.ID 551 | } 552 | }(family.ID) 553 | } 554 | // Launch our workers getting individual plugin details. 555 | for i := 0; i < 10; i++ { 556 | go func() { 557 | for id := range idChan { 558 | plugin, err := n.PluginDetails(id) 559 | if err != nil { 560 | wgp.Done() 561 | continue 562 | } 563 | plugChan <- *plugin 564 | wgp.Done() 565 | } 566 | }() 567 | } 568 | 569 | go func() { 570 | wgf.Wait() 571 | // Once we finished adding all the plugin IDs, we can close the channel. 572 | close(idChan) 573 | // Once all the plugins have been returned, we can close the plugin channel 574 | // to let the receiver know. 575 | wgp.Wait() 576 | close(plugChan) 577 | }() 578 | 579 | return plugChan, nil 580 | } 581 | 582 | func (n *nessusImpl) Policies() ([]Policy, error) { 583 | if n.verbose { 584 | log.Println("Getting policies list...") 585 | } 586 | 587 | resp, err := n.Request("GET", "/policies", nil, []int{http.StatusOK}) 588 | if err != nil { 589 | return nil, err 590 | } 591 | defer resp.Body.Close() 592 | var reply listPoliciesResp 593 | if err = json.NewDecoder(resp.Body).Decode(&reply); err != nil { 594 | return nil, err 595 | } 596 | return reply.Policies, nil 597 | } 598 | 599 | const ( 600 | LaunchOnDemand = "ON_DEMAND" 601 | LaunchDaily = "DAILY" 602 | LaunchWeekly = "WEEKLY" 603 | LaunchMonthly = "MONTHLY" 604 | LaunchYearly = "YEARLY" 605 | ) 606 | 607 | func (n *nessusImpl) NewScan( 608 | editorTmplUUID string, 609 | settingsName string, 610 | outputFolderID int64, 611 | policyID int64, 612 | scannerID int64, 613 | launch string, 614 | targets []string) (*Scan, error) { 615 | data := NewScanRequest{ 616 | UUID: editorTmplUUID, 617 | Settings: ScanSettingsRequest{ 618 | Name: settingsName, 619 | Description: "Some description", 620 | FolderID: outputFolderID, 621 | ScannerID: scannerID, 622 | PolicyID: policyID, 623 | Launch: launch, 624 | TextTargets: strings.Join(targets, ", "), 625 | }, 626 | } 627 | 628 | return n.CreateScan(data) 629 | } 630 | 631 | func (n *nessusImpl) CreateScan(newScanRequest NewScanRequest) (*Scan, error) { 632 | if n.verbose { 633 | log.Println("Creating a new scan...") 634 | } 635 | 636 | resp, err := n.Request("POST", "/scans", newScanRequest, []int{http.StatusOK}) 637 | if err != nil { 638 | return nil, err 639 | } 640 | 641 | defer resp.Body.Close() 642 | reply := struct { 643 | Scan Scan `json:"scan"` 644 | }{} 645 | 646 | if err = json.NewDecoder(resp.Body).Decode(&reply); err != nil { 647 | return nil, err 648 | } 649 | return &reply.Scan, nil 650 | } 651 | 652 | func (n *nessusImpl) Scans() (*ListScansResponse, error) { 653 | if n.verbose { 654 | log.Println("Getting scans list...") 655 | } 656 | 657 | resp, err := n.Request("GET", "/scans", nil, []int{http.StatusOK}) 658 | if err != nil { 659 | return nil, err 660 | } 661 | defer resp.Body.Close() 662 | reply := &ListScansResponse{} 663 | if err = json.NewDecoder(resp.Body).Decode(&reply); err != nil { 664 | return nil, err 665 | } 666 | return reply, nil 667 | } 668 | 669 | func (n *nessusImpl) ScanTemplates() ([]Template, error) { 670 | if n.verbose { 671 | log.Println("Getting scans templates...") 672 | } 673 | 674 | resp, err := n.Request("GET", "/editor/scan/templates", nil, []int{http.StatusOK}) 675 | if err != nil { 676 | return nil, err 677 | } 678 | defer resp.Body.Close() 679 | reply := &listTemplatesResp{} 680 | if err = json.NewDecoder(resp.Body).Decode(&reply); err != nil { 681 | return nil, err 682 | } 683 | return reply.Templates, nil 684 | } 685 | 686 | func (n *nessusImpl) PolicyTemplates() ([]Template, error) { 687 | if n.verbose { 688 | log.Println("Getting policy templates...") 689 | } 690 | 691 | resp, err := n.Request("GET", "/editor/policy/templates", nil, []int{http.StatusOK}) 692 | if err != nil { 693 | return nil, err 694 | } 695 | defer resp.Body.Close() 696 | reply := &listTemplatesResp{} 697 | if err = json.NewDecoder(resp.Body).Decode(&reply); err != nil { 698 | return nil, err 699 | } 700 | return reply.Templates, nil 701 | } 702 | 703 | // StartScan starts the given scan and returns its UUID. 704 | func (n *nessusImpl) StartScan(scanID int64) (string, error) { 705 | if n.verbose { 706 | log.Println("Starting scan...") 707 | } 708 | 709 | resp, err := n.Request("POST", fmt.Sprintf("/scans/%d/launch", scanID), nil, []int{http.StatusOK}) 710 | if err != nil { 711 | return "", err 712 | } 713 | defer resp.Body.Close() 714 | reply := &startScanResp{} 715 | if err = json.NewDecoder(resp.Body).Decode(&reply); err != nil { 716 | return "", err 717 | } 718 | return reply.UUID, nil 719 | } 720 | 721 | func (n *nessusImpl) PauseScan(scanID int64) error { 722 | if n.verbose { 723 | log.Println("Pausing scan...") 724 | } 725 | 726 | _, err := n.Request("POST", fmt.Sprintf("/scans/%d/pause", scanID), nil, []int{http.StatusOK}) 727 | return err 728 | } 729 | 730 | func (n *nessusImpl) ResumeScan(scanID int64) error { 731 | if n.verbose { 732 | log.Println("Resume scan...") 733 | } 734 | 735 | _, err := n.Request("POST", fmt.Sprintf("/scans/%d/resume", scanID), nil, []int{http.StatusOK}) 736 | return err 737 | } 738 | 739 | func (n *nessusImpl) StopScan(scanID int64) error { 740 | if n.verbose { 741 | log.Println("Stop scan...") 742 | } 743 | 744 | _, err := n.Request("POST", fmt.Sprintf("/scans/%d/stop", scanID), nil, []int{http.StatusOK}) 745 | return err 746 | } 747 | 748 | func (n *nessusImpl) DeleteScan(scanID int64) error { 749 | if n.verbose { 750 | log.Println("Deleting scan...") 751 | } 752 | 753 | _, err := n.Request("DELETE", fmt.Sprintf("/scans/%d", scanID), nil, []int{http.StatusOK}) 754 | return err 755 | } 756 | 757 | func (n *nessusImpl) ScanDetails(scanID int64) (*ScanDetailsResp, error) { 758 | if n.verbose { 759 | log.Println("Getting details about a scan...") 760 | } 761 | 762 | resp, err := n.Request("GET", fmt.Sprintf("/scans/%d", scanID), nil, []int{http.StatusOK}) 763 | if err != nil { 764 | return nil, err 765 | } 766 | defer resp.Body.Close() 767 | reply := &ScanDetailsResp{} 768 | if err = json.NewDecoder(resp.Body).Decode(&reply); err != nil { 769 | return nil, err 770 | } 771 | return reply, nil 772 | } 773 | 774 | func (n *nessusImpl) ConfigureScan(scanID int64, scanSetting NewScanRequest) (*Scan, error) { 775 | if n.verbose { 776 | log.Println("Configuring a scan...") 777 | } 778 | 779 | resp, err := n.Request("PUT", fmt.Sprintf("/scans/%d", scanID), scanSetting, []int{http.StatusOK}) 780 | if nil != err { 781 | return nil, err 782 | } 783 | 784 | defer resp.Body.Close() 785 | reply := &Scan{} 786 | if err = json.NewDecoder(resp.Body).Decode(&reply); nil != err { 787 | return nil, err 788 | } 789 | return reply, nil 790 | } 791 | 792 | func (n *nessusImpl) Timezones() ([]TimeZone, error) { 793 | if n.verbose { 794 | log.Println("Getting list of timezones...") 795 | } 796 | 797 | resp, err := n.Request("GET", "/scans/timezones", nil, []int{http.StatusOK}) 798 | if err != nil { 799 | return nil, err 800 | } 801 | defer resp.Body.Close() 802 | reply := &tzResp{} 803 | if err = json.NewDecoder(resp.Body).Decode(&reply); err != nil { 804 | return nil, err 805 | } 806 | return reply.Timezones, nil 807 | } 808 | 809 | func (n *nessusImpl) Folders() ([]Folder, error) { 810 | if n.verbose { 811 | log.Println("Getting list of folders...") 812 | } 813 | 814 | resp, err := n.Request("GET", "/folders", nil, []int{http.StatusOK}) 815 | if err != nil { 816 | return nil, err 817 | } 818 | defer resp.Body.Close() 819 | reply := &listFoldersResp{} 820 | if err = json.NewDecoder(resp.Body).Decode(&reply); err != nil { 821 | return nil, err 822 | } 823 | return reply.Folders, nil 824 | } 825 | 826 | func (n *nessusImpl) CreateFolder(name string) error { 827 | if n.verbose { 828 | log.Println("Creating folders...") 829 | } 830 | 831 | req := createFolderRequest{Name: name} 832 | _, err := n.Request("POST", "/folders", req, []int{http.StatusOK}) 833 | return err 834 | } 835 | 836 | func (n *nessusImpl) EditFolder(folderID int64, newName string) error { 837 | if n.verbose { 838 | log.Println("Editing folders...") 839 | } 840 | 841 | req := editFolderRequest{Name: newName} 842 | _, err := n.Request("PUT", fmt.Sprintf("/folders/%d", folderID), req, []int{http.StatusOK}) 843 | return err 844 | } 845 | 846 | func (n *nessusImpl) DeleteFolder(folderID int64) error { 847 | if n.verbose { 848 | log.Println("Deleting folders...") 849 | } 850 | 851 | _, err := n.Request("DELETE", fmt.Sprintf("/folders/%d", folderID), nil, []int{http.StatusOK}) 852 | return err 853 | } 854 | 855 | const ( 856 | ExportNessus = "nessus" 857 | ExportPDF = "pdf" 858 | ExportHTML = "html" 859 | ExportCSV = "csv" 860 | ExportDB = "db" 861 | ) 862 | 863 | // ExportScan exports a scan to a File resource. 864 | // Call ExportStatus to get the status of the export and call Download() to download the actual file. 865 | func (n *nessusImpl) ExportScan(scanID int64, format string) (int64, error) { 866 | if n.verbose { 867 | log.Println("Exporting scan...") 868 | } 869 | 870 | req := exportScanRequest{Format: format} 871 | resp, err := n.Request("POST", fmt.Sprintf("/scans/%d/export", scanID), req, []int{http.StatusOK}) 872 | if err != nil { 873 | return 0, err 874 | } 875 | defer resp.Body.Close() 876 | reply := &exportScanResp{} 877 | if err = json.NewDecoder(resp.Body).Decode(&reply); err != nil { 878 | return 0, err 879 | } 880 | return reply.File, nil 881 | } 882 | 883 | // ExportFinished returns whether the given scan export file has finished being prepared. 884 | func (n *nessusImpl) ExportFinished(scanID, exportID int64) (bool, error) { 885 | if n.verbose { 886 | log.Println("Getting export status...") 887 | } 888 | 889 | resp, err := n.Request("GET", fmt.Sprintf("/scans/%d/export/%d/status", scanID, exportID), nil, []int{http.StatusOK}) 890 | if err != nil { 891 | return false, err 892 | } 893 | defer resp.Body.Close() 894 | reply := &exportStatusResp{} 895 | if err = json.NewDecoder(resp.Body).Decode(&reply); err != nil { 896 | return false, err 897 | } 898 | return reply.Status == "ready", nil 899 | } 900 | 901 | // DownloadExport will download the given export from nessus. 902 | func (n *nessusImpl) DownloadExport(scanID, exportID int64) ([]byte, error) { 903 | if n.verbose { 904 | log.Println("Downloading export file...") 905 | } 906 | 907 | resp, err := n.Request("GET", fmt.Sprintf("/scans/%d/export/%d/download", scanID, exportID), nil, []int{http.StatusOK}) 908 | if err != nil { 909 | return nil, err 910 | } 911 | body, err := ioutil.ReadAll(resp.Body) 912 | defer resp.Body.Close() 913 | if err != nil { 914 | return nil, err 915 | } 916 | return body, err 917 | } 918 | 919 | // TODO: Currently returns a 404... not exposed yet? 920 | func (n *nessusImpl) ListGroups() ([]Group, error) { 921 | if n.verbose { 922 | log.Println("Listing groups...") 923 | } 924 | 925 | resp, err := n.Request("GET", "/groups", nil, []int{http.StatusOK}) 926 | if err != nil { 927 | return nil, err 928 | } 929 | defer resp.Body.Close() 930 | reply := &listGroupsResp{} 931 | if err = json.NewDecoder(resp.Body).Decode(&reply); err != nil { 932 | return nil, err 933 | } 934 | return reply.Groups, nil 935 | } 936 | 937 | // TODO: Currently returns a 404... not exposed yet? 938 | func (n *nessusImpl) CreateGroup(name string) (Group, error) { 939 | if n.verbose { 940 | log.Println("Creating a group...") 941 | } 942 | 943 | req := createGroupRequest{ 944 | Name: name, 945 | } 946 | resp, err := n.Request("POST", "/groups", req, []int{http.StatusOK}) 947 | if err != nil { 948 | return Group{}, err 949 | } 950 | defer resp.Body.Close() 951 | var reply Group 952 | if err = json.NewDecoder(resp.Body).Decode(&reply); err != nil { 953 | return Group{}, err 954 | } 955 | return reply, nil 956 | } 957 | 958 | func (n *nessusImpl) Permissions(objectType string, objectID int64) ([]Permission, error) { 959 | if n.verbose { 960 | log.Println("Creating a group...") 961 | } 962 | 963 | resp, err := n.Request("GET", fmt.Sprintf("/permissions/%s/%d", objectType, objectID), nil, []int{http.StatusOK}) 964 | if err != nil { 965 | return nil, err 966 | } 967 | defer resp.Body.Close() 968 | var reply []Permission 969 | if err = json.NewDecoder(resp.Body).Decode(&reply); err != nil { 970 | return nil, err 971 | } 972 | return reply, nil 973 | } 974 | 975 | // CreatePolicy Create a policy. 976 | func (n *nessusImpl) CreatePolicy(createPolicyRequest CreatePolicyRequest) (CreatePolicyResp, error) { 977 | if n.verbose { 978 | log.Println("Creating a policy...") 979 | } 980 | 981 | resp, err := n.Request("POST", "/policies", createPolicyRequest, []int{http.StatusOK}) 982 | if err != nil { 983 | return CreatePolicyResp{}, err 984 | } 985 | 986 | defer resp.Body.Close() 987 | var reply CreatePolicyResp 988 | if err = json.NewDecoder(resp.Body).Decode(&reply); err != nil { 989 | return CreatePolicyResp{}, err 990 | } 991 | return reply, nil 992 | } 993 | 994 | // ConfigurePolicy Changes the parameters of a policy. 995 | func (n *nessusImpl) ConfigurePolicy(policyID int64, createPolicyRequest CreatePolicyRequest) error { 996 | if n.verbose { 997 | log.Println("Configuring a policy...") 998 | } 999 | 1000 | _, err := n.Request("PUT", fmt.Sprintf("/policies/%d", policyID), createPolicyRequest, []int{http.StatusOK}) 1001 | return err 1002 | } 1003 | 1004 | // DeletePolicy Delete a policy. 1005 | func (n *nessusImpl) DeletePolicy(policyID int64) error { 1006 | if n.verbose { 1007 | log.Println("Deleting a policy...") 1008 | } 1009 | 1010 | _, err := n.Request("DELETE", fmt.Sprintf("/policies/%d", policyID), nil, []int{http.StatusOK}) 1011 | return err 1012 | } 1013 | 1014 | // Upload Upload a file. 1015 | func (n *nessusImpl) Upload(filePath string) error { 1016 | if n.verbose { 1017 | log.Println("Uploading a file...") 1018 | } 1019 | 1020 | f, err := os.OpenFile(filePath, os.O_RDONLY, 0644) 1021 | if err != nil { 1022 | return err 1023 | } 1024 | 1025 | body := &bytes.Buffer{} 1026 | writer := multipart.NewWriter(body) 1027 | part, err := writer.CreateFormFile("Filedata", filepath.Base(filePath)) 1028 | if err != nil { 1029 | return err 1030 | } 1031 | _, err = io.Copy(part, f) 1032 | 1033 | if err = writer.Close(); nil != err { 1034 | return err 1035 | } 1036 | 1037 | u, err := url.ParseRequestURI(n.apiURL) 1038 | if err != nil { 1039 | return err 1040 | } 1041 | u.Path = "/file/upload" 1042 | urlStr := fmt.Sprintf("%v", u) 1043 | 1044 | req, err := http.NewRequest(http.MethodPost, urlStr, body) 1045 | req.Header.Set("Content-Type", writer.FormDataContentType()) 1046 | 1047 | req.Header.Add("Accept", "application/json") 1048 | if n.authCookie != "" { 1049 | req.Header.Add("X-Cookie", fmt.Sprintf("token=%s", n.authCookie)) 1050 | } 1051 | 1052 | resp, err := n.client.Do(req) 1053 | if nil != err { 1054 | return err 1055 | } 1056 | 1057 | reply := struct { 1058 | FileUploaded string `json:"fileuploaded"` 1059 | }{} 1060 | 1061 | if err = json.NewDecoder(resp.Body).Decode(&reply); nil != err { 1062 | return err 1063 | } 1064 | 1065 | // Duplicate updates will get different replies 1066 | // request: CIS_CentOS_7_Server_L1_v3.0.0.audit 1067 | // reply: {FileUploaded:CIS_CentOS_7_Server_L1_v3.0.0-6.audit} 1068 | if 0 == len(reply.FileUploaded) { 1069 | return fmt.Errorf("Upload failed, api reply: %+v", reply) 1070 | } 1071 | 1072 | return nil 1073 | } 1074 | 1075 | // AgentGroups Returns a list of agent groups. 1076 | func (n *nessusImpl) AgentGroups() ([]AgentGroup, error) { 1077 | if n.verbose { 1078 | log.Println("Getting list of agent-groups...") 1079 | } 1080 | 1081 | resp, err := n.Request("GET", "/agent-groups", nil, []int{http.StatusOK}) 1082 | if err != nil { 1083 | return nil, err 1084 | } 1085 | defer resp.Body.Close() 1086 | reply := &listAgentGroupsResp{ 1087 | Groups: make([]AgentGroup, 0), 1088 | } 1089 | if err = json.NewDecoder(resp.Body).Decode(&reply); err != nil { 1090 | return nil, err 1091 | } 1092 | return reply.Groups, nil 1093 | } 1094 | -------------------------------------------------------------------------------- /nessie_test.go: -------------------------------------------------------------------------------- 1 | package nessie 2 | 3 | import ( 4 | "crypto/rand" 5 | "crypto/rsa" 6 | "crypto/tls" 7 | "crypto/x509" 8 | "crypto/x509/pkix" 9 | "encoding/json" 10 | "encoding/pem" 11 | "math/big" 12 | "net/http" 13 | "net/http/httptest" 14 | "testing" 15 | "time" 16 | 17 | "github.com/gorilla/schema" 18 | ) 19 | 20 | func TestRequest(t *testing.T) { 21 | // Test structure to be serialized. 22 | type payload struct { 23 | A int `json:"a"` 24 | } 25 | var tests = []struct { 26 | method string 27 | resource string 28 | sentPayload payload 29 | wantPayload string 30 | serverStatus int 31 | wantStatus []int 32 | wantError bool 33 | }{ 34 | // All succeeding methods. 35 | {"GET", "/test", payload{}, "{\"a\":0}", http.StatusOK, []int{http.StatusOK}, false}, 36 | {"POST", "/test", payload{}, "{\"a\":0}", http.StatusOK, []int{http.StatusOK}, false}, 37 | {"DELETE", "/test", payload{}, "{\"a\":0}", http.StatusOK, []int{http.StatusOK}, false}, 38 | {"PUT", "/test", payload{}, "{\"a\":0}", http.StatusOK, []int{http.StatusOK}, false}, 39 | // Querystring test 40 | {"GET", "/test?a=42", payload{}, "{\"a\":42}", http.StatusOK, []int{http.StatusOK}, false}, 41 | // Payload test. 42 | {"GET", "/test", payload{42}, "{\"a\":42}", http.StatusOK, []int{http.StatusOK}, false}, 43 | // Expected failure. 44 | {"POST", "/test", payload{}, "{\"a\":0}", http.StatusInternalServerError, []int{http.StatusInternalServerError}, false}, 45 | // Unexpected failure 46 | {"POST", "/test", payload{}, "{\"a\":0}", http.StatusInternalServerError, []int{http.StatusOK}, true}, 47 | } 48 | for _, tt := range tests { 49 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 50 | w.WriteHeader(tt.serverStatus) 51 | request := &payload{} 52 | 53 | if err := json.NewDecoder(r.Body).Decode(&request); err != nil { 54 | t.Errorf("could not decode request body: %v", err) 55 | return 56 | } 57 | 58 | decoder := schema.NewDecoder() 59 | if err := r.ParseForm(); nil != err { 60 | t.Errorf("could not parse form: %v", err) 61 | } 62 | 63 | if err := decoder.Decode(request, r.Form); nil != err { 64 | t.Errorf("could not decode request: %v", err) 65 | } 66 | 67 | requestBytes, err := json.Marshal(request) 68 | if nil != err { 69 | return 70 | } 71 | 72 | requestStr := string(requestBytes) 73 | if string(requestStr) != tt.wantPayload { 74 | t.Errorf("unexpected payload, got=%s, want=%s", requestStr, tt.wantPayload) 75 | } 76 | })) 77 | n := &nessusImpl{ 78 | apiURL: ts.URL, 79 | client: &http.Client{ 80 | Transport: &http.Transport{ 81 | TLSClientConfig: &tls.Config{ 82 | InsecureSkipVerify: true, 83 | }, 84 | }, 85 | }, 86 | } 87 | n.SetVerbose(true) 88 | resp, err := n.Request(tt.method, tt.resource, tt.sentPayload, tt.wantStatus) 89 | if tt.wantError { 90 | if err == nil { 91 | t.Errorf("got no error, expected one (%+v)", tt) 92 | } 93 | continue 94 | } 95 | if err != nil { 96 | t.Errorf("error in Request: %v (%+v)", err, tt) 97 | continue 98 | } 99 | if resp.StatusCode != tt.serverStatus { 100 | t.Errorf("got status code=%d, wanted=%d", resp.StatusCode, tt.serverStatus) 101 | } 102 | } 103 | } 104 | 105 | func TestLogin(t *testing.T) { 106 | server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 107 | w.WriteHeader(200) 108 | w.Header().Set("Content-Type", "application/json") 109 | j, err := json.Marshal(&loginResp{Token: "some token"}) 110 | if err != nil { 111 | t.Fatalf("cannot serialize login response: %v", err) 112 | } 113 | w.Write(j) 114 | })) 115 | defer server.Close() 116 | n, err := NewInsecureNessus(server.URL) 117 | if err != nil { 118 | t.Fatalf("cannot create nessus instance: %v", err) 119 | } 120 | 121 | if err := n.Login("username", "password"); err != nil { 122 | t.Fatalf("got error during login: %v", err) 123 | } 124 | if got, want := n.AuthCookie(), "some token"; got != want { 125 | t.Fatalf("wrong auth cookie, got=%q, want=%q", got, want) 126 | } 127 | } 128 | 129 | func TestMethods(t *testing.T) { 130 | var tests = []struct { 131 | resp interface{} 132 | statusCode int 133 | call func(n Nessus) 134 | }{ 135 | {&Session{}, http.StatusOK, func(n Nessus) { n.Session() }}, 136 | {&ServerProperties{}, http.StatusOK, func(n Nessus) { n.ServerProperties() }}, 137 | {&ServerStatus{}, http.StatusOK, func(n Nessus) { n.ServerStatus() }}, 138 | {&User{}, http.StatusOK, func(n Nessus) { 139 | n.CreateUser("username", "pass", UserTypeLocal, Permissions32, "name", "email@foo.com") 140 | }}, 141 | {&listUsersResp{}, http.StatusOK, func(n Nessus) { n.ListUsers() }}, 142 | {nil, http.StatusOK, func(n Nessus) { n.DeleteUser(42) }}, 143 | {nil, http.StatusOK, func(n Nessus) { n.SetUserPassword(42, "newpass") }}, 144 | {&User{}, http.StatusOK, func(n Nessus) { 145 | n.EditUser(42, Permissions128, "newname", "newmain@goo.fom") 146 | }}, 147 | {[]PluginFamily{}, http.StatusOK, func(n Nessus) { n.PluginFamilies() }}, 148 | {&FamilyDetails{}, http.StatusOK, func(n Nessus) { n.FamilyDetails(42) }}, 149 | {&PluginDetails{}, http.StatusOK, func(n Nessus) { n.PluginDetails(42) }}, 150 | {[]Scanner{}, http.StatusOK, func(n Nessus) { n.Scanners() }}, 151 | {&listPoliciesResp{}, http.StatusOK, func(n Nessus) { n.Policies() }}, 152 | {&Scan{}, http.StatusOK, func(n Nessus) { 153 | n.NewScan("editorUUID", "settingsName", 42, 43, 44, LaunchDaily, []string{"target1", "target2"}) 154 | }}, 155 | {&ListScansResponse{}, http.StatusOK, func(n Nessus) { n.Scans() }}, 156 | {[]Template{}, http.StatusOK, func(n Nessus) { n.ScanTemplates() }}, 157 | {[]Template{}, http.StatusOK, func(n Nessus) { n.PolicyTemplates() }}, 158 | {"id", http.StatusOK, func(n Nessus) { n.StartScan(42) }}, 159 | {nil, http.StatusOK, func(n Nessus) { n.PauseScan(42) }}, 160 | {nil, http.StatusOK, func(n Nessus) { n.ResumeScan(42) }}, 161 | {nil, http.StatusOK, func(n Nessus) { n.StopScan(42) }}, 162 | {nil, http.StatusOK, func(n Nessus) { n.DeleteScan(42) }}, 163 | {&ScanDetailsResp{}, http.StatusOK, func(n Nessus) { n.ScanDetails(42) }}, 164 | {[]TimeZone{}, http.StatusOK, func(n Nessus) { n.Timezones() }}, 165 | {[]Folder{}, http.StatusOK, func(n Nessus) { n.Folders() }}, 166 | {nil, http.StatusOK, func(n Nessus) { n.CreateFolder("name") }}, 167 | {nil, http.StatusOK, func(n Nessus) { n.EditFolder(42, "newname") }}, 168 | {nil, http.StatusOK, func(n Nessus) { n.DeleteFolder(42) }}, 169 | {42, http.StatusOK, func(n Nessus) { n.ExportScan(42, ExportPDF) }}, 170 | {true, http.StatusOK, func(n Nessus) { n.ExportFinished(42, 43) }}, 171 | {[]byte("raw export"), http.StatusOK, func(n Nessus) { n.DownloadExport(42, 43) }}, 172 | {[]Permission{}, http.StatusOK, func(n Nessus) { n.Permissions("scanner", 42) }}, 173 | } 174 | for _, tt := range tests { 175 | server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 176 | w.WriteHeader(tt.statusCode) 177 | if tt.resp != nil { 178 | j, err := json.Marshal(tt.resp) 179 | if err != nil { 180 | t.Fatalf("cannot serialize response: %v", err) 181 | } 182 | w.Write(j) 183 | } 184 | })) 185 | defer server.Close() 186 | n, err := NewInsecureNessus(server.URL) 187 | if err != nil { 188 | t.Fatalf("cannot create nessus instance: %v", err) 189 | } 190 | n.SetVerbose(true) 191 | tt.call(n) 192 | } 193 | } 194 | 195 | func TestSha256Fingerprint(t *testing.T) { 196 | want := "AzuD2SQxVI4TQkkDwjWpkir1bdNNU8m3KzfPFYSJIT4=" 197 | got := sha256Fingerprint([]byte("abc123!")) 198 | if got != want { 199 | t.Errorf("fingerprint calculation failed, got=%v, want=%v", got, want) 200 | } 201 | } 202 | 203 | func generateCert(validNotBefore time.Time, validNotAfter time.Time) (*x509.Certificate, tls.Certificate, error) { 204 | privKey, err := rsa.GenerateKey(rand.Reader, 2048) 205 | if err != nil { 206 | return nil, tls.Certificate{}, err 207 | } 208 | template := x509.Certificate{ 209 | BasicConstraintsValid: true, 210 | ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, 211 | KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, 212 | NotBefore: validNotBefore, 213 | NotAfter: validNotAfter, 214 | SerialNumber: big.NewInt(1), 215 | Subject: pkix.Name{Organization: []string{"Example Inc"}}, 216 | } 217 | certDer, err := x509.CreateCertificate(rand.Reader, &template, &template, &privKey.PublicKey, privKey) 218 | if err != nil { 219 | return nil, tls.Certificate{}, err 220 | } 221 | certX509, err := x509.ParseCertificate(certDer) 222 | if err != nil { 223 | return nil, tls.Certificate{}, err 224 | } 225 | keypair, err := tls.X509KeyPair( 226 | pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDer}), 227 | pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privKey)})) 228 | return certX509, keypair, err 229 | } 230 | 231 | // An empty fingerprint would allow to create a nessus instance without any verification. 232 | func TestNewFingerprintedNessus(t *testing.T) { 233 | _, err := NewFingerprintedNessus("https://192.0.2.1", []string{}) 234 | if err == nil { 235 | t.Fatalf("should not accept empty fingerprint: %v", err) 236 | } 237 | _, err = NewFingerprintedNessus("https://192.0.2.1", []string{"a"}) 238 | if err != nil { 239 | t.Fatalf("should accept a non-empty fingerprint: %v", err) 240 | } 241 | } 242 | 243 | func TestCreateDialTLSFuncToVerifyFingerprint(t *testing.T) { 244 | var tests = []struct { 245 | fingerprint func([]byte) string 246 | validNotBefore time.Time 247 | validNotAfter time.Time 248 | wantError bool 249 | }{ 250 | // Correct fingerprint, should succeed. 251 | {func(cert []byte) string { return sha256Fingerprint(cert) }, time.Now().Truncate(1 * time.Hour), time.Now().Add(1 * time.Hour), false}, 252 | // Correct fingerprint, cert not yet valid, should succeed. 253 | {func(cert []byte) string { return sha256Fingerprint(cert) }, time.Now().Add(1 * time.Hour), time.Now().Add(2 * time.Hour), false}, 254 | // Correct fingerprint, cert not valid anymore, should succeed. 255 | {func(cert []byte) string { return sha256Fingerprint(cert) }, time.Now().Truncate(2 * time.Hour), time.Now().Truncate(1 * time.Hour), false}, 256 | // No fingerprint given (empty string), should fail. 257 | {func(_ []byte) string { return "" }, time.Now().Truncate(1 * time.Hour), time.Now().Add(1 * time.Hour), true}, 258 | // Wrong fingerprint given, should fail. 259 | {func(_ []byte) string { return "TW1NeU5tSTBObUkyT0dabVl6WTRabVk1T1dJME5UTmpNV1E=" }, time.Now().Truncate(1 * time.Hour), time.Now().Add(1 * time.Hour), true}, 260 | // Wrong fingerprint given, should fail. 261 | {func(_ []byte) string { return "x" }, time.Now().Truncate(1 * time.Hour), time.Now().Add(1 * time.Hour), true}, 262 | } 263 | for _, tt := range tests { 264 | srvCertX509, srvKeypair, err := generateCert(tt.validNotBefore, tt.validNotAfter) 265 | if err != nil { 266 | t.Fatalf("failed to create x509 key pair: %v", err) 267 | } 268 | srvConfig := &tls.Config{Certificates: []tls.Certificate{srvKeypair}} 269 | srvListener, err := tls.Listen("tcp", "127.0.0.1:0", srvConfig) 270 | if err != nil { 271 | t.Fatalf("cannot listen: %v", err) 272 | return 273 | } 274 | go http.Serve(srvListener, http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {})) 275 | cConfig := &tls.Config{ 276 | InsecureSkipVerify: true, 277 | } 278 | wantFingerprint := tt.fingerprint(srvCertX509.RawSubjectPublicKeyInfo) 279 | client := &http.Client{ 280 | Transport: &http.Transport{ 281 | TLSClientConfig: cConfig, 282 | DialTLS: createDialTLSFuncToVerifyFingerprint([]string{wantFingerprint}, cConfig), 283 | }, 284 | } 285 | _, err = client.Get("https://" + srvListener.Addr().String()) 286 | if tt.wantError { 287 | if err == nil { 288 | t.Errorf("got no error, expected one (%+v)", tt) 289 | } 290 | continue 291 | } 292 | if err != nil { 293 | t.Errorf("error during fingerprint verification: %v (%+v)", err, tt) 294 | continue 295 | } 296 | } 297 | } 298 | -------------------------------------------------------------------------------- /requests.go: -------------------------------------------------------------------------------- 1 | package nessie 2 | 3 | type loginRequest struct { 4 | Username string `json:"username"` 5 | Password string `json:"password"` 6 | } 7 | 8 | type createUserRequest struct { 9 | Username string `json:"username"` 10 | Password string `json:"password"` 11 | Permissions string `json:"permissions"` 12 | Name string `json:"name"` 13 | Email string `json:"email"` 14 | Type string `json:"type"` 15 | } 16 | 17 | type setUserPasswordRequest struct { 18 | Password string `json:"password"` 19 | } 20 | 21 | type editUserRequest struct { 22 | Permissions string `json:"permissions"` 23 | Name string `json:"name"` 24 | Email string `json:"email"` 25 | } 26 | 27 | type NewScanRequest struct { 28 | UUID string `json:"uuid"` 29 | Settings ScanSettingsRequest `json:"settings"` 30 | } 31 | type ScanSettingsRequest struct { 32 | Acls []Acls `json:"acls"` 33 | Emails string `json:"emails"` 34 | FilterType string `json:"filter_type"` 35 | Filters []interface{} `json:"filters"` 36 | Launch string `json:"launch"` 37 | LaunchNow bool `json:"launch_now"` 38 | Enabled bool `json:"enabled"` 39 | UseDashboard string `json:"use_dashboard"` 40 | Name string `json:"name"` 41 | Description string `json:"description"` 42 | FolderID int64 `json:"folder_id"` 43 | ScannerID int64 `json:"scanner_id"` 44 | AgentGroupID []string `json:"agent_group_id"` 45 | ScanTimeWindow int64 `json:"scan_time_window"` 46 | PolicyID int64 `json:"policy_id"` 47 | TextTargets string `json:"text_targets"` 48 | FileTargets string `json:"file_targets"` 49 | RRules string `json:"rrules"` 50 | TimeZone string `json:"timezone"` 51 | StartTime string `json:"starttime"` 52 | } 53 | 54 | type createFolderRequest struct { 55 | Name string `json:"name"` 56 | } 57 | 58 | type editFolderRequest struct { 59 | Name string `json:"name"` 60 | } 61 | 62 | type exportScanRequest struct { 63 | Format string `json:"format"` 64 | } 65 | 66 | type createGroupRequest struct { 67 | Name string `json:"name"` 68 | } 69 | 70 | // CreatePolicyRequest Policies are created by sending the below fields. 71 | type CreatePolicyRequest struct { 72 | UUID string `json:"uuid"` 73 | Audits PolicyAudits `json:"audits"` 74 | Settings PolicySettings `json:"settings"` 75 | } 76 | type PolicyAudits struct { 77 | Custom interface{} `json:"custom"` 78 | Feed interface{} `json:"feed"` 79 | } 80 | type Acls struct { 81 | ObjectType string `json:"object_type"` 82 | Permissions int `json:"permissions"` 83 | Type string `json:"type"` 84 | DisplayName string `json:"display_name,omitempty"` 85 | Name string `json:"name,omitempty"` 86 | Owner int `json:"owner,omitempty"` 87 | ID int `json:"id,omitempty"` 88 | } 89 | type PolicySettings struct { 90 | UnixfileanalysisDisableXdev string `json:"unixfileanalysis_disable_xdev"` 91 | UnixfileanalysisIncludePaths string `json:"unixfileanalysis_include_paths"` 92 | UnixfileanalysisExcludePaths string `json:"unixfileanalysis_exclude_paths"` 93 | UnixfileanalysisFileExtensions string `json:"unixfileanalysis_file_extensions"` 94 | UnixfileanalysisMaxSize string `json:"unixfileanalysis_max_size"` 95 | UnixfileanalysisMaxCumulativeSize string `json:"unixfileanalysis_max_cumulative_size"` 96 | UnixfileanalysisMaxDepth string `json:"unixfileanalysis_max_depth"` 97 | StaggeredStartMins string `json:"staggered_start_mins"` 98 | LogWholeAttack string `json:"log_whole_attack"` 99 | EnablePluginDebugging string `json:"enable_plugin_debugging"` 100 | AuditTrail string `json:"audit_trail"` 101 | IncludeKb string `json:"include_kb"` 102 | EnablePluginList string `json:"enable_plugin_list"` 103 | AllowPostScanEditing string `json:"allow_post_scan_editing"` 104 | WmiNetstatScanner string `json:"wmi_netstat_scanner"` 105 | SSHNetstatScanner string `json:"ssh_netstat_scanner"` 106 | Acls []Acls `json:"acls"` 107 | Name string `json:"name"` 108 | Description string `json:"description"` 109 | } 110 | 111 | // AuditCustomItem custom audit item 112 | type AuditCustomItem struct { 113 | Category string `json:"category"` 114 | File string `json:"file"` 115 | } 116 | -------------------------------------------------------------------------------- /resources.go: -------------------------------------------------------------------------------- 1 | package nessie 2 | 3 | // Editor resources. 4 | 5 | // Template is used to create scans or policies with predefined parameters. 6 | type Template struct { 7 | // The uuid for the template. 8 | UUID string `json:"uuid"` 9 | // The short name of the template. 10 | Name string `json:"name"` 11 | // The long name of the template. 12 | Title string `json:"title"` 13 | // The description of the template. 14 | Desc string `json:"description"` 15 | // If true, template is only available on the cloud. 16 | CloudOnly bool `json:"cloud_only"` 17 | // If true, the template is only available for subscribers. 18 | SubscriptionOnly bool `json:"subscription_only"` 19 | // If true, the template is for agent scans. 20 | IsAgent bool `json:"is_agent"` 21 | // An external URL to link the template to. 22 | MoreInfo string `json:"more_info"` 23 | } 24 | 25 | type TemplateFormInput struct { 26 | ID string `json:"id"` 27 | Type string `json:"type"` 28 | Label string `json:"label"` 29 | Default string `json:"default"` 30 | Options []string `json:"options"` 31 | } 32 | 33 | type TemplateDisplayGroup struct { 34 | Name string `json:"name"` 35 | Title string `json:"title"` 36 | Inputs []string `json:"inputs"` 37 | Sections []string `json:"sections"` 38 | } 39 | 40 | type TemplateSection struct { 41 | Name string `json:"name"` 42 | Title string `json:"title"` 43 | Inputs []string `json:"inputs"` 44 | } 45 | 46 | type TemplateMode struct { 47 | ID string `json:"id"` 48 | Name string `json:"name"` 49 | Desc string `json:"desc"` 50 | } 51 | 52 | type TemplatePluginFamily struct { 53 | ID int64 `json:"id"` 54 | Count int64 `json:"count"` 55 | Status string `json:"status"` 56 | } 57 | 58 | type Filter struct { 59 | Name string `json:"name"` 60 | ReadableName string `json:"readable_name"` 61 | Operators []string `json:"operators"` 62 | Controls []FilterControls `json:"controls"` 63 | } 64 | 65 | type FilterControls struct { 66 | Type string `json:"type"` 67 | ReadableRegex string `json:"readable_regest"` 68 | Regex string `json:"regex"` 69 | Ooptions []string `json:"options"` 70 | } 71 | 72 | // Folders resources. 73 | 74 | type Folder struct { 75 | ID int64 `json:"id"` 76 | Name string `json:"name"` 77 | Type string `json:"type"` 78 | DefaultTag int64 `json:"default_tag"` 79 | Custom int64 `json:"custom"` 80 | UnreadCount int64 `json:"unread_count"` 81 | } 82 | 83 | // Groups resources. 84 | 85 | type Group struct { 86 | ID int64 `json:"id"` 87 | Name string `json:"name"` 88 | Permissions int64 `json:"permissions"` 89 | UserCount int64 `json:"user_count"` 90 | } 91 | 92 | // Permissions resources. 93 | 94 | type Permission struct { 95 | Owner int64 `json:"owner"` 96 | Type string `json:"type"` 97 | Permissions int64 `json:"permissions"` 98 | ID int64 `json:"id"` 99 | Name string `json:"name"` 100 | } 101 | 102 | // Plugins resources. 103 | 104 | type PluginAttr struct { 105 | Name string `json:"attribute_name"` 106 | Val string `json:"attribute_value"` 107 | } 108 | 109 | type PluginFamily struct { 110 | ID int64 `json:"id"` 111 | Name string `json:"name"` 112 | Count int64 `json:"count"` 113 | } 114 | 115 | type PluginFamilies struct { 116 | Families []PluginFamily `json:"families"` 117 | } 118 | 119 | type Plugin struct { 120 | ID int64 `json:"id"` 121 | Name string `json:"name"` 122 | } 123 | 124 | // Plugin-rules resources. 125 | 126 | type Rule struct { 127 | ID int64 `json:"id"` 128 | PluginID int64 `json:"plugin_id"` 129 | Date string `json:"date"` 130 | Host string `json:"host"` 131 | Type string `json:"type"` 132 | Owner string `json:"owner"` 133 | OwnerID int64 `json:"owner_id"` 134 | } 135 | 136 | // Policies resources. 137 | 138 | type Policy struct { 139 | ID int64 `json:"id"` 140 | TemplateUUID string `json:"template_uuid"` 141 | Name string `json:"name"` 142 | Desc string `json:"description"` 143 | OwnerID int64 `json:"owner_id"` 144 | Owner string `json:"owner"` 145 | Shared int64 `json:"shared"` 146 | UserPerms int64 `json:"user_permissions"` 147 | CreationDate int64 `json:"creation_date"` 148 | LastModificationDate int64 `json:"last_modification_date"` 149 | Visibility string `json:"visibility"` 150 | NoTarget string `json:"no_target"` 151 | } 152 | 153 | // Scanners resources. 154 | 155 | type Scanner struct { 156 | ID int64 `json:"id"` 157 | UUID string `json:"uuid"` 158 | Name string `json:"name"` 159 | Type string `json:"type"` 160 | Status string `json:"status"` 161 | ScanCount int64 `json:"scan_count"` 162 | EngineVersion string `json:"engine_version"` 163 | Platform string `json:"platform"` 164 | LoadedPluginSet string `json:"loaded_plugin_set"` 165 | RegistrationCode string `json:"registration_code"` 166 | Owner string `json:"owner"` 167 | } 168 | 169 | // Scan resource. 170 | type Scan struct { 171 | ID int64 `json:"id"` 172 | UUID string `json:"uuid"` 173 | Name string `json:"name"` 174 | Owner string `json:"owner"` 175 | Shared int `json:"shared"` 176 | UserPermissions int64 `json:"user_permissions"` 177 | CreationDate int64 `json:"creation_date"` 178 | LastModificationDate int64 `json:"last_modification_date"` 179 | StartTime string `json:"starttime"` 180 | TimeZone string `json:"timezone"` 181 | RRules string `json:"rrules"` 182 | ContainerID int `json:"container_id"` 183 | Description string `json:"description"` 184 | PolicyID int `json:"policy_id"` 185 | ScannerID int `json:"scanner_id"` 186 | Emails string `json:"emails"` 187 | AttachReport int `json:"attach_report"` 188 | AttachedReportMaximumSize int `json:"attached_report_maximum_size"` 189 | AttachedReportType interface{} `json:"attached_report_type"` 190 | Sms interface{} `json:"sms"` 191 | Enabled int `json:"enabled"` 192 | UseDashboard int `json:"use_dashboard"` 193 | DashboardFile interface{} `json:"dashboard_file"` 194 | LiveResults int `json:"live_results"` 195 | ScanTimeWindow int `json:"scan_time_window"` 196 | CustomTargets string `json:"custom_targets"` 197 | Migrated int `json:"migrated"` 198 | LastScheduledRun string `json:"last_scheduled_run"` 199 | NotificationFilters interface{} `json:"notification_filters"` 200 | TagID int `json:"tag_id"` 201 | DefaultPermisssions int `json:"default_permisssions"` 202 | OwnerID int `json:"owner_id"` 203 | Type string `json:"type"` 204 | } 205 | 206 | type Host struct { 207 | HostID int64 `json:"host_id"` 208 | HostIdx int64 `json:"host_index"` 209 | Hostname string `json:"hostname"` 210 | Progress string `json:"progress"` 211 | Critical int64 `json:"critical"` 212 | High int64 `json:"high"` 213 | Medium int64 `json:"medium"` 214 | Low int64 `json:"low"` 215 | Info int64 `json:"info"` 216 | TotalChecksConsidered int64 `json:"totalchecksconsidered"` 217 | NumChecksConsidered int64 `json:"numchecksconsidered"` 218 | ScanProgressTotal int64 `json:"scanprogresstotal"` 219 | ScanProgressCurrent int64 `json:"scanprogresscurrent"` 220 | Score int64 `json:"score"` 221 | } 222 | 223 | type Note struct { 224 | Title string `json:"title"` 225 | Message string `json:"message"` 226 | Severity int64 `json:"severity"` 227 | } 228 | 229 | type Remediation struct { 230 | Value string `json:"value"` 231 | Remediation string `json:"remediation"` 232 | NumHosts int64 `json:"hosts"` 233 | NumVulns string `json:"vulns"` 234 | } 235 | 236 | type History struct { 237 | HistoryID int64 `json:"history_id"` 238 | UUID string `json:"uuid"` 239 | OwnerID int64 `json:"owner_id"` 240 | Status string `json:"status"` 241 | CreationDate int64 `json:"creation_date"` 242 | LastModificationDate int64 `json:"last_modification_date"` 243 | } 244 | 245 | type Vulnerability struct { 246 | PluginID int64 `json:"plugin_id"` 247 | PluginName string `json:"plugin_name"` 248 | PluginFamily string `json:"plugin_family"` 249 | Count int64 `json:"count"` 250 | VulnIdx int64 `json:"vuln_index"` 251 | SeverityIdx int64 `json:"severity_index"` 252 | } 253 | 254 | type HostVulnerability struct { 255 | HostID int64 `json:"host_id"` 256 | Hostname string `json:"hostname"` 257 | PluginID int64 `json:"plugin_id"` 258 | PluginName string `json:"plugin_name"` 259 | PluginFamily string `json:"plugin_family"` 260 | Count int64 `json:"count"` 261 | VulnIdx int64 `json:"vuln_index"` 262 | SeverityIdx int64 `json:"severity_index"` 263 | Severity int64 `json:"severity"` 264 | } 265 | 266 | type HostCompliance struct { 267 | HostID int64 `json:"host_id"` 268 | Hostname string `json:"hostname"` 269 | PluginID int64 `json:"plugin_id"` 270 | PluginName string `json:"plugin_name"` 271 | PluginFamily string `json:"plugin_family"` 272 | Count int64 `json:"count"` 273 | SeverityIdx int64 `json:"severity_index"` 274 | Severity int64 `json:"severity"` 275 | } 276 | 277 | type PluginOutput struct { 278 | PluginOutput string `json:"plugin_output"` 279 | Hosts string `json:"hosts"` 280 | Severity int64 `json:"severity"` 281 | Ports []string `json:"ports"` 282 | } 283 | 284 | type TimeZone struct { 285 | Name string `json:"name"` 286 | Val string `json:"value"` 287 | } 288 | 289 | // Sessions resources. 290 | 291 | type Session struct { 292 | ID int64 `json:"id"` 293 | Username string `json:"username"` 294 | Email string `json:"email"` 295 | Name string `json:"name"` 296 | Type string `json:"type"` 297 | Perms int64 `json:"permissions"` 298 | LastLogin int64 `json:"last_login"` 299 | ContainerID int64 `json:"container_id"` 300 | Groups []string `json:"groups"` 301 | } 302 | 303 | type User struct { 304 | ID int `json:"id"` 305 | Username string `json:"username"` 306 | Name string `json:"name"` 307 | Email string `json:"email"` 308 | Permissions int `json:"permissions"` 309 | LastLogin int `json:"lastlogin"` 310 | Type string `json:"type"` 311 | } 312 | 313 | // AgentGroup The details of an agent group. 314 | type AgentGroup struct { 315 | ID int64 `json:"id"` 316 | Name string `json:"name"` 317 | OwnerID int64 `json:"owner_id"` 318 | Owner string `json:"owner"` 319 | Shared int `json:"shared"` 320 | UserPerms int64 `json:"user_permissions"` 321 | CreationDate int64 `json:"creation_date"` 322 | LastModificationDate int64 `json:"last_modification_date"` 323 | } 324 | -------------------------------------------------------------------------------- /responses.go: -------------------------------------------------------------------------------- 1 | package nessie 2 | 3 | // loginResp is the internal response to login attemps. 4 | type loginResp struct { 5 | Token string `json:"token"` 6 | } 7 | 8 | // ServerProperties is the structure returned by the ServerProperties() method. 9 | type ServerProperties struct { 10 | Token string `json:"token"` 11 | NessusType string `json:"nessus_type"` 12 | NessusUIVersion string `json:"nessus_ui_version"` 13 | ServerVersion string `json:"server_version"` 14 | Feed string `json:"feed"` 15 | Enterprise bool `json:"enterprise"` 16 | LoadedPluginSet string `json:"loaded_plugin_set"` 17 | ServerUUID string `json:"server_uuid"` 18 | Expiration int64 `json:"expiration"` 19 | Notifications []struct { 20 | Type string `json:"type"` 21 | Msg string `json:"message"` 22 | } `json:"notifications"` 23 | ExpirationTime int64 `json:"expiration_time"` 24 | Capabilities struct { 25 | MultiScanner bool `json:"multi_scanner"` 26 | ReportEmailConfig bool `json:"report_email_config"` 27 | } `json:"capabilities"` 28 | PluginSet string `json:"plugin_set"` 29 | IdleTImeout int64 `json:"idle_timeout"` 30 | ScannerBoottime int64 `json:"scanner_boottime"` 31 | LoginBanner bool `json:"login_banner"` 32 | } 33 | 34 | // ServerStatus is the stucture returned by the ServerStatus() method. 35 | type ServerStatus struct { 36 | Status string `json:"status"` 37 | Progress int64 `json:"progress"` 38 | MustDestroySession bool 39 | } 40 | 41 | type listUsersResp struct { 42 | Users []User `json:"users"` 43 | } 44 | 45 | type FamilyDetails struct { 46 | Name string `json:"name"` 47 | ID int64 `json:"id"` 48 | Plugins []Plugin `json:"plugins"` 49 | } 50 | 51 | type PluginDetails struct { 52 | Plugin 53 | FamilyName string `json:"family_name"` 54 | Attrs []PluginAttr `json:"attributes"` 55 | } 56 | 57 | type listPoliciesResp struct { 58 | Policies []Policy `json:"policies"` 59 | } 60 | 61 | type ListScansResponse struct { 62 | Folders []Folder `json:"folders"` 63 | Scans []Scan `json:"scans"` 64 | Timestamp int64 `json:"timestamp"` 65 | } 66 | 67 | type listTemplatesResp struct { 68 | Templates []Template `json:"templates"` 69 | } 70 | 71 | type startScanResp struct { 72 | UUID string `json:"scan_uuid"` 73 | } 74 | 75 | type ScanDetailsResp struct { 76 | UUID string `json:"scan_uuid"` 77 | Info struct { 78 | EditAllowed bool `json:"edit_allowed"` 79 | Status string `json:"status"` 80 | Policy string `json:"policy"` 81 | PCICanUpload bool `json:"pci-can-upload"` 82 | HasAuditTrail bool `json:"hasaudittrail"` 83 | ScanStart int64 `json:"scan_start"` 84 | FolderID int64 `json:"folder_id"` 85 | Targets string `json:"targets"` 86 | Timestamp int64 `json:"timestamp"` 87 | ObjectID int64 `json:"object_id"` 88 | ScannerName string `json:"scanner_name"` 89 | HasKB bool `json:"haskb"` 90 | UUID string `json:"uuid"` 91 | HostCount int64 `json:"hostcount"` 92 | ScanEnd int64 `json:"scan_end"` 93 | Name string `json:"name"` 94 | UserPerms int64 `json:"user_permissions"` 95 | Control bool `json:"control"` 96 | } `json:"info"` 97 | Hosts []Host `json:"hosts"` 98 | CompHosts []Host `json:"comphosts"` 99 | Notes []Note `json:"notes"` 100 | Remediations struct { 101 | Remediation Remediation `json:"remediation"` 102 | } `json:"remediations"` 103 | NumHosts int64 `json:"num_hosts"` 104 | NumCVEs int64 `json:"num_cves"` 105 | NumImpactedHosts int64 `json:"num_impacted_hosts"` 106 | NumRemediatedCVEs int64 `json:"num_remediated_cves"` 107 | Vulnerabilities []Vulnerability `json:"vulnerabilities"` 108 | Compliance []Vulnerability `json:"compliance"` 109 | History []History `json:"history"` 110 | Filters []Filter `json:"filters"` 111 | } 112 | 113 | type tzResp struct { 114 | Timezones []TimeZone `json:"timezones"` 115 | } 116 | 117 | type listFoldersResp struct { 118 | Folders []Folder `json:"folders"` 119 | } 120 | 121 | type exportScanResp struct { 122 | File int64 `json:"file"` 123 | } 124 | 125 | type exportStatusResp struct { 126 | Status string `json:"status"` 127 | } 128 | 129 | type listGroupsResp struct { 130 | Groups []Group `json:"groups"` 131 | } 132 | 133 | type listAgentGroupsResp struct { 134 | Groups []AgentGroup `json:"groups"` 135 | } 136 | 137 | // CreatePolicyResp response body If successful 138 | type CreatePolicyResp struct { 139 | PolicyID int64 `json:"policy_id"` 140 | PolicyName string `json:"policy_name"` 141 | } 142 | --------------------------------------------------------------------------------