providers/fireeye/api/client.go (92 lines of code) (raw):
// Copyright (c) Facebook, Inc. and its affiliates.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package api
import (
"bytes"
"context"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"hash"
"io"
"net/http"
"sync"
"time"
"github.com/pkg/errors"
"github.com/facebookincubator/nvdtools/providers/fireeye/schema"
"github.com/facebookincubator/nvdtools/providers/lib/client"
"github.com/facebookincubator/nvdtools/stats"
)
const (
acceptVersion = "2.6"
)
// Client struct
type Client struct {
client.Client
hash hash.Hash
publicKey string
baseURL string
m sync.Mutex
}
// NewClient creates an object which is used to query the FireEye API
func NewClient(c client.Client, baseURL, publicKey, privateKey string) *Client {
return &Client{
Client: c,
hash: hmac.New(sha256.New, []byte(privateKey)),
publicKey: publicKey,
baseURL: baseURL,
}
}
// Request will fetch the given endpoint and return the response
func (c *Client) Request(ctx context.Context, endpoint string) (io.Reader, error) {
req, err := http.NewRequest("GET", c.baseURL+endpoint, nil)
if err != nil {
return nil, errors.Wrap(err, "cannot create http get request")
}
req = req.WithContext(ctx)
acceptHeader := "application/json"
timestamp := time.Now().Format(time.RFC1123)
auth := c.getHash("%s%s%s%s", endpoint, acceptVersion, acceptHeader, timestamp)
// FireEye required
req.Header.Set("Accept", acceptHeader)
req.Header.Set("Accept-Version", acceptVersion)
req.Header.Set("X-Auth", c.publicKey)
req.Header.Set("X-Auth-Hash", auth)
req.Header.Set("Date", timestamp)
// execute the request
stats.IncrementCounter("request")
resp, err := c.Do(req)
if err != nil {
stats.IncrementCounter("request.error")
return nil, errors.Wrap(err, "cannot get url")
}
defer resp.Body.Close()
stats.IncrementCounter(fmt.Sprintf("request.code.%d", resp.StatusCode))
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("%d - %s", resp.StatusCode, http.StatusText(resp.StatusCode))
}
// response is always {success:boolean, message:something}
// First we decode this from response, and fail fast if success = false
// Otherwise, we return the message only
var fireeyeResult schema.Result
body := io.LimitReader(resp.Body, 2<<30) // 1 GB
if err := json.NewDecoder(body).Decode(&fireeyeResult); err != nil {
stats.IncrementCounter("request.feed.error")
return nil, errors.Wrap(err, "couldn't decode result")
}
var buff bytes.Buffer
if err := json.NewEncoder(&buff).Encode(fireeyeResult.Message); err != nil {
stats.IncrementCounter("request.feed.error")
return nil, errors.Wrap(err, "couldn't encode message back to buffer")
}
if !fireeyeResult.Success {
stats.IncrementCounter("request.feed.error")
var errorMessage schema.ResultErrorMessage
if err := json.Unmarshal(buff.Bytes(), &errorMessage); err != nil {
return nil, errors.Wrap(err, "failed to decode error message")
}
return nil, fmt.Errorf("%s: %s", errorMessage.Error, errorMessage.Description)
}
stats.IncrementCounter("request.success")
return &buff, nil
}
func (c *Client) getHash(format string, a ...interface{}) string {
c.m.Lock()
defer c.m.Unlock()
fmt.Fprintf(c.hash, format, a...)
b := c.hash.Sum(nil)
c.hash.Reset()
return hex.EncodeToString(b)
}