Skip to content

Commit

Permalink
⭐️ add scanner client (#270)
Browse files Browse the repository at this point in the history
* ⭐️ add scanner client
* don't use errors package
clarify that a '2' is what comes back from a valid scan result

Signed-off-by: Christoph Hartmann <chris@lollyrock.com>
Signed-off-by: Joel Diaz <joel@mondoo.com>

Co-authored-by: Joel Diaz <joel@mondoo.com>
  • Loading branch information
chris-rock and Joel Diaz authored Apr 13, 2022
1 parent 1befa64 commit dae3784
Show file tree
Hide file tree
Showing 3 changed files with 480 additions and 0 deletions.
155 changes: 155 additions & 0 deletions pkg/scanner/scanner.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
package scanner

import (
"bytes"
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net"
"net/http"
"time"
)

const (
healthCheckEndpoint = "/Health/Check"
scanKubernetesEndpoint = "/Scan/RunKubernetesManifest"
defaultHttpTimeout = 30 * time.Second
defaultIdleConnTimeout = 30 * time.Second
defaultKeepAlive = 30 * time.Second
defaultTLSHandshakeTimeout = 10 * time.Second
maxIdleConnections = 100
)

func DefaultHttpClient() *http.Client {
tr := &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: defaultHttpTimeout,
KeepAlive: defaultKeepAlive,
}).DialContext,
MaxIdleConns: maxIdleConnections,
IdleConnTimeout: defaultIdleConnTimeout,
TLSHandshakeTimeout: defaultTLSHandshakeTimeout,
ExpectContinueTimeout: 1 * time.Second,
}

httpClient := &http.Client{
Transport: tr,
Timeout: defaultHttpTimeout,
}
return httpClient
}

type Scanner struct {
Endpoint string
Token string
httpclient http.Client
}

func (s *Scanner) request(ctx context.Context, url string, reqBodyBytes []byte) ([]byte, error) {
client := s.httpclient

header := make(http.Header)
header.Set("Accept", "application/json")
header.Set("Content-Type", "application/json")
header.Set("Authorization", "Bearer "+s.Token)

reader := bytes.NewReader(reqBodyBytes)
req, err := http.NewRequest(http.MethodPost, url, reader)
if err != nil {
return nil, err
}
req = req.WithContext(ctx)
req.Header = header

// do http call
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to do request: %v", err)
}

defer func() {
resp.Body.Close()
}()

return ioutil.ReadAll(resp.Body)
}

func (s *Scanner) HealthCheck(ctx context.Context, in *HealthCheckRequest) (*HealthCheckResponse, error) {
url := s.Endpoint + healthCheckEndpoint

reqBodyBytes, err := json.Marshal(in)
if err != nil {
return nil, fmt.Errorf("failed to marshal request: %v", err)
}

respBodyBytes, err := s.request(ctx, url, reqBodyBytes)
if err != nil {
return nil, fmt.Errorf("failed to parse response: %v", err)
}

out := &HealthCheckResponse{}
if err = json.Unmarshal(respBodyBytes, out); err != nil {
return nil, fmt.Errorf("failed to unmarshal proto response: %v", err)
}

return out, nil
}

func (s *Scanner) RunKubernetesManifest(ctx context.Context, in *KubernetesManifestJob) (*ScanResult, error) {
url := s.Endpoint + scanKubernetesEndpoint

reqBodyBytes, err := json.Marshal(in)
if err != nil {
return nil, fmt.Errorf("failed to marshal request: %v", err)
}

respBodyBytes, err := s.request(ctx, url, reqBodyBytes)
if err != nil {
return nil, fmt.Errorf("failed to parse response: %v", err)
}

out := &ScanResult{}
if err = json.Unmarshal(respBodyBytes, out); err != nil {
return nil, fmt.Errorf("failed to unmarshal proto response: %v", err)
}

return out, nil
}

type KubernetesManifestJob struct {
Files []*File `json:"files,omitempty"`
}

type File struct {
Data []byte `json:"data,omitempty"`
}

type ScanResult struct {
WorstScore *Score `json:"worstScore,omitempty"`
Ok bool `json:"ok,omitempty"`
}

type Score struct {
QrId string `json:"qr_id,omitempty"`
Type uint32 `json:"type,omitempty"`
Value uint32 `json:"value,omitempty"`
Weight uint32 `json:"weight,omitempty"`
ScoreCompletion uint32 `json:"score_completion,omitempty"`
DataTotal uint32 `json:"data_total,omitempty"`
DataCompletion uint32 `json:"data_completion,omitempty"`
Message string `json:"message,omitempty"`
}

type HealthCheckRequest struct{}

type HealthCheckResponse struct {
Status string `json:"status,omitempty"`
// returns rfc 3339 timestamp
Time string `json:"time,omitempty"`
// returns the major api version
ApiVersion string `json:"apiVersion,omitempty"`
// returns the git commit checksum
Build string `json:"build,omitempty"`
}
104 changes: 104 additions & 0 deletions pkg/scanner/scanner_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package scanner

import (
"context"
_ "embed"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
"sigs.k8s.io/yaml"
)

const (
// A valid result would come back as a '2'
validScanResult = uint32(2)
)

//go:embed testdata/webhook-payload.json
var webhookPayload []byte

func testServer() *httptest.Server {
mux := http.NewServeMux()
mux.HandleFunc(healthCheckEndpoint, func(w http.ResponseWriter, r *http.Request) {
result := &HealthCheckResponse{
Status: "SERVING",
}
data, err := json.Marshal(result)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
if _, err = w.Write(data); err != nil {
http.Error(w, err.Error(), 500)
return
}
})

mux.HandleFunc(scanKubernetesEndpoint, func(w http.ResponseWriter, r *http.Request) {
result := &ScanResult{
Ok: true,
WorstScore: &Score{
Type: validScanResult,
Value: 100,
},
}
data, err := json.Marshal(result)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
if _, err = w.Write(data); err != nil {
http.Error(w, err.Error(), 500)
return
}

})
return httptest.NewServer(mux)
}

func TestScanner(t *testing.T) {
testserver := testServer()
url := testserver.URL
token := ""

// To test with a real client, just set
// url := "http://127.0.0.1:8990"
// token := "<token here>"

// do client request
s := &Scanner{
Endpoint: url,
Token: token,
}

// Run Health Check
healthResp, err := s.HealthCheck(context.Background(), &HealthCheckRequest{})
require.NoError(t, err)
assert.True(t, healthResp.Status == "SERVING")

request := admission.Request{}
err = yaml.Unmarshal(webhookPayload, &request)
require.NoError(t, err)

k8sObjectData, err := yaml.Marshal(request.Object)
require.NoError(t, err)

result, err := s.RunKubernetesManifest(context.Background(), &KubernetesManifestJob{
Files: []*File{
{
Data: k8sObjectData,
},
},
})
require.NoError(t, err)
assert.NotNil(t, result)

// check if the scan passed
passed := result.WorstScore.Type == 2 && result.WorstScore.Value == 100
assert.True(t, passed)
}
Loading

0 comments on commit dae3784

Please sign in to comment.