Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion connectors/defectdojo/const.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ const (
CreateEngagementPath = "/engagements/"
UpdateEngagementPath = "/engagements/%d/"
ImportScanPath = "/import-scan/"
ReimportScanPath = "/reimport-scan/"
GetTestsPath = "/tests/?engagement=%d&scan_type=%s"
GetFindingsPath = "/findings/?test__engagement=%d&active=true&offset=%d&limit=%d"
)

Expand Down Expand Up @@ -36,7 +38,9 @@ const (
logErrorUnknownType = "Unknow type [%s]"
logErrorAddFile = "Cannot attach file to request"
logErrorCreateMultipartRequest = "Cannot create multipart request"
logErrorImportScan = "Cannot import scan via %s (HTTP %d)"
logErrorImportScan = "Cannot import scan via %s (HTTP %d)"
logErrorReimportScan = "Cannot reimport scan via %s (HTTP %d)"
logErrorRetrieveTests = "Cannot retrieve tests for engagement ID %d"
logErrorRetrieveFindings = "Cannot retrieve findings for engagement ID %d"
)

Expand All @@ -51,5 +55,7 @@ const (
errUpdateEngagementEndDate = "cannot update engagement end date"
errWritingFile = "cannot write file to request"
errImportScan = "cannot import scan"
errReimportScan = "cannot reimport scan"
errRetrieveTests = "cannot retrieve tests"
errRetrieveFindings = "cannot retrieve findings"
)
11 changes: 11 additions & 0 deletions connectors/defectdojo/dto.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,16 @@ type GetFindingsResponse struct {
Results []Finding `json:"results"`
}

type Test struct {
Id int `json:"id"`
ScanType string `json:"scan_type"`
}

type GetTestsResponse struct {
Count int `json:"count"`
Results []Test `json:"results"`
}

type ScanPayload struct {
Timestamp string `json:"scan_date" form:"scan_date"`
SeverityThreshold string `json:"minimum_severity" form:"minimum_severity"`
Expand All @@ -77,4 +87,5 @@ type ScanPayload struct {
ScanType string `json:"scan_type" form:"scan_type"`
EngagementId int `json:"engagement" form:"engagement"`
CloseOldFinding bool `json:"close_old_findings_product_scope" form:"close_old_findings_product_scope"`
TestId int `json:"test,omitempty" form:"test"`
}
51 changes: 51 additions & 0 deletions connectors/defectdojo/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"fmt"
"mime/multipart"
"net/http"
"net/url"
"reflect"
"ScopeGuardian/connectors/defectdojo/client"
"ScopeGuardian/logger"
Expand All @@ -21,6 +22,8 @@ type DefectDojoService interface {
GetEngagements(productId uint, offset int, limit int, engagements []Engagement) ([]Engagement, error)
UpdateEngagementEndDate(engagementId, productId int, protected bool) (bool, error)
ImportScan(payload ScanPayload, filename string) (bool, error)
ReimportScan(payload ScanPayload, filename string) (bool, error)
GetTests(engagementId int, scanType string) ([]Test, error)
GetFindings(engagementId int, offset int, limit int, findings []Finding) ([]Finding, error)
SetAccessToken(token string)
SetURL(url string)
Expand Down Expand Up @@ -221,6 +224,51 @@ func (s *DefectDojoServiceImpl) ImportScan(payload ScanPayload, filename string)
return true, nil
}

// ReimportScan reimports a scan result file into an existing test in DefectDojo using the
// given ScanPayload. It finds the matching test by scan_type and engagement automatically.
// filename is used as the multipart form file name. Returns true on success.
func (s *DefectDojoServiceImpl) ReimportScan(payload ScanPayload, filename string) (bool, error) {
body, boundary, err := createMultipartFromScanPayload(payload, filename)
if err != nil {
logger.Error(logErrorCreateMultipartRequest)
return false, err
}

headers := s.client.GetHeaders(s.accessToken)
headers.Set(client.ContentTypeKey, boundary)

_, code := s.client.Post(fmt.Sprintf(
"%s%s%s", s.url, APIPrefix, ReimportScanPath), body, headers)

if code < http.StatusOK || code >= http.StatusMultipleChoices {
logger.Error(fmt.Sprintf(logErrorReimportScan, ReimportScanPath, code))
return false, errors.New(errReimportScan)
}

return true, nil
}

// GetTests retrieves all tests for the given engagement and scan type from DefectDojo.
// It returns the list of matching tests or an error if the request fails.
func (s *DefectDojoServiceImpl) GetTests(engagementId int, scanType string) ([]Test, error) {
var res GetTestsResponse

body, code := s.client.Get(fmt.Sprintf(
"%s%s%s", s.url, APIPrefix, fmt.Sprintf(GetTestsPath, engagementId, url.QueryEscape(scanType))), s.client.GetHeaders(s.accessToken))
if code != http.StatusOK {
logger.Error(fmt.Sprintf(logErrorRetrieveTests, engagementId))
return []Test{}, errors.New(errRetrieveTests)
}

err := json.Unmarshal(body, &res)
if err != nil {
logger.Error(fmt.Sprintf(logErrorDecodingToken, err.Error()))
return []Test{}, errors.New(errUnmarshal)
}

return res.Results, nil
}

// createMultipartFromScanPayload serialises a ScanPayload into a multipart/form-data
// body. Struct fields are mapped to form keys via the "form" struct tag. The raw
// scan file is appended under the "file" key using filename. It returns the body
Expand Down Expand Up @@ -252,6 +300,9 @@ func createMultipartFromScanPayload(payload ScanPayload, filename string) ([]byt
logger.Error(fmt.Sprintf(logErrorReflection, formKey))
}
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
if value.Int() == 0 {
continue
}
valStr := strconv.FormatInt(value.Int(), 10)
if err := writer.WriteField(formKey, valStr); err != nil {
logger.Error(fmt.Sprintf(logErrorReflection, formKey))
Expand Down
30 changes: 30 additions & 0 deletions connectors/defectdojo/service_mock.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

46 changes: 46 additions & 0 deletions connectors/defectdojo/service_mock_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,52 @@ func TestMockDefectDojoService_ImportScan_ReturnsError(t *testing.T) {
assert.False(t, ok)
}

func TestMockDefectDojoService_ReimportScan_Succeeds(t *testing.T) {
ctrl := gomock.NewController(t)
mock := NewMockDefectDojoService(ctrl)

mock.EXPECT().ReimportScan(gomock.Any(), "results.json").Return(true, nil)

ok, err := mock.ReimportScan(ScanPayload{}, "results.json")
assert.Nil(t, err)
assert.True(t, ok)
}

func TestMockDefectDojoService_ReimportScan_ReturnsError(t *testing.T) {
ctrl := gomock.NewController(t)
mock := NewMockDefectDojoService(ctrl)

mock.EXPECT().ReimportScan(gomock.Any(), gomock.Any()).Return(false, errors.New(errReimportScan))

ok, err := mock.ReimportScan(ScanPayload{}, "results.json")
assert.NotNil(t, err)
assert.False(t, ok)
}

func TestMockDefectDojoService_GetTests_Succeeds(t *testing.T) {
ctrl := gomock.NewController(t)
mock := NewMockDefectDojoService(ctrl)

expected := []Test{{Id: 5, ScanType: "KICS Scan"}}
mock.EXPECT().GetTests(42, "KICS Scan").Return(expected, nil)

tests, err := mock.GetTests(42, "KICS Scan")
assert.Nil(t, err)
assert.Len(t, tests, 1)
assert.Equal(t, 5, tests[0].Id)
}

func TestMockDefectDojoService_GetTests_ReturnsError(t *testing.T) {
ctrl := gomock.NewController(t)
mock := NewMockDefectDojoService(ctrl)

mock.EXPECT().GetTests(gomock.Any(), gomock.Any()).Return([]Test{}, errors.New(errRetrieveTests))

tests, err := mock.GetTests(42, "KICS Scan")
assert.NotNil(t, err)
assert.Empty(t, tests)
}

func TestMockDefectDojoService_SetAccessToken(t *testing.T) {
ctrl := gomock.NewController(t)
mock := NewMockDefectDojoService(ctrl)
Expand Down
163 changes: 163 additions & 0 deletions connectors/defectdojo/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,30 @@ func TestCreateMultipartFromScanPayload(t *testing.T) {

assert.Nil(t, err)
})

t.Run("Should omit test field when TestId is zero", func(t *testing.T) {
var payload ScanPayload
payload.EngagementId = 12
payload.File, _ = os.ReadFile("../../features/scans/kics/mocks/working_results/results/kics-results.json")
// TestId left at zero

body, _, err := createMultipartFromScanPayload(payload, "test.json")

assert.Nil(t, err)
assert.NotContains(t, string(body), "name=\"test\"")
})

t.Run("Should include test field when TestId is non-zero", func(t *testing.T) {
var payload ScanPayload
payload.EngagementId = 12
payload.TestId = 42
payload.File, _ = os.ReadFile("../../features/scans/kics/mocks/working_results/results/kics-results.json")

body, _, err := createMultipartFromScanPayload(payload, "test.json")

assert.Nil(t, err)
assert.Contains(t, string(body), "name=\"test\"")
})
}

func TestSetURL(t *testing.T) {
Expand All @@ -509,6 +533,145 @@ func TestSetAccessToken(t *testing.T) {
assert.Equal(t, "new-token-xyz", impl.accessToken)
}

func TestReimportScan(t *testing.T) {
gomockController := gomock.NewController(t)

t.Run("Should reimport scan with 201 Created", func(t *testing.T) {
clientMock := client.NewMockClient(gomockController)

clientMock.EXPECT().Post(gomock.Any(), gomock.Any(), gomock.Any()).Return([]byte(""), 201).AnyTimes()
clientMock.EXPECT().GetHeaders(gomock.Any()).Return(http.Header{}).AnyTimes()

service := newDefectDojoService(clientMock, URL, TOKEN)

ok, err := service.ReimportScan(ScanPayload{}, "../../features/scans/kics/mocks/working_results/results/kics-results.json")

assert.Nil(t, err)
assert.True(t, ok)
})

t.Run("Should reimport scan with 200 OK", func(t *testing.T) {
clientMock := client.NewMockClient(gomockController)

clientMock.EXPECT().Post(gomock.Any(), gomock.Any(), gomock.Any()).Return([]byte(""), 200).AnyTimes()
clientMock.EXPECT().GetHeaders(gomock.Any()).Return(http.Header{}).AnyTimes()

service := newDefectDojoService(clientMock, URL, TOKEN)

ok, err := service.ReimportScan(ScanPayload{}, "../../features/scans/kics/mocks/working_results/results/kics-results.json")

assert.Nil(t, err)
assert.True(t, ok)
})

t.Run("Should not reimport scan", func(t *testing.T) {
clientMock := client.NewMockClient(gomockController)

clientMock.EXPECT().Post(gomock.Any(), gomock.Any(), gomock.Any()).Return([]byte(""), 403).AnyTimes()
clientMock.EXPECT().GetHeaders(gomock.Any()).Return(http.Header{}).AnyTimes()

service := newDefectDojoService(clientMock, URL, TOKEN)

ok, err := service.ReimportScan(ScanPayload{}, "../../features/scans/kics/mocks/working_results/results/kics-results.json")

assert.NotNil(t, err)
assert.EqualValues(t, errReimportScan, err.Error())
assert.False(t, ok)
})
}

func TestGetTests(t *testing.T) {
gomockController := gomock.NewController(t)

t.Run("Should retrieve tests for an engagement and scan type", func(t *testing.T) {
clientMock := client.NewMockClient(gomockController)

responseReturnMock := []byte(`
{
"count": 1,
"results": [
{
"id": 5,
"scan_type": "KICS Scan"
}
]
}
`)

clientMock.EXPECT().GetHeaders(gomock.Any()).Return(http.Header{}).AnyTimes()
clientMock.EXPECT().Get(gomock.Any(), gomock.Any()).Return(responseReturnMock, 200)

service := newDefectDojoService(clientMock, URL, TOKEN)

tests, err := service.GetTests(42, "KICS Scan")

assert.Nil(t, err)
assert.EqualValues(t, 1, len(tests))
assert.EqualValues(t, 5, tests[0].Id)
assert.EqualValues(t, "KICS Scan", tests[0].ScanType)
})

t.Run("Should return empty slice when no tests exist", func(t *testing.T) {
clientMock := client.NewMockClient(gomockController)

responseReturnMock := []byte(`
{
"count": 0,
"results": []
}
`)

clientMock.EXPECT().GetHeaders(gomock.Any()).Return(http.Header{}).AnyTimes()
clientMock.EXPECT().Get(gomock.Any(), gomock.Any()).Return(responseReturnMock, 200)

service := newDefectDojoService(clientMock, URL, TOKEN)

tests, err := service.GetTests(42, "KICS Scan")

assert.Nil(t, err)
assert.EqualValues(t, 0, len(tests))
})

t.Run("Should not retrieve tests due to wrong HTTP status code", func(t *testing.T) {
clientMock := client.NewMockClient(gomockController)

clientMock.EXPECT().GetHeaders(gomock.Any()).Return(http.Header{}).AnyTimes()
clientMock.EXPECT().Get(gomock.Any(), gomock.Any()).Return([]byte(`{}`), 403)

service := newDefectDojoService(clientMock, URL, TOKEN)

tests, err := service.GetTests(42, "KICS Scan")

assert.NotNil(t, err)
assert.Equal(t, errRetrieveTests, err.Error())
assert.EqualValues(t, 0, len(tests))
})

t.Run("Should not retrieve tests due to wrong JSON object", func(t *testing.T) {
clientMock := client.NewMockClient(gomockController)

responseReturnMock := []byte(`
{
"count": 1,
"results": [
{
"id": 1
}
`)

clientMock.EXPECT().GetHeaders(gomock.Any()).Return(http.Header{}).AnyTimes()
clientMock.EXPECT().Get(gomock.Any(), gomock.Any()).Return(responseReturnMock, 200)

service := newDefectDojoService(clientMock, URL, TOKEN)

tests, err := service.GetTests(42, "KICS Scan")

assert.NotNil(t, err)
assert.Equal(t, errUnmarshal, err.Error())
assert.EqualValues(t, 0, len(tests))
})
}

func TestGetFindings(t *testing.T) {
gomockController := gomock.NewController(t)

Expand Down
Loading
Loading