Skip to content

Commit

Permalink
feat: binarly teststep
Browse files Browse the repository at this point in the history
Signed-off-by: Fabian Wienand <[email protected]>
  • Loading branch information
Fabian Wienand committed Aug 20, 2024
1 parent 320e5e6 commit a2452e8
Show file tree
Hide file tree
Showing 3 changed files with 284 additions and 0 deletions.
80 changes: 80 additions & 0 deletions plugins/teststeps/binarly/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package binarly

import (
"encoding/json"
"fmt"
"time"

"github.com/linuxboot/contest/pkg/event"
"github.com/linuxboot/contest/pkg/event/testevent"
"github.com/linuxboot/contest/pkg/events"
"github.com/linuxboot/contest/pkg/test"
"github.com/linuxboot/contest/pkg/xcontext"
"github.com/linuxboot/contest/plugins/teststeps"
"github.com/linuxboot/contest/plugins/teststeps/abstraction/options"
)

const (
parametersKeyword = "parameters"
)

const (
defaultTimeout time.Duration = 60 * time.Minute
)

type parameters struct {
Token string `json:"token"`
File string `json:"file_path"`
}

// Name is the name used to look this plugin up.
var Name = "Binarly Report"

// TestStep implementation for the exec plugin
type TestStep struct {
parameters
options options.Parameters
}

// Run executes the step.
func (ts *TestStep) Run(ctx xcontext.Context, ch test.TestStepChannels, params test.TestStepParameters, ev testevent.Emitter, resumeState json.RawMessage) (json.RawMessage, error) {
tr := NewTargetRunner(ts, ev)
return teststeps.ForEachTarget(Name, ctx, ch, tr.Run)
}

func (ts *TestStep) populateParams(stepParams test.TestStepParameters) error {
var parameters, optionsParams *test.Param

if parameters = stepParams.GetOne(parametersKeyword); parameters.IsEmpty() {
return fmt.Errorf("parameters cannot be empty")
}

if err := json.Unmarshal(parameters.JSON(), &ts.parameters); err != nil {
return fmt.Errorf("failed to deserialize parameters: %v", err)
}

optionsParams = stepParams.GetOne(options.Keyword)

if !optionsParams.IsEmpty() {
if err := json.Unmarshal(optionsParams.JSON(), &ts.options); err != nil {
return fmt.Errorf("failed to deserialize options: %v", err)
}
}

return nil
}

// ValidateParameters validates the parameters associated to the step
func (ts *TestStep) ValidateParameters(_ xcontext.Context, stepParams test.TestStepParameters) error {
return ts.populateParams(stepParams)
}

// New initializes and returns a new exec step.
func New() test.TestStep {
return &TestStep{}
}

// Load returns the name, factory and events which are needed to register the step.
func Load() (string, test.TestStepFactory, []event.Name) {
return Name, New, events.Events
}
33 changes: 33 additions & 0 deletions plugins/teststeps/binarly/output.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package binarly

import (
"fmt"
"strings"
"time"
)

// Name returns the name of the Step
func (ts TestStep) Name() string {
return Name
}

// Function to format teststep information and append it to a string builder.
func (ts TestStep) writeTestStep(builders ...*strings.Builder) {
for _, builder := range builders {

builder.WriteString("\n")

builder.WriteString(" Parameter:\n")
builder.WriteString(" Token: *hidden*\n")
builder.WriteString(fmt.Sprintf(" File: %s\n", ts.parameters.File))
builder.WriteString("\n")

builder.WriteString(" Options:\n")
builder.WriteString(fmt.Sprintf(" Timeout: %s\n", time.Duration(ts.options.Timeout)))
builder.WriteString("\n")

builder.WriteString("Default Values:\n")
builder.WriteString(fmt.Sprintf(" Timeout: %s\n", defaultTimeout))
builder.WriteString("\n\n")
}
}
171 changes: 171 additions & 0 deletions plugins/teststeps/binarly/runner.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
package binarly

import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"mime/multipart"
"net/http"
"os"
"path/filepath"
"strings"
"time"

"github.com/linuxboot/contest/pkg/event/testevent"
"github.com/linuxboot/contest/pkg/events"
"github.com/linuxboot/contest/pkg/target"
"github.com/linuxboot/contest/pkg/xcontext"
"github.com/linuxboot/contest/plugins/teststeps/abstraction/options"
)

type Error struct {
Msg string `json:"error"`
}

type TargetRunner struct {
ts *TestStep
ev testevent.Emitter
}

func NewTargetRunner(ts *TestStep, ev testevent.Emitter) *TargetRunner {
return &TargetRunner{
ts: ts,
ev: ev,
}
}

type StartScanResponse struct {
ID string `json:"id"`
Status string `json:"status"`
}

func (r *TargetRunner) Run(ctx xcontext.Context, target *target.Target) error {
var outputBuf strings.Builder

ctx, cancel := options.NewOptions(ctx, defaultTimeout, r.ts.options.Timeout)
defer cancel()

outputBuf.WriteString(fmt.Sprintf("Running test step %s\n", Name))

if r.ts.Token == "" {
err := fmt.Errorf("you need to provide a token")
outputBuf.WriteString(fmt.Sprintf("%v", err))

return events.EmitError(ctx, outputBuf.String(), target, r.ev)
}

client := &http.Client{}

id, err := postFileToAPI(ctx, client, r.ts.File, r.ts.Token)
if err != nil {
outputBuf.WriteString(fmt.Sprintf("Failed to post file to binarly: %v", err))

return events.EmitError(ctx, outputBuf.String(), target, r.ev)
}

result, err := awaitResult(ctx, client, id, r.ts.Token)
if err != nil {
outputBuf.WriteString(fmt.Sprintf("Failed to get results from binarly: %v", err))

return events.EmitError(ctx, outputBuf.String(), target, r.ev)
}

var data json.RawMessage
events.EmitOuput(ctx, "binarly", data, target, r.ev)

events.EmitLog(ctx, outputBuf.String(), target, r.ev)

events.EmitLog(ctx, outputBuf.String(), target, r.ev)

return nil
}

func postFileToAPI(ctx context.Context, client *http.Client, filePath string, token string) (string, error) {
// Prepare the file to be uploaded
file, err := os.Open(filePath)
if err != nil {
return "", fmt.Errorf("failed to open file: %v", err)
}
defer file.Close()

// Create a new multipart form request
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
part, err := writer.CreateFormFile("file", filepath.Base(file.Name()))
if err != nil {
return "", fmt.Errorf("failed to create form file: %v", err)
}
_, err = io.Copy(part, file)
if err != nil {
return "", fmt.Errorf("failed to copy file: %v", err)
}
writer.Close()

req, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://risk-hunt-production-serve-y6yijivpnq-uc.a.run.app/scan", body)
if err != nil {
return "", fmt.Errorf("failed to create request: %v", err)
}

req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", writer.FormDataContentType())

resp, err := client.Do(req)
if err != nil {
return "", fmt.Errorf("failed to send request: %v", err)
}
defer resp.Body.Close()

var response StartScanResponse
json.NewDecoder(resp.Body).Decode(&response)
if err = json.NewDecoder(resp.Body).Decode(&response); err != nil {
return "", fmt.Errorf("failed to decode response: %v", err)
}

if response.Status != "ok" {
return "", fmt.Errorf("failed to start scan: %v", response.Status)
}

return response.ID, nil
}

type ScanStatusResponse struct {
FileName string `json:"file_name"`
Scan json.RawMessage `json:"scan"`
Ratings json.RawMessage `json:"ratings"`
Status string `json:"status"`
UploadTime string `json:"upload_time"`
}

func awaitResult(ctx context.Context, client *http.Client, id string, token string) (json.RawMessage, error) {
var statusResponse ScanStatusResponse

req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("https://risk-hunt-production-serve-y6yijivpnq-uc.a.run.app/scan/%s", id), nil)
if err != nil {
return json.RawMessage{}, fmt.Errorf("failed to create request: %v", err)
}
req.Header.Set("Authorization", "Bearer "+token)

for {
select {
case <-ctx.Done():
return json.RawMessage{}, fmt.Errorf("timed out waiting for results")
case <-time.After(30 * time.Second):
resp, err := client.Do(req)
if err != nil {
return json.RawMessage{}, fmt.Errorf("failed to send request: %v", err)
}
defer resp.Body.Close()

if err = json.NewDecoder(resp.Body).Decode(&statusResponse); err != nil {
return json.RawMessage{}, fmt.Errorf("failed to decode response: %v", err)
}

// Check the status
if statusResponse.Status == "ok" {
return statusResponse.Scan, nil
}
}
}
}

0 comments on commit a2452e8

Please sign in to comment.