Skip to content

Commit

Permalink
Merge pull request #8 from liatrio/ENG-1974
Browse files Browse the repository at this point in the history
Eng 1974
  • Loading branch information
aalsabag authored Nov 12, 2020
2 parents d2976b2 + 108531d commit cd7b8f2
Show file tree
Hide file tree
Showing 9 changed files with 500 additions and 100 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,5 @@
# vendor/

.vscode/

listener/coverage.txt
20 changes: 12 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,15 @@

Collects build metadata from SonarQube to be used to validate automated governance policies. When the collector starts it adds a webhook to SonarQube. It then responds to SonarQube events and sends metadata to a central [Rode Collector](https://github.com/liatrio/rode-collector-service) which stores the metadata in a Grafeas Occurrence.

## Arguments
| Argument | Environment Variables | Description | Default | Required |
|----------|-----------------------|-------------|---------|----------|
| -url | URL | Collector URL for SonarQube to send events to | | [x] |
| -sonar-url | SONAR_URL | SonarQube URL | http://localhost:9000 | [x] |
| -sonar-username | SONAR_USERNAME | Username to authenticate with SonarQube | admin | if token not set |
| -sonar-password | SONAR_PASSWORD | Password to authenticate with SonarQube | admin | if token not set |
| -sonar-token | SONAR_TOKEN | Token to authenticate with SonarQube | | overrides username and password |
## Using the Sonarqube Collector
If Sonarqube instance being pointed to is the community edition, an additional step must be followed when executing the sonar scan. This step allows the collector to determine what resource URI should be used.

A command line parameter can be passed in like so, indicating the git url of the project
```
-Dsonar.analysis.resourceUriPrefix=https://github.com/liatrio/springtrader-marketsummary-java
```

It can also be passed into your sonar.properties the same way or your gradle.properties like so:
```
systemProp.sonar.analysis.resourceUriPrefix=https://github.com/liatrio/springtrader-marketsummary-java
```
16 changes: 13 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
module liatr.io/rode-collector-sonarqube
module github.com/liatrio/rode-collector-sonarqube

go 1.15

require (
github.com/kr/pretty v0.1.0 // indirect
github.com/liatrio/rode-api v0.0.0-20201111165410-30d476656f6f
github.com/nxadm/tail v1.4.5 // indirect
github.com/onsi/ginkgo v1.14.2
github.com/onsi/gomega v1.10.3
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
github.com/pkg/errors v0.9.1 // indirect
go.uber.org/zap v1.16.0
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b // indirect
golang.org/x/sys v0.0.0-20201110211018-35f3e6cf4a65 // indirect
golang.org/x/text v0.3.4 // indirect
golang.org/x/tools v0.0.0-20200407143752-a3568bac92ae // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
google.golang.org/genproto v0.0.0-20201111145450-ac7456db90a6 // indirect
google.golang.org/grpc v1.33.2
google.golang.org/protobuf v1.25.0
)
125 changes: 125 additions & 0 deletions go.sum

Large diffs are not rendered by default.

148 changes: 112 additions & 36 deletions listener/listener.go
Original file line number Diff line number Diff line change
@@ -1,62 +1,138 @@
package listener

import (
"context"
"encoding/json"
"fmt"
"log"
"net/http"
)
"time"

// Event is...
type Event struct {
TaskID string `json:"taskid"`
Status string `json:"status"`
AnalyzedAt string `json:"analyzedat"`
GitCommit string `json:"revision"`
Project *Project `json:"project"`
QualityGate *QualityGate `json:"qualityGate"`
}
"github.com/liatrio/rode-collector-sonarqube/sonar"
"go.uber.org/zap"

// Project is
type Project struct {
Key string `json:"key"`
Name string `json:"name"`
URL string `json:"url"`
pb "github.com/liatrio/rode-api/proto/v1alpha1"
"github.com/liatrio/rode-api/protodeps/grafeas/proto/v1beta1/common_go_proto"
"github.com/liatrio/rode-api/protodeps/grafeas/proto/v1beta1/grafeas_go_proto"
"github.com/liatrio/rode-api/protodeps/grafeas/proto/v1beta1/package_go_proto"
"github.com/liatrio/rode-api/protodeps/grafeas/proto/v1beta1/vulnerability_go_proto"
"google.golang.org/protobuf/types/known/timestamppb"
)

type listener struct {
rodeClient pb.RodeClient
logger *zap.Logger
}

// QualityGate is...
type QualityGate struct {
Conditions []*Condition `json:"conditions"`
Name string `json:"name"`
Status string `json:"status"`
type Listener interface {
ProcessEvent(http.ResponseWriter, *http.Request)
}

// Condition is...
type Condition struct {
ErrorThreshold string `json:"errorThreshold"`
Metric string `json:"metric"`
OnLeakPeriod bool `json:"onLeakPeriod"`
Operator string `json:"operator"`
Status string `json:"status"`
func NewListener(logger *zap.Logger, client pb.RodeClient) Listener {
return &listener{
rodeClient: client,
logger: logger,
}
}

// ProcessEvent handles incoming webhook events
func ProcessEvent(w http.ResponseWriter, request *http.Request) {
log.Print("Received SonarQube event")
func (l *listener) ProcessEvent(w http.ResponseWriter, request *http.Request) {
log := l.logger.Named("ProcessEvent")

event := &Event{}
event := &sonar.Event{}
if err := json.NewDecoder(request.Body).Decode(event); err != nil {
w.WriteHeader(500)
fmt.Fprintf(w, "Error reading webhook event: %s", err)
fmt.Fprintf(w, "error reading webhook event")
log.Error("error reading webhook event", zap.NamedError("error", err))
return
}

log.Printf("SonarQube Event Payload: [%+v]", event)
log.Printf("SonarQube Event Project: [%+v]", event.Project)
log.Printf("SonarQube Event Quality Gate: [%+v]", event.QualityGate)
log.Debug("received sonarqube event", zap.Any("event", event), zap.Any("project", event.Project), zap.Any("qualityGate", event.QualityGate))

repo := getRepoFromSonar(event)

var occurrences []*grafeas_go_proto.Occurrence
for _, condition := range event.QualityGate.Conditions {
log.Printf("SonarQube Event Quality Gate Condition: [%+v]", condition)
log.Debug("sonarqube event quality gate condition", zap.Any("condition", condition))
occurrence := createQualityGateOccurrence(condition, repo)
occurrences = append(occurrences, occurrence)
}

ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
response, err := l.rodeClient.BatchCreateOccurrences(ctx, &pb.BatchCreateOccurrencesRequest{
Occurrences: occurrences,
})
if err != nil {
log.Error("error sending occurrences to rode", zap.NamedError("error", err))
w.WriteHeader(500)
return
}

log.Debug("response payload", zap.Any("response", response.GetOccurrences()))
w.WriteHeader(200)
}

func getRepoFromSonar(event *sonar.Event) string {
/*
// Need to add logic to check if they are using developer or enterprise edition, but current API
// only exposes this to admin users. Getting the resource URI is easier in the developer edition and is
// not dependent on a value passed in from the project. It can be done like this:
if isNotCommunityEdition(){
repoString := fmt.Sprintf("%s:%s",event.Branch.URL,event.GitCommit)
return repoString
}
*/

repoString := fmt.Sprintf("%s:%s", event.Properties["sonar.analysis.resourceUriPrefix"], event.GitCommit)
return repoString
}

func createQualityGateOccurrence(condition *sonar.Condition, repo string) *grafeas_go_proto.Occurrence {
occurrence := &grafeas_go_proto.Occurrence{
Name: condition.Metric,
Resource: &grafeas_go_proto.Resource{
Name: repo,
Uri: repo,
},
NoteName: "projects/notes_project/notes/sonarqube",
Kind: common_go_proto.NoteKind_NOTE_KIND_UNSPECIFIED,
Remediation: "test",
CreateTime: timestamppb.Now(),
// To be changed when a proper occurrence type is determined
Details: &grafeas_go_proto.Occurrence_Vulnerability{
Vulnerability: &vulnerability_go_proto.Details{
Type: "test",
Severity: vulnerability_go_proto.Severity_CRITICAL,
ShortDescription: "abc",
LongDescription: "abc123",
RelatedUrls: []*common_go_proto.RelatedUrl{
{
Url: "test",
Label: "test",
},
{
Url: "test",
Label: "test",
},
},
EffectiveSeverity: vulnerability_go_proto.Severity_CRITICAL,
PackageIssue: []*vulnerability_go_proto.PackageIssue{
{
SeverityName: "test",
AffectedLocation: &vulnerability_go_proto.VulnerabilityLocation{
CpeUri: "test",
Package: "test",
Version: &package_go_proto.Version{
Name: "test",
Revision: "test",
Epoch: 35,
Kind: package_go_proto.Version_MINIMUM,
},
},
},
},
},
},
}
return occurrence
}
30 changes: 29 additions & 1 deletion listener/listener_suite_test.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,44 @@
package listener_test
package listener

import (
"context"
"io/ioutil"
"log"
"testing"

pb "github.com/liatrio/rode-api/proto/v1alpha1"
"go.uber.org/zap"
"google.golang.org/grpc"

. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)

var logger *zap.Logger

func TestListener(t *testing.T) {
RegisterFailHandler(Fail)
log.SetOutput(ioutil.Discard)
RunSpecs(t, "Listener Suite")
}

var _ = BeforeSuite(func() {
logger, _ = zap.NewDevelopment()
})

type mockRodeClient struct {
receivedBatchCreateOccurrenceRequest *pb.BatchCreateOccurrencesRequest
preparedBatchCreateOccurrenceResponse *pb.BatchCreateOccurrencesResponse
expectedError error
}

func (m *mockRodeClient) BatchCreateOccurrences(ctx context.Context, in *pb.BatchCreateOccurrencesRequest, opts ...grpc.CallOption) (*pb.BatchCreateOccurrencesResponse, error) {
m.receivedBatchCreateOccurrenceRequest = in

// if we have a prepared response, send it. otherwise, return nil
if m.preparedBatchCreateOccurrenceResponse != nil {
return m.preparedBatchCreateOccurrenceResponse, m.expectedError
}

return nil, m.expectedError
}
Loading

0 comments on commit cd7b8f2

Please sign in to comment.