You've already forked sap-jenkins-library
mirror of
https://github.com/SAP/jenkins-library.git
synced 2025-09-16 09:26:22 +02:00
feat(sonar): add hotspot security report (#5365)
Co-authored-by: Yuriy.Tereshchuk <astro.lutsk.aa@gmail.com>
This commit is contained in:
committed by
GitHub
parent
15492ee833
commit
fcd167b2e6
@@ -250,9 +250,25 @@ func runSonar(config sonarExecuteScanOptions, client piperhttp.Downloader, runne
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = getStaticCodeCheckResults(config, &taskReport, serverUrl, influx, apiClient)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = getHotSpotSecurityCheckResults(config, &taskReport, serverUrl, apiClient)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getStaticCodeCheckResults(config sonarExecuteScanOptions, taskReport *SonarUtils.TaskReportData, serverUrl string, influx *sonarExecuteScanInflux, apiClient SonarUtils.Sender) error {
|
||||
// fetch number of issues by severity
|
||||
issueService := SonarUtils.NewIssuesService(serverUrl, config.Token, taskReport.ProjectKey, config.Organization, config.BranchName, config.ChangeID, apiClient)
|
||||
var categories []SonarUtils.Severity
|
||||
var err error
|
||||
influx.sonarqube_data.fields.blocker_issues, err = issueService.GetNumberOfBlockerIssues(&categories)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -274,7 +290,7 @@ func runSonar(config sonarExecuteScanOptions, client piperhttp.Downloader, runne
|
||||
return err
|
||||
}
|
||||
|
||||
reportData := SonarUtils.ReportData{
|
||||
reportData := SonarUtils.ReportCodeCheckData{
|
||||
ServerURL: taskReport.ServerURL,
|
||||
ProjectKey: taskReport.ProjectKey,
|
||||
TaskID: taskReport.TaskID,
|
||||
@@ -282,7 +298,7 @@ func runSonar(config sonarExecuteScanOptions, client piperhttp.Downloader, runne
|
||||
BranchName: config.BranchName,
|
||||
Organization: config.Organization,
|
||||
Errors: categories[:],
|
||||
NumberOfIssues: SonarUtils.Issues{
|
||||
NumberOfIssues: &SonarUtils.Issues{
|
||||
Blocker: influx.sonarqube_data.fields.blocker_issues,
|
||||
Critical: influx.sonarqube_data.fields.critical_issues,
|
||||
Major: influx.sonarqube_data.fields.major_issues,
|
||||
@@ -307,12 +323,29 @@ func runSonar(config sonarExecuteScanOptions, client piperhttp.Downloader, runne
|
||||
|
||||
log.Entry().Debugf("Influx values: %v", influx.sonarqube_data.fields)
|
||||
|
||||
err = SonarUtils.WriteReport(reportData, sonar.workingDir, os.WriteFile)
|
||||
return SonarUtils.WriteCodeCheckReport(reportData, sonar.workingDir, os.WriteFile)
|
||||
}
|
||||
|
||||
func getHotSpotSecurityCheckResults(config sonarExecuteScanOptions, taskReport *SonarUtils.TaskReportData, serverUrl string, apiClient SonarUtils.Sender) error {
|
||||
// fetch number of issues by severity
|
||||
issueService := SonarUtils.NewIssuesService(serverUrl, config.Token, taskReport.ProjectKey, config.Organization, config.BranchName, config.ChangeID, apiClient)
|
||||
var hotspotissues []SonarUtils.SecurityHotspot
|
||||
err := issueService.GetHotSpotSecurityIssues(&hotspotissues)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
|
||||
reportData := SonarUtils.ReportHotSpotData{
|
||||
ServerURL: taskReport.ServerURL,
|
||||
ProjectKey: taskReport.ProjectKey,
|
||||
TaskID: taskReport.TaskID,
|
||||
ChangeID: config.ChangeID,
|
||||
BranchName: config.BranchName,
|
||||
Organization: config.Organization,
|
||||
SecurityHotspots: hotspotissues[:],
|
||||
}
|
||||
|
||||
return SonarUtils.WriteHotSpotReport(reportData, sonar.workingDir, os.WriteFile)
|
||||
}
|
||||
|
||||
// isInOptions returns true, if the given property is already provided in config.Options.
|
||||
|
@@ -153,6 +153,7 @@ func TestRunSonar(t *testing.T) {
|
||||
// add response handler
|
||||
httpmock.RegisterResponder(http.MethodGet, sonarServerURL+"/api/"+SonarUtils.EndpointCeTask+"", httpmock.NewStringResponder(http.StatusOK, `{ "task": { "componentId": "AXERR2JBbm9IiM5TEST", "status": "SUCCESS" }}`))
|
||||
httpmock.RegisterResponder(http.MethodGet, sonarServerURL+"/api/"+SonarUtils.EndpointIssuesSearch+"", httpmock.NewStringResponder(http.StatusOK, `{ "total": 0 }`))
|
||||
httpmock.RegisterResponder(http.MethodGet, sonarServerURL+"/api/"+SonarUtils.EndpointHotSpotsSearch+"", httpmock.NewStringResponder(http.StatusOK, `{ "total": 0 }`))
|
||||
httpmock.RegisterResponder(http.MethodGet, sonarServerURL+"/api/"+SonarUtils.EndpointMeasuresComponent+"", httpmock.NewStringResponder(http.StatusOK, measuresComponentResponse))
|
||||
|
||||
t.Run("default", func(t *testing.T) {
|
||||
|
@@ -12,6 +12,9 @@ import (
|
||||
// EndpointIssuesSearch API endpoint for https://sonarcloud.io/web_api/api/issues/search
|
||||
const EndpointIssuesSearch = "issues/search"
|
||||
|
||||
// EndpointHotSpotSearch API endpoint for https://sonarcloud.io/web_api/api/hotspots/search
|
||||
const EndpointHotSpotsSearch = "hotspots/search"
|
||||
|
||||
// IssueService ...
|
||||
type IssueService struct {
|
||||
Organization string
|
||||
@@ -50,6 +53,35 @@ func (service *IssueService) SearchIssues(options *IssuesSearchOption) (*sonargo
|
||||
return result, response, nil
|
||||
}
|
||||
|
||||
// SearchIssues ...
|
||||
func (service *IssueService) SearchHotSpots(options *HotSpotSearchOption) (*HotSpotSearchObject, *http.Response, error) {
|
||||
request, err := service.apiClient.create("GET", EndpointHotSpotsSearch, options)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
// use custom HTTP client to send request
|
||||
response, err := service.apiClient.send(request)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
// reuse response verrification from sonargo
|
||||
err = sonargo.CheckResponse(response)
|
||||
if err != nil {
|
||||
return nil, response, err
|
||||
}
|
||||
|
||||
// log response
|
||||
log.Entry().Debugf("HTTP Response: %v", func() string { rsp, _ := httputil.DumpResponse(response, true); return string(rsp) }())
|
||||
|
||||
// decode JSON response
|
||||
result := new(HotSpotSearchObject)
|
||||
err = service.apiClient.decode(response, result)
|
||||
if err != nil {
|
||||
return nil, response, err
|
||||
}
|
||||
return result, response, nil
|
||||
}
|
||||
|
||||
func (service *IssueService) getIssueCount(severity issueSeverity, categories *[]Severity) (int, error) {
|
||||
options := &IssuesSearchOption{
|
||||
ComponentKeys: service.Project,
|
||||
@@ -114,6 +146,34 @@ func (service *IssueService) GetNumberOfInfoIssues(categories *[]Severity) (int,
|
||||
return service.getIssueCount(info, categories)
|
||||
}
|
||||
|
||||
func (service *IssueService) GetHotSpotSecurityIssues(securityHotspots *[]SecurityHotspot) error {
|
||||
options := &HotSpotSearchOption{
|
||||
Project: service.Project,
|
||||
Status: to_review,
|
||||
}
|
||||
result, _, err := service.SearchHotSpots(options)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to fetch the numer of hotspots.")
|
||||
}
|
||||
|
||||
table := map[string]int{}
|
||||
service.updateHotSpotTypesTable(&result.HotSpots, table)
|
||||
for priority, hotspots := range table {
|
||||
var hotspot SecurityHotspot
|
||||
hotspot.Priority = priority
|
||||
hotspot.Hotspots = hotspots
|
||||
*securityHotspots = append(*securityHotspots, hotspot)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (service *IssueService) updateHotSpotTypesTable(issues *[]HotSpot, table map[string]int) {
|
||||
for _, issue := range *issues {
|
||||
table[issue.VulnerabilityProbability]++
|
||||
}
|
||||
delete(table, "") // remove undefined key if any exists in response
|
||||
}
|
||||
|
||||
// NewIssuesService returns a new instance of a service for the issues API endpoint.
|
||||
func NewIssuesService(host, token, project, organization, branch, pullRequest string, client Sender) *IssueService {
|
||||
return &IssueService{
|
||||
|
@@ -332,3 +332,213 @@ const responseIssueSearchBug = `{
|
||||
],
|
||||
"facets": []
|
||||
}`
|
||||
|
||||
func TestHotSpotService(t *testing.T) {
|
||||
testURL := "https://example.org"
|
||||
t.Run("success", func(t *testing.T) {
|
||||
httpmock.Activate()
|
||||
defer httpmock.DeactivateAndReset()
|
||||
|
||||
sender := &piperhttp.Client{}
|
||||
sender.SetOptions(piperhttp.ClientOptions{MaxRetries: -1, UseDefaultTransport: true})
|
||||
// add response handler
|
||||
httpmock.RegisterResponder(http.MethodGet, testURL+"/api/"+EndpointHotSpotsSearch+"", httpmock.NewStringResponder(http.StatusOK, responseHotSpotSearchMedium))
|
||||
// create service instance
|
||||
serviceUnderTest := NewIssuesService(testURL, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, sender)
|
||||
// Severities
|
||||
var hotspots []SecurityHotspot
|
||||
// test
|
||||
err := serviceUnderTest.GetHotSpotSecurityIssues(&hotspots)
|
||||
// assert
|
||||
assert.Equal(t, []SecurityHotspot{{Priority: "MEDIUM", Hotspots: 1}}, hotspots)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, httpmock.GetTotalCallCount(), "unexpected number of requests")
|
||||
})
|
||||
t.Run("error", func(t *testing.T) {
|
||||
httpmock.Activate()
|
||||
defer httpmock.DeactivateAndReset()
|
||||
|
||||
sender := &piperhttp.Client{}
|
||||
sender.SetOptions(piperhttp.ClientOptions{MaxRetries: -1, UseDefaultTransport: true})
|
||||
// add response handler
|
||||
httpmock.RegisterResponder(http.MethodGet, testURL+"/api/"+EndpointHotSpotsSearch+"", httpmock.NewStringResponder(http.StatusNotFound, responseHotSpotSearchError))
|
||||
// create service instance
|
||||
serviceUnderTest := NewIssuesService(testURL, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, sender)
|
||||
// Severities
|
||||
var hotspots []SecurityHotspot
|
||||
// test
|
||||
err := serviceUnderTest.GetHotSpotSecurityIssues(&hotspots)
|
||||
// assert
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, 1, httpmock.GetTotalCallCount(), "unexpected number of requests")
|
||||
})
|
||||
t.Run("multiple severities", func(t *testing.T) {
|
||||
httpmock.Activate()
|
||||
defer httpmock.DeactivateAndReset()
|
||||
|
||||
sender := &piperhttp.Client{}
|
||||
sender.SetOptions(piperhttp.ClientOptions{MaxRetries: -1, UseDefaultTransport: true})
|
||||
// add response handler
|
||||
httpmock.RegisterResponder(http.MethodGet, testURL+"/api/"+EndpointHotSpotsSearch+"", httpmock.NewStringResponder(http.StatusOK, responseHotSpotSearchMultiple))
|
||||
// create service instance
|
||||
serviceUnderTest := NewIssuesService(testURL, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, sender)
|
||||
// Severities
|
||||
// Severities
|
||||
var hotspots []SecurityHotspot
|
||||
// test
|
||||
err := serviceUnderTest.GetHotSpotSecurityIssues(&hotspots)
|
||||
// assert
|
||||
assert.Equal(t, []SecurityHotspot{
|
||||
{Priority: "MEDIUM", Hotspots: 2},
|
||||
{Priority: "LOW", Hotspots: 1},
|
||||
}, hotspots)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, httpmock.GetTotalCallCount(), "unexpected number of requests")
|
||||
})
|
||||
}
|
||||
|
||||
const responseHotSpotSearchMedium = `{
|
||||
"paging": {
|
||||
"pageIndex": 1,
|
||||
"pageSize": 100,
|
||||
"total": 1
|
||||
},
|
||||
"hotspots": [
|
||||
{
|
||||
"key": "d1502ebc-941a-4262-8081-04914834d75a",
|
||||
"component": "java-camera-viewer:build/reports/configuration-cache/cbcm7qev21o4weakgfsv12gen/9cfjq3uac12x26p527bjxudhn/configuration-cache-report.html",
|
||||
"project": "java-camera-viewer",
|
||||
"securityCategory": "weak-cryptography",
|
||||
"vulnerabilityProbability": "MEDIUM",
|
||||
"status": "TO_REVIEW",
|
||||
"line": 658,
|
||||
"message": "Make sure that using this pseudorandom number generator is safe here.",
|
||||
"author": "",
|
||||
"creationDate": "2025-03-31T18:16:09+0000",
|
||||
"updateDate": "2025-04-23T11:02:47+0000",
|
||||
"textRange": {
|
||||
"startLine": 658,
|
||||
"endLine": 658,
|
||||
"startOffset": 16799,
|
||||
"endOffset": 16812
|
||||
},
|
||||
"flows": [],
|
||||
"ruleKey": "javascript:S2245",
|
||||
"messageFormattings": []
|
||||
}
|
||||
],
|
||||
"components": [
|
||||
{
|
||||
"key": "java-camera-viewer:build/reports/configuration-cache/cbcm7qev21o4weakgfsv12gen/9cfjq3uac12x26p527bjxudhn/configuration-cache-report.html",
|
||||
"qualifier": "FIL",
|
||||
"name": "configuration-cache-report.html",
|
||||
"longName": "build/reports/configuration-cache/cbcm7qev21o4weakgfsv12gen/9cfjq3uac12x26p527bjxudhn/configuration-cache-report.html",
|
||||
"path": "build/reports/configuration-cache/cbcm7qev21o4weakgfsv12gen/9cfjq3uac12x26p527bjxudhn/configuration-cache-report.html"
|
||||
},
|
||||
{
|
||||
"key": "java-camera-viewer",
|
||||
"qualifier": "TRK",
|
||||
"name": "java-camera-viewer",
|
||||
"longName": "java-camera-viewer"
|
||||
}
|
||||
]
|
||||
}`
|
||||
|
||||
const responseHotSpotSearchError = `{
|
||||
"errors":[
|
||||
{
|
||||
"msg":"Project java-camera-viewer1 not found"
|
||||
}
|
||||
]
|
||||
}`
|
||||
|
||||
const responseHotSpotSearchMultiple = `{
|
||||
"paging": {
|
||||
"pageIndex": 1,
|
||||
"pageSize": 100,
|
||||
"total": 1
|
||||
},
|
||||
"hotspots": [
|
||||
{
|
||||
"key": "d1502ebc-941a-4262-8081-04914834d75a",
|
||||
"component": "java-camera-viewer:build/reports/configuration-cache/cbcm7qev21o4weakgfsv12gen/9cfjq3uac12x26p527bjxudhn/configuration-cache-report.html",
|
||||
"project": "java-camera-viewer",
|
||||
"securityCategory": "weak-cryptography",
|
||||
"vulnerabilityProbability": "MEDIUM",
|
||||
"status": "TO_REVIEW",
|
||||
"line": 658,
|
||||
"message": "Make sure that using this pseudorandom number generator is safe here.",
|
||||
"author": "",
|
||||
"creationDate": "2025-03-31T18:16:09+0000",
|
||||
"updateDate": "2025-04-23T11:02:47+0000",
|
||||
"textRange": {
|
||||
"startLine": 658,
|
||||
"endLine": 658,
|
||||
"startOffset": 16799,
|
||||
"endOffset": 16812
|
||||
},
|
||||
"flows": [],
|
||||
"ruleKey": "javascript:S2245",
|
||||
"messageFormattings": []
|
||||
},
|
||||
{
|
||||
"key": "d1502ebc-941a-4262-8081-04914834d75b",
|
||||
"component": "java-camera-viewer:build/reports/configuration-cache/cbcm7qev21o4weakgfsv12gen/9cfjq3uac12x26p527bjxudhn/configuration-cache-report.html",
|
||||
"project": "java-camera-viewer",
|
||||
"securityCategory": "weak-cryptography",
|
||||
"vulnerabilityProbability": "MEDIUM",
|
||||
"status": "TO_REVIEW",
|
||||
"line": 658,
|
||||
"message": "Make sure that using this pseudorandom number generator is safe here.",
|
||||
"author": "",
|
||||
"creationDate": "2025-03-31T18:16:09+0000",
|
||||
"updateDate": "2025-04-23T11:02:47+0000",
|
||||
"textRange": {
|
||||
"startLine": 658,
|
||||
"endLine": 658,
|
||||
"startOffset": 16799,
|
||||
"endOffset": 16812
|
||||
},
|
||||
"flows": [],
|
||||
"ruleKey": "javascript:S2245",
|
||||
"messageFormattings": []
|
||||
},
|
||||
{
|
||||
"key": "d1502ebc-941a-4262-8081-04914834d75c",
|
||||
"component": "java-camera-viewer:build/reports/configuration-cache/cbcm7qev21o4weakgfsv12gen/9cfjq3uac12x26p527bjxudhn/configuration-cache-report.html",
|
||||
"project": "java-camera-viewer",
|
||||
"securityCategory": "weak-cryptography",
|
||||
"vulnerabilityProbability": "LOW",
|
||||
"status": "TO_REVIEW",
|
||||
"line": 658,
|
||||
"message": "Make sure that using this pseudorandom number generator is safe here.",
|
||||
"author": "",
|
||||
"creationDate": "2025-03-31T18:16:09+0000",
|
||||
"updateDate": "2025-04-23T11:02:47+0000",
|
||||
"textRange": {
|
||||
"startLine": 658,
|
||||
"endLine": 658,
|
||||
"startOffset": 16799,
|
||||
"endOffset": 16812
|
||||
},
|
||||
"flows": [],
|
||||
"ruleKey": "javascript:S2245",
|
||||
"messageFormattings": []
|
||||
}
|
||||
],
|
||||
"components": [
|
||||
{
|
||||
"key": "java-camera-viewer:build/reports/configuration-cache/cbcm7qev21o4weakgfsv12gen/9cfjq3uac12x26p527bjxudhn/configuration-cache-report.html",
|
||||
"qualifier": "FIL",
|
||||
"name": "configuration-cache-report.html",
|
||||
"longName": "build/reports/configuration-cache/cbcm7qev21o4weakgfsv12gen/9cfjq3uac12x26p527bjxudhn/configuration-cache-report.html",
|
||||
"path": "build/reports/configuration-cache/cbcm7qev21o4weakgfsv12gen/9cfjq3uac12x26p527bjxudhn/configuration-cache-report.html"
|
||||
},
|
||||
{
|
||||
"key": "java-camera-viewer",
|
||||
"qualifier": "TRK",
|
||||
"name": "java-camera-viewer",
|
||||
"longName": "java-camera-viewer"
|
||||
}
|
||||
]
|
||||
}`
|
||||
|
@@ -6,22 +6,40 @@ import (
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
const reportFileName = "sonarscan.json"
|
||||
const reportCodeCheckFileName = "sonarscan.json"
|
||||
const reportHotSpotFileName = "hotspot.json"
|
||||
|
||||
// ReportData is representing the data of the step report JSON
|
||||
type ReportData struct {
|
||||
// ReportCodeCheckData is representing the data of the step report JSON
|
||||
type ReportCodeCheckData struct {
|
||||
ServerURL string `json:"serverUrl"`
|
||||
ProjectKey string `json:"projectKey"`
|
||||
TaskID string `json:"taskId"`
|
||||
ChangeID string `json:"changeID,omitempty"`
|
||||
BranchName string `json:"branchName,omitempty"`
|
||||
Organization string `json:"organization,omitempty"`
|
||||
NumberOfIssues Issues `json:"numberOfIssues"`
|
||||
NumberOfIssues *Issues `json:"numberOfIssues"`
|
||||
Errors []Severity `json:"errors"`
|
||||
Coverage *SonarCoverage `json:"coverage,omitempty"`
|
||||
LinesOfCode *SonarLinesOfCode `json:"linesOfCode,omitempty"`
|
||||
}
|
||||
|
||||
// ReportCodeCheckData is representing the data of the step report JSON
|
||||
type ReportHotSpotData struct {
|
||||
ServerURL string `json:"serverUrl"`
|
||||
ProjectKey string `json:"projectKey"`
|
||||
TaskID string `json:"taskId"`
|
||||
ChangeID string `json:"changeID,omitempty"`
|
||||
BranchName string `json:"branchName,omitempty"`
|
||||
Organization string `json:"organization,omitempty"`
|
||||
SecurityHotspots []SecurityHotspot `json:"securityHotspots"`
|
||||
}
|
||||
|
||||
// HotSpot Security Issues
|
||||
type SecurityHotspot struct {
|
||||
Priority string `json:"priority"`
|
||||
Hotspots int `json:"hotspots"`
|
||||
}
|
||||
|
||||
// Issues ...
|
||||
type Issues struct {
|
||||
Blocker int `json:"blocker"`
|
||||
@@ -38,10 +56,18 @@ type Severity struct {
|
||||
}
|
||||
|
||||
// WriteReport ...
|
||||
func WriteReport(data ReportData, reportPath string, writeToFile func(f string, d []byte, p os.FileMode) error) error {
|
||||
func WriteCodeCheckReport(data ReportCodeCheckData, reportPath string, writeToFile func(f string, d []byte, p os.FileMode) error) error {
|
||||
jsonData, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return writeToFile(filepath.Join(reportPath, reportFileName), jsonData, 0644)
|
||||
return writeToFile(filepath.Join(reportPath, reportCodeCheckFileName), jsonData, 0644)
|
||||
}
|
||||
|
||||
func WriteHotSpotReport(data ReportHotSpotData, reportPath string, writeToFile func(f string, d []byte, p os.FileMode) error) error {
|
||||
jsonData, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return writeToFile(filepath.Join(reportPath, reportHotSpotFileName), jsonData, 0644)
|
||||
}
|
||||
|
@@ -20,10 +20,10 @@ func writeToFileMock(f string, d []byte, p os.FileMode) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestWriteReport(t *testing.T) {
|
||||
func TestWriteCodeCheckReport(t *testing.T) {
|
||||
// init
|
||||
const expected = `{"serverUrl":"https://sonarcloud.io","projectKey":"Piper-Validation/Golang","taskId":"mock.Anything","numberOfIssues":{"blocker":0,"critical":1,"major":2,"minor":3,"info":4},"errors":[{"severity":"CRITICAL","error_type":"CODE_SMELL","issues":10}],"coverage":{"coverage":13.7,"lineCoverage":37.1,"linesToCover":123,"uncoveredLines":23,"branchCoverage":42,"branchesToCover":30,"uncoveredBranches":3},"linesOfCode":{"total":327,"languageDistribution":[{"languageKey":"java","linesOfCode":327}]}}`
|
||||
testData := ReportData{
|
||||
testData := ReportCodeCheckData{
|
||||
ServerURL: "https://sonarcloud.io",
|
||||
ProjectKey: "Piper-Validation/Golang",
|
||||
TaskID: mock.Anything,
|
||||
@@ -34,7 +34,7 @@ func TestWriteReport(t *testing.T) {
|
||||
IssueCount: 10,
|
||||
},
|
||||
},
|
||||
NumberOfIssues: Issues{
|
||||
NumberOfIssues: &Issues{
|
||||
Critical: 1,
|
||||
Major: 2,
|
||||
Minor: 3,
|
||||
@@ -55,9 +55,29 @@ func TestWriteReport(t *testing.T) {
|
||||
},
|
||||
}
|
||||
// test
|
||||
err := WriteReport(testData, "", writeToFileMock)
|
||||
err := WriteCodeCheckReport(testData, "", writeToFileMock)
|
||||
// assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expected, fileContent)
|
||||
assert.Equal(t, reportFileName, fileName)
|
||||
assert.Equal(t, reportCodeCheckFileName, fileName)
|
||||
}
|
||||
|
||||
func TestWriteHotSpotReport(t *testing.T) {
|
||||
// init
|
||||
const expected = `{"serverUrl":"https://sonarcloud.io","projectKey":"Piper-Validation/Golang","taskId":"mock.Anything","securityHotspots":[{"priority":"HIGH","hotspots":1},{"priority":"LOW","hotspots":4}]}`
|
||||
testData := ReportHotSpotData{
|
||||
ServerURL: "https://sonarcloud.io",
|
||||
ProjectKey: "Piper-Validation/Golang",
|
||||
TaskID: mock.Anything,
|
||||
SecurityHotspots: []SecurityHotspot{
|
||||
{Priority: "HIGH", Hotspots: 1},
|
||||
{Priority: "LOW", Hotspots: 4},
|
||||
},
|
||||
}
|
||||
// test
|
||||
err := WriteHotSpotReport(testData, "", writeToFileMock)
|
||||
// assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expected, fileContent)
|
||||
assert.Equal(t, reportHotSpotFileName, fileName)
|
||||
}
|
||||
|
@@ -35,6 +35,35 @@ type IssuesSearchOption struct {
|
||||
Types string `url:"types,omitempty"` // Description:"Comma-separated list of types.",ExampleValue:"CODE_SMELL,BUG"
|
||||
}
|
||||
|
||||
type HotSpotSearchOption struct {
|
||||
Project string `url:"project"` // Description:"Project name"
|
||||
Status hotSpotStatus `url:"status"` // Security issue review status (TO_REVIEW | REVIEWED)
|
||||
}
|
||||
|
||||
type HotSpotSearchObject struct {
|
||||
Paging interface{} `json:"paging"`
|
||||
HotSpots []HotSpot `json:"hotspots"`
|
||||
Components []interface{} `json:"components"`
|
||||
}
|
||||
|
||||
type HotSpot struct {
|
||||
Key string `json:"key"`
|
||||
Component string `json:"component"`
|
||||
Project string `json:"project"`
|
||||
SecurityCategory string `json:"securityCategory"`
|
||||
VulnerabilityProbability string `json:"vulnerabilityProbability"`
|
||||
Status string `json:"status"`
|
||||
Line int `json:"line"`
|
||||
Message string `json:"message"`
|
||||
Author string `json:"author"`
|
||||
CreationDate string `json:"creationDate"`
|
||||
UpdateDate string `json:"updateDate"`
|
||||
TextRange interface{} `json:"textRange"`
|
||||
Flows []interface{} `json:"flows"`
|
||||
RuleKey string `json:"ruleKey"`
|
||||
MessageFormattings []interface{} `json:"messageFormattings"`
|
||||
}
|
||||
|
||||
// MeasuresComponentOption is a copy from magicsong/sonargo plus the "internal" field branch.
|
||||
type MeasuresComponentOption struct {
|
||||
Branch string `url:"branch,omitempty"` // Description:"Branch key"
|
||||
@@ -59,3 +88,14 @@ const (
|
||||
minor issueSeverity = "MINOR"
|
||||
info issueSeverity = "INFO"
|
||||
)
|
||||
|
||||
type hotSpotStatus string
|
||||
|
||||
func (s hotSpotStatus) ToString() string {
|
||||
return string(s)
|
||||
}
|
||||
|
||||
const (
|
||||
reviwed hotSpotStatus = "REVIEWED"
|
||||
to_review hotSpotStatus = "TO_REVIEW"
|
||||
)
|
||||
|
Reference in New Issue
Block a user