diff --git a/sechub-cli/src/mercedes-benz.com/sechub/cli/false-positives.go b/sechub-cli/src/mercedes-benz.com/sechub/cli/false-positives.go index fd0e6e6b4c..5d8f46015c 100644 --- a/sechub-cli/src/mercedes-benz.com/sechub/cli/false-positives.go +++ b/sechub-cli/src/mercedes-benz.com/sechub/cli/false-positives.go @@ -8,11 +8,13 @@ import ( "io" "os" "path/filepath" + "slices" + "strings" sechubUtil "mercedes-benz.com/sechub/util" ) -// Keyword for false-posisitves json file +// Keyword for false-positives json file const falsePositivesListType = "falsePositiveJobDataList" // FalsePositivesList - structure for handling download of false-positive lists @@ -24,9 +26,10 @@ type FalsePositivesList struct { // FalsePositivesConfig - struct containing information for defining false-positives type FalsePositivesConfig struct { - APIVersion string `json:"apiVersion"` - Type string `json:"type"` - JobData []FalsePositivesJobData `json:"jobData"` + APIVersion string `json:"apiVersion"` + Type string `json:"type"` + JobData []FalsePositivesJobData `json:"jobData"` + ProjectData []FalsePositivesProjectData `json:"projectData"` } // FalsePositivesJobData - contains data related to a scan job for defining false-positives @@ -36,6 +39,20 @@ type FalsePositivesJobData struct { Comment string `json:"comment"` } +// FalsePositivesProjectData - contains data related to a project for defining false-positives +type FalsePositivesProjectData struct { + ID string `json:"id"` + Comment string `json:"comment"` + WebScan FalsePositivesProjectDataForWebScan `json:"webScan"` +} + +// FalsePositivesProjectDataForWebScan - contains the definition for false-positives in web scans +type FalsePositivesProjectDataForWebScan struct { + CweID int `json:"cweId"` + UrlPattern string `json:"urlPattern"` + Methods []string `json:"methods"` +} + // FalsePositivesDefinition - the struct that comes from SecHub server with getFalsePositives type FalsePositivesDefinition struct { Items []FalsePositiveDefinition `json:"falsePositives"` @@ -43,10 +60,11 @@ type FalsePositivesDefinition struct { // FalsePositiveDefinition - a single false-positive definition from server type FalsePositiveDefinition struct { - JobData FalsePositivesJobData `json:"jobData"` - Author string `json:"author"` - MetaData FalsePositiveDefinitionMetaData `json:"metaData"` - Created string `json:"created"` + JobData FalsePositivesJobData `json:"jobData"` + ProjectData FalsePositivesProjectData `json:"projectData"` + Author string `json:"author"` + MetaData FalsePositiveDefinitionMetaData `json:"metaData"` + Created string `json:"created"` } // FalsePositiveDefinitionMetaData - metadata part of FalsePositiveDefinition @@ -149,11 +167,11 @@ func readFileIntoContext(context *Context, fallbackFile string) { func defineFalsePositivesFromFile(context *Context) { readFileIntoContext(context, DefaultSecHubFalsePositivesJSONFile) - // read json into go struct + // Read json into go struct falsePositivesDefinitionList := newFalsePositivesListFromBytes(context.inputForContentProcessing) sechubUtil.LogDebug(context.config.debug, fmt.Sprintf("False positives to be defined: %+v", falsePositivesDefinitionList)) - // download false-positives list for project from SecHub server + // Download false-positives list for project from SecHub server jsonFPBlob := FalsePositivesList{serverResult: getFalsePositivesList(context), outputFolder: "", outputFileName: ""} var falsePositivesServerList FalsePositivesDefinition err := json.Unmarshal(jsonFPBlob.serverResult, &falsePositivesServerList) @@ -179,7 +197,7 @@ func defineFalsePositives(newFalsePositives FalsePositivesConfig, currentFalsePo falsePositivesToRemove.APIVersion = CurrentAPIVersion falsePositivesToRemove.Type = falsePositivesListType - // Loop through definition list and figure out, what to add and what to remove + // Loop through JobData definition list and figure out, what to add and what to remove for _, newFalsePositive := range newFalsePositives.JobData { matched := false for i, falsePositive := range currentFalsePositives { @@ -197,12 +215,38 @@ func defineFalsePositives(newFalsePositives FalsePositivesConfig, currentFalsePo } } + // Loop through ProjectData definition list and figure out, what to add and what to remove + for _, newFalsePositive := range newFalsePositives.ProjectData { + matched := false + for i, falsePositive := range currentFalsePositives { + if newFalsePositive.ID == falsePositive.ProjectData.ID { + // Remove item from list if ID exists + currentFalsePositives[i] = currentFalsePositives[len(currentFalsePositives)-1] // Copy last item to current position + currentFalsePositives = currentFalsePositives[:len(currentFalsePositives)-1] // Truncate slice + + // Compare alle elements to decide if an update is needed + if (newFalsePositive.Comment == falsePositive.ProjectData.Comment) && + (newFalsePositive.WebScan.CweID == falsePositive.ProjectData.WebScan.CweID) && + (newFalsePositive.WebScan.UrlPattern == falsePositive.ProjectData.WebScan.UrlPattern) && + slices.Equal(newFalsePositive.WebScan.Methods, falsePositive.ProjectData.WebScan.Methods) { + matched = true + } + break + } + } + if !matched { + // Add False positive to list (will be updated if it exists on the server) + falsePositivesToAdd.ProjectData = append(falsePositivesToAdd.ProjectData, newFalsePositive) + } + } + // currentFalsePositives now contains all false positives to remove for _, falsePositiveToBeRemoved := range currentFalsePositives { - var fp FalsePositivesJobData - fp.JobUUID = falsePositiveToBeRemoved.JobData.JobUUID - fp.FindingID = falsePositiveToBeRemoved.JobData.FindingID - falsePositivesToRemove.JobData = append(falsePositivesToRemove.JobData, fp) + if falsePositiveToBeRemoved.JobData.JobUUID != "" { + falsePositivesToRemove.JobData = append(falsePositivesToRemove.JobData, falsePositiveToBeRemoved.JobData) + } else if falsePositiveToBeRemoved.ProjectData.ID != "" { + falsePositivesToRemove.ProjectData = append(falsePositivesToRemove.ProjectData, falsePositiveToBeRemoved.ProjectData) + } } return falsePositivesToAdd, falsePositivesToRemove @@ -239,22 +283,35 @@ func uploadFalsePositives(context *Context) { // Read inputForContentProcessing into a JSON struct falsePositivesList := newFalsePositivesListFromBytes(context.inputForContentProcessing) - // Upload the list in chunks of maximal MaxChunkSizeFalsePositives items + // Upload the jobData list in chunks of maximal MaxChunkSizeFalsePositives items for i := 0; ; i++ { uploadChunk := getFalsePositivesUploadChunk(falsePositivesList, i) if len(uploadChunk.JobData) == 0 { break } - jsonBlob, err := json.Marshal(uploadChunk) - sechubUtil.HandleError(err, ExitCodeFailed) - context.inputForContentProcessing = jsonBlob - processContent(context) + uploadFalsePositivesChunk(context, uploadChunk) + } - // Send context.contentToSend to SecHub server - sendWithDefaultHeader("PUT", buildFalsePositivesAPICall(context), context) + // Upload fp projectData if present + if len(falsePositivesList.ProjectData) > 0 { + var falsePositivesProjectData FalsePositivesConfig + falsePositivesProjectData.APIVersion = falsePositivesList.APIVersion + falsePositivesProjectData.Type = falsePositivesList.Type + falsePositivesProjectData.ProjectData = falsePositivesList.ProjectData + uploadFalsePositivesChunk(context, falsePositivesProjectData) } } +func uploadFalsePositivesChunk(context *Context, uploadChunk FalsePositivesConfig) { + jsonBlob, err := json.Marshal(uploadChunk) + sechubUtil.HandleError(err, ExitCodeFailed) + context.inputForContentProcessing = jsonBlob + processContent(context) + + // Send context.contentToSend to SecHub server + sendWithDefaultHeader("PUT", buildFalsePositivesAPICall(context), context) +} + func unmarkFalsePositivesFromFile(context *Context) { sechubUtil.LogDebug(context.config.debug, fmt.Sprintf("Action %q: remove false positives - read from file: %s", context.config.action, context.config.file)) @@ -268,24 +325,40 @@ func unmarkFalsePositivesFromFile(context *Context) { } func unmarkFalsePositives(context *Context, list *FalsePositivesConfig) { - if len(list.JobData) == 0 { + if len(list.JobData) == 0 && len(list.ProjectData) == 0 { sechubUtil.Log("0 false-positives removed from project \""+context.config.projectID+"\"", context.config.quiet) return } - sechubUtil.Log("Removing as false-positives from project \""+context.config.projectID+"\":", context.config.quiet) - // Loop over list and push to SecHub server - // Url scheme: curl 'https://sechub.example.com/api/project/project1/false-positive/f1d02a9d-5e1b-4f52-99e5-401854ccf936/42' -i -X DELETE - urlPrefix := buildFalsePositiveAPICall(context) - // we don't want to send content here context.inputForContentProcessing = []byte(``) processContent(context) - for _, element := range list.JobData { - sechubUtil.Log(fmt.Sprintf("- JobUUID %s: Finding #%d", element.JobUUID, element.FindingID), context.config.quiet) - sendWithDefaultHeader("DELETE", fmt.Sprintf("%s/%s/%d", urlPrefix, element.JobUUID, element.FindingID), context) + sechubUtil.Log("Removing as false-positives from project \""+context.config.projectID+"\":", context.config.quiet) + // Loop over lists and push to SecHub server + + if len(list.JobData) > 0 { + // Iterate over JobData list: + // Url scheme: curl 'https://sechub.example.com/api/project/project1/false-positive/f1d02a9d-5e1b-4f52-99e5-401854ccf936/42' -i -X DELETE + urlPrefix := buildFalsePositiveAPICall(context) + + for _, element := range list.JobData { + sechubUtil.Log(fmt.Sprintf("- JobUUID %s: Finding #%d", element.JobUUID, element.FindingID), context.config.quiet) + sendWithDefaultHeader("DELETE", fmt.Sprintf("%s/%s/%d", urlPrefix, element.JobUUID, element.FindingID), context) + } + } + + if len(list.ProjectData) > 0 { + // Iterate over ProjectData list: + // Url scheme: curl 'https://sechub.example.com//api/project/project1/false-positive/project-data/fp-id-1' -i -X DELETE + urlPrefix := buildFalsePositiveProjectDataAPICall(context) + + for _, element := range list.ProjectData { + sechubUtil.Log(fmt.Sprintf("- project's false-positive-ID: \"%s\"", element.ID), context.config.quiet) + sendWithDefaultHeader("DELETE", fmt.Sprintf("%s/%s", urlPrefix, element.ID), context) + } } + sechubUtil.Log("Transfer completed", context.config.quiet) } @@ -353,16 +426,20 @@ func newFalsePositivesListFromConsole(context *Context) (list FalsePositivesConf } func markFalsePositives(context *Context, list *FalsePositivesConfig) { - if len(list.JobData) == 0 { + if len(list.JobData) == 0 && len(list.ProjectData) == 0 { sechubUtil.Log("0 false-positives added to project \""+context.config.projectID+"\"", context.config.quiet) return } - sechubUtil.Log("Adding as false-positives to project \""+context.config.projectID+"\":", context.config.quiet) + sechubUtil.Log("Adding/updating as false-positives in project \""+context.config.projectID+"\":", context.config.quiet) for _, element := range list.JobData { sechubUtil.Log(fmt.Sprintf("- JobUUID %s: Finding #%d, Comment: %s", element.JobUUID, element.FindingID, element.Comment), context.config.quiet) } + for _, element := range list.ProjectData { + sechubUtil.Log(fmt.Sprintf("- project's false-positive-ID: %s, Comment: %s", element.ID, element.Comment), context.config.quiet) + } + // upload to server jsonBlob, err := json.Marshal(list) sechubUtil.HandleError(err, ExitCodeFailed) @@ -390,7 +467,7 @@ func interactiveUnmarkFalsePositives(context *Context) { FalsePositivesList := newUnmarkFalsePositivesListFromConsole(context) sechubUtil.LogDebug(context.config.debug, fmt.Sprintf("False-positives unmark list for upload:\n%+v", FalsePositivesList)) - if len(FalsePositivesList.JobData) == 0 { + if len(FalsePositivesList.JobData) == 0 && len(FalsePositivesList.ProjectData) == 0 { sechubUtil.Log("No false positives to unmark.", context.config.quiet) return } @@ -413,8 +490,6 @@ func newUnmarkFalsePositivesListFromConsole(context *Context) (result FalsePosit sechubUtil.HandleError(err, ExitCodeFailed) sechubUtil.LogDebug(context.config.debug, fmt.Sprintf("Read from Server:\n%+v", list)) - // ToDo: sort report by severity,finding id - // iterate over entries and ask which to unmark var ExpectedInputs = []sechubUtil.ConsoleInputItem{ {Input: "y", ShortDescription: "Yes"}, @@ -429,8 +504,16 @@ func newUnmarkFalsePositivesListFromConsole(context *Context) (result FalsePosit sechubUtil.HandleError(err, ExitCodeFailed) if input == "y" { // append finding to list - var listEntry = FalsePositivesJobData{falsepositive.JobData.JobUUID, falsepositive.JobData.FindingID, ""} - result.JobData = append(result.JobData, listEntry) + if falsepositive.JobData.JobUUID != "" { + var listEntry = FalsePositivesJobData{ JobUUID: falsepositive.JobData.JobUUID, FindingID: falsepositive.JobData.FindingID } + result.JobData = append(result.JobData, listEntry) + } + + if falsepositive.ProjectData.ID != "" { + var listEntry = FalsePositivesProjectData{ ID: falsepositive.ProjectData.ID } + result.ProjectData = append(result.ProjectData, listEntry) + } + } else if input == "c" { os.Exit(ExitCodeOK) } else if input == "s" { @@ -442,21 +525,47 @@ func newUnmarkFalsePositivesListFromConsole(context *Context) (result FalsePosit } func printFalsePositiveDefinition(falsepositive *FalsePositiveDefinition) { - // Example output: - // ------------------------------------------------------------------ - // Creation of Temp File in Dir with Incorrect Permissions, codeScan severity: LOW - // Origin: Finding ID 3 in job f94d815c-7f69-48c3-8433-8f03d52ce32a - // File: java/com/mercedes-benz/sechub/docgen/kubernetes/KubernetesTemplateFilesGenerator.java - // Code: File secHubServer = new File("./sechub-server"); - // (Added by admin at 2020-07-10 13:41:06; comment: "Only temporary directory") - // ------------------------------------------------------------------ sechubUtil.PrintDashedLine() - fmt.Printf("%s, %s severity: %s\n", falsepositive.MetaData.Name, falsepositive.MetaData.ScanType, falsepositive.MetaData.Severity) - fmt.Printf(" Origin: Finding ID %d in job %s\n", falsepositive.JobData.FindingID, falsepositive.JobData.JobUUID) - // would be cool to have line and column in source code location - fmt.Printf(" File: %s\n", falsepositive.MetaData.Code.Start.Location) - fmt.Printf(" Code: %s\n", falsepositive.MetaData.Code.Start.SourceCode) - fmt.Printf("(Added by %s at %s; comment: %q)\n", falsepositive.Author, falsepositive.Created, falsepositive.JobData.Comment) - // added by name at date + + // Is of type JobData? + if falsepositive.JobData.JobUUID != "" { + // Example output: + // ------------------------------------------------------------------ + // Creation of Temp File in Dir with Incorrect Permissions, codeScan severity: LOW + // Origin: Finding ID 3 in job f94d815c-7f69-48c3-8433-8f03d52ce32a + // File: java/com/mercedes-benz/sechub/docgen/kubernetes/KubernetesTemplateFilesGenerator.java + // Code: File secHubServer = new File("./sechub-server"); + // (Added by admin at 2024-07-10 13:41:06; comment: "Only temporary directory") + // ------------------------------------------------------------------ + fmt.Printf("%s, %s severity: %s\n", falsepositive.MetaData.Name, falsepositive.MetaData.ScanType, falsepositive.MetaData.Severity) + fmt.Printf(" Origin: Finding ID %d in job %s\n", falsepositive.JobData.FindingID, falsepositive.JobData.JobUUID) + // would be cool to have line and column in source code location + if falsepositive.MetaData.Code.Start.Location != "" { + fmt.Printf(" File: %s\n", falsepositive.MetaData.Code.Start.Location) + fmt.Printf(" Code: %s\n", falsepositive.MetaData.Code.Start.SourceCode) + } + fmt.Printf("(Added by %s at %s; comment: %q)\n", falsepositive.Author, falsepositive.Created, falsepositive.JobData.Comment) + } + + // Is of type ProjectData? + if falsepositive.ProjectData.ID != "" { + // Example output: + // ------------------------------------------------------------------ + // Project's false-positive-ID: "my-fp-definition1" (logout url) + // urlPattern: https://myapp-*.example.com:80*/logout?* + // CWE-ID: 89, Methods: GET, PUT, POST + // (Added by admin at 2024-09-06 08:01:03) + // ------------------------------------------------------------------ + fmt.Printf("Project's false-positive-ID: %q (%s)\n", falsepositive.ProjectData.ID, falsepositive.ProjectData.Comment) + if falsepositive.ProjectData.WebScan.UrlPattern != "" { + fmt.Printf(" urlPattern: %s\n", falsepositive.ProjectData.WebScan.UrlPattern) + fmt.Printf(" CWE-ID: %d", falsepositive.ProjectData.WebScan.CweID) + if len (falsepositive.ProjectData.WebScan.Methods) > 0 { + fmt.Printf(", Methods: %s", strings.Join(falsepositive.ProjectData.WebScan.Methods, ", ")) + } + } + fmt.Printf("\n(Added by %s at %s)\n", falsepositive.Author, falsepositive.Created) + } + sechubUtil.PrintDashedLine() } diff --git a/sechub-cli/src/mercedes-benz.com/sechub/cli/false-positives_test.go b/sechub-cli/src/mercedes-benz.com/sechub/cli/false-positives_test.go index 54466ae651..65bdf8e2f6 100644 --- a/sechub-cli/src/mercedes-benz.com/sechub/cli/false-positives_test.go +++ b/sechub-cli/src/mercedes-benz.com/sechub/cli/false-positives_test.go @@ -3,6 +3,7 @@ package cli import ( + "encoding/json" "fmt" "os" "path/filepath" @@ -44,7 +45,7 @@ func TestFalsePositivesSaveWritesAFile(t *testing.T) { sechubTestUtil.AssertFileExists(expected, t) } -func Example_defineFalsePositives() { +func Example_defineFalsePositivesJobData() { /* prepare */ definedFalsePositives := []FalsePositivesJobData{ {JobUUID: "11111111-1111-1111-1111-111111111111", FindingID: 1, Comment: "test1"}, @@ -68,11 +69,11 @@ func Example_defineFalsePositives() { fmt.Printf("Remove: %+v\n", falsePositivesToRemove) // Output: - // Add: {APIVersion:1.0 Type:falsePositiveJobDataList JobData:[{JobUUID:33333333-3333-3333-3333-333333333333 FindingID:3 Comment:test3} {JobUUID:55555555-5555-5555-5555-555555555555 FindingID:5 Comment:test5}]} - // Remove: {APIVersion:1.0 Type:falsePositiveJobDataList JobData:[{JobUUID:44444444-4444-4444-4444-444444444444 FindingID:4 Comment:}]} + // Add: {APIVersion:1.0 Type:falsePositiveJobDataList JobData:[{JobUUID:33333333-3333-3333-3333-333333333333 FindingID:3 Comment:test3} {JobUUID:55555555-5555-5555-5555-555555555555 FindingID:5 Comment:test5}] ProjectData:[]} + // Remove: {APIVersion:1.0 Type:falsePositiveJobDataList JobData:[{JobUUID:44444444-4444-4444-4444-444444444444 FindingID:4 Comment:}] ProjectData:[]} } -func Example_defineFalsePositivesEmptyInputList() { +func Example_defineFalsePositivesJobDataEmptyInputList() { // An empty input list will remove all defined false-positives /* prepare */ @@ -92,11 +93,11 @@ func Example_defineFalsePositivesEmptyInputList() { fmt.Printf("Remove: %+v\n", falsePositivesToRemove) // Output: - // Add: {APIVersion:1.0 Type:falsePositiveJobDataList JobData:[]} - // Remove: {APIVersion:1.0 Type:falsePositiveJobDataList JobData:[{JobUUID:11111111-1111-1111-1111-111111111111 FindingID:1 Comment:} {JobUUID:22222222-2222-2222-2222-222222222222 FindingID:2 Comment:}]} + // Add: {APIVersion:1.0 Type:falsePositiveJobDataList JobData:[] ProjectData:[]} + // Remove: {APIVersion:1.0 Type:falsePositiveJobDataList JobData:[{JobUUID:11111111-1111-1111-1111-111111111111 FindingID:1 Comment:} {JobUUID:22222222-2222-2222-2222-222222222222 FindingID:2 Comment:}] ProjectData:[]} } -func Example_defineFalsePositivesEmptyServerList() { +func Example_defineFalsePositivesJobDataEmptyServerList() { // An empty server list will simply add all defined false-positives /* prepare */ @@ -116,8 +117,122 @@ func Example_defineFalsePositivesEmptyServerList() { fmt.Printf("Remove: %+v\n", falsePositivesToRemove) // Output: - // Add: {APIVersion:1.0 Type:falsePositiveJobDataList JobData:[{JobUUID:11111111-1111-1111-1111-111111111111 FindingID:1 Comment:test1} {JobUUID:22222222-2222-2222-2222-222222222222 FindingID:2 Comment:test2}]} - // Remove: {APIVersion:1.0 Type:falsePositiveJobDataList JobData:[]} + // Add: {APIVersion:1.0 Type:falsePositiveJobDataList JobData:[{JobUUID:11111111-1111-1111-1111-111111111111 FindingID:1 Comment:test1} {JobUUID:22222222-2222-2222-2222-222222222222 FindingID:2 Comment:test2}] ProjectData:[]} + // Remove: {APIVersion:1.0 Type:falsePositiveJobDataList JobData:[] ProjectData:[]} +} + +func Example_defineFalsePositivesProjectData() { + /* prepare */ + definedFalsePositives := []FalsePositivesProjectData{ + {ID: "test1", Comment: "test1", WebScan: FalsePositivesProjectDataForWebScan{CweID: 1, UrlPattern: "https://example1/*", Methods: []string{"GET", "PUT"}}}, + {ID: "test2", Comment: "test2", WebScan: FalsePositivesProjectDataForWebScan{CweID: 2, UrlPattern: "https://example2/*"}}, + {ID: "test3", Comment: "test3", WebScan: FalsePositivesProjectDataForWebScan{CweID: 3, UrlPattern: "https://example3/*"}}, + {ID: "test5", Comment: "test5", WebScan: FalsePositivesProjectDataForWebScan{CweID: 5, UrlPattern: "https://example5/*"}}, + } + falsePositivesDefinitionList := FalsePositivesConfig{APIVersion: CurrentAPIVersion, Type: falsePositivesListType, ProjectData: definedFalsePositives} + + falsePositivesServerList := []FalsePositiveDefinition{ + {ProjectData: FalsePositivesProjectData{ID: "test1", Comment: "test1", WebScan: FalsePositivesProjectDataForWebScan{CweID: 1, UrlPattern: "https://example1/*", Methods: []string{"GET", "POST"}}}}, + {ProjectData: FalsePositivesProjectData{ID: "test2", Comment: "test2 old", WebScan: FalsePositivesProjectDataForWebScan{CweID: 2, UrlPattern: "https://example2/*"}}}, + {ProjectData: FalsePositivesProjectData{ID: "test4", Comment: "test4", WebScan: FalsePositivesProjectDataForWebScan{CweID: 4, UrlPattern: "https://example4/*"}}}, + } + + /* execute */ + falsePositivesToAdd, falsePositivesToRemove := defineFalsePositives(falsePositivesDefinitionList, falsePositivesServerList) + + /* test */ + fmt.Printf("Add: %+v\n", falsePositivesToAdd) + fmt.Printf("Remove: %+v\n", falsePositivesToRemove) + + // Output: + // Add: {APIVersion:1.0 Type:falsePositiveJobDataList JobData:[] ProjectData:[{ID:test1 Comment:test1 WebScan:{CweID:1 UrlPattern:https://example1/* Methods:[GET PUT]}} {ID:test2 Comment:test2 WebScan:{CweID:2 UrlPattern:https://example2/* Methods:[]}} {ID:test3 Comment:test3 WebScan:{CweID:3 UrlPattern:https://example3/* Methods:[]}} {ID:test5 Comment:test5 WebScan:{CweID:5 UrlPattern:https://example5/* Methods:[]}}]} + // Remove: {APIVersion:1.0 Type:falsePositiveJobDataList JobData:[] ProjectData:[{ID:test4 Comment:test4 WebScan:{CweID:4 UrlPattern:https://example4/* Methods:[]}}]} +} + +func Example_defineFalsePositivesProjectDataEmptyInputList() { + // An empty input list will remove all defined false-positives + + /* prepare */ + definedFalsePositives := []FalsePositivesProjectData{} + falsePositivesDefinitionList := FalsePositivesConfig{APIVersion: CurrentAPIVersion, Type: falsePositivesListType, ProjectData: definedFalsePositives} + + falsePositivesServerList := []FalsePositiveDefinition{ + {ProjectData: FalsePositivesProjectData{ID: "test1"}}, + {ProjectData: FalsePositivesProjectData{ID: "test2"}}, + } + + /* execute */ + falsePositivesToAdd, falsePositivesToRemove := defineFalsePositives(falsePositivesDefinitionList, falsePositivesServerList) + + /* test */ + fmt.Printf("Add: %+v\n", falsePositivesToAdd) + fmt.Printf("Remove: %+v\n", falsePositivesToRemove) + + // Output: + // Add: {APIVersion:1.0 Type:falsePositiveJobDataList JobData:[] ProjectData:[]} + // Remove: {APIVersion:1.0 Type:falsePositiveJobDataList JobData:[] ProjectData:[{ID:test1 Comment: WebScan:{CweID:0 UrlPattern: Methods:[]}} {ID:test2 Comment: WebScan:{CweID:0 UrlPattern: Methods:[]}}]} +} + +func Example_defineFalsePositivesProjectDataEmptyServerList() { + // An empty server list will simply add all defined false-positives + + /* prepare */ + definedFalsePositives := []FalsePositivesProjectData{ + {ID: "test1", Comment: "test1", WebScan: FalsePositivesProjectDataForWebScan{CweID: 1, UrlPattern: "https://example1/*", Methods: []string{"GET", "PUT"}}}, + {ID: "test2", Comment: "test2", WebScan: FalsePositivesProjectDataForWebScan{CweID: 2, UrlPattern: "https://example2/*"}}, + } + falsePositivesDefinitionList := FalsePositivesConfig{APIVersion: CurrentAPIVersion, Type: falsePositivesListType, ProjectData: definedFalsePositives} + + falsePositivesServerList := []FalsePositiveDefinition{} + + /* execute */ + falsePositivesToAdd, falsePositivesToRemove := defineFalsePositives(falsePositivesDefinitionList, falsePositivesServerList) + + /* test */ + fmt.Printf("Add: %+v\n", falsePositivesToAdd) + fmt.Printf("Remove: %+v\n", falsePositivesToRemove) + + // Output: + // Add: {APIVersion:1.0 Type:falsePositiveJobDataList JobData:[] ProjectData:[{ID:test1 Comment:test1 WebScan:{CweID:1 UrlPattern:https://example1/* Methods:[GET PUT]}} {ID:test2 Comment:test2 WebScan:{CweID:2 UrlPattern:https://example2/* Methods:[]}}]} + // Remove: {APIVersion:1.0 Type:falsePositiveJobDataList JobData:[] ProjectData:[]} +} + +func Example_defineFalsePositivesWhenIdenticalToServerList() { + // When both lists are identical then no changes shall be made + + /* prepare */ + definedFalsePositivesJobData := []FalsePositivesJobData{ + {JobUUID: "11111111-1111-1111-1111-111111111111", FindingID: 1}, + {JobUUID: "22222222-2222-2222-2222-222222222222", FindingID: 2}, + } + definedFalsePositivesProjectData := []FalsePositivesProjectData{ + {ID: "test1", Comment: "test1", WebScan: FalsePositivesProjectDataForWebScan{CweID: 1, UrlPattern: "https://example1/*", Methods: []string{"GET", "PUT"}}}, + {ID: "test2", Comment: "test2", WebScan: FalsePositivesProjectDataForWebScan{CweID: 2, UrlPattern: "https://example2/*"}}, + } + falsePositivesDefinitionList := FalsePositivesConfig{ + APIVersion: CurrentAPIVersion, + Type: falsePositivesListType, + JobData: definedFalsePositivesJobData, + ProjectData: definedFalsePositivesProjectData, + } + + falsePositivesServerList := []FalsePositiveDefinition{ + {JobData: FalsePositivesJobData{JobUUID: "11111111-1111-1111-1111-111111111111", FindingID: 1}}, + {JobData: FalsePositivesJobData{JobUUID: "22222222-2222-2222-2222-222222222222", FindingID: 2}}, + {ProjectData: FalsePositivesProjectData{ID: "test1", Comment: "test1", WebScan: FalsePositivesProjectDataForWebScan{CweID: 1, UrlPattern: "https://example1/*", Methods: []string{"GET", "PUT"}}}}, + {ProjectData: FalsePositivesProjectData{ID: "test2", Comment: "test2", WebScan: FalsePositivesProjectDataForWebScan{CweID: 2, UrlPattern: "https://example2/*"}}}, + } + + /* execute */ + falsePositivesToAdd, falsePositivesToRemove := defineFalsePositives(falsePositivesDefinitionList, falsePositivesServerList) + + /* test */ + fmt.Printf("Add: %+v\n", falsePositivesToAdd) + fmt.Printf("Remove: %+v\n", falsePositivesToRemove) + + // Output: + // Add: {APIVersion:1.0 Type:falsePositiveJobDataList JobData:[] ProjectData:[]} + // Remove: {APIVersion:1.0 Type:falsePositiveJobDataList JobData:[] ProjectData:[]} } func Example_getFalsePositivesUploadChunk1() { @@ -141,7 +256,103 @@ func Example_getFalsePositivesUploadChunk1() { /* test */ // Output: - // {APIVersion:1.0 Type:falsePositiveJobDataList JobData:[{JobUUID:11111111-1111-1111-1111-111111111111 FindingID:1 Comment:test1} {JobUUID:22222222-2222-2222-2222-222222222222 FindingID:2 Comment:test2}]} - // {APIVersion:1.0 Type:falsePositiveJobDataList JobData:[{JobUUID:33333333-3333-3333-3333-333333333333 FindingID:3 Comment:test3}]} - // {APIVersion:1.0 Type:falsePositiveJobDataList JobData:[]} + // {APIVersion:1.0 Type:falsePositiveJobDataList JobData:[{JobUUID:11111111-1111-1111-1111-111111111111 FindingID:1 Comment:test1} {JobUUID:22222222-2222-2222-2222-222222222222 FindingID:2 Comment:test2}] ProjectData:[]} + // {APIVersion:1.0 Type:falsePositiveJobDataList JobData:[{JobUUID:33333333-3333-3333-3333-333333333333 FindingID:3 Comment:test3}] ProjectData:[]} + // {APIVersion:1.0 Type:falsePositiveJobDataList JobData:[] ProjectData:[]} +} + +func Example_newFalsePositivesListFromBytes_jobData() { + /* prepare */ + fpJSON := ` +{ + "apiVersion": "1.0", + "type": "falsePositiveDataList", + "jobData": [ + { + "jobUUID": "6cfa2ccf-da13-4dee-b529-0225ed9661bd", + "findingId": 1, + "comment": "Meaningful comment" + }, + { + "jobUUID": "6cfa2ccf-da13-4dee-b529-0225ed9661bd", + "findingId": 2 + } + ] +} + ` + + inputfile := []byte(fpJSON) + + /* execute */ + fpList := newFalsePositivesListFromBytes(inputfile) + + /* test */ + jsonBlob, _ := json.Marshal(fpList) + fmt.Println(string(jsonBlob)) + + // Output: + // {"apiVersion":"1.0","type":"falsePositiveDataList","jobData":[{"jobUUID":"6cfa2ccf-da13-4dee-b529-0225ed9661bd","findingId":1,"comment":"Meaningful comment"},{"jobUUID":"6cfa2ccf-da13-4dee-b529-0225ed9661bd","findingId":2,"comment":""}],"projectData":null} +} + +func Example_newFalsePositivesListFromBytes_projectData() { + /* prepare */ + fpJSON := ` +{ + "apiVersion": "1.0", + "type": "falsePositiveDataList", + "projectData": [ + { + "id": "my-id", + "comment": "text1", + "webScan": { + "cweId": 89, + "urlPattern": "https://myapp-*.example.com:80*/rest/*/search?*", + "methods": [ "GET", "DELETE" ] + } + } + ] +} + ` + + inputfile := []byte(fpJSON) + + /* execute */ + fpList := newFalsePositivesListFromBytes(inputfile) + + /* test */ + jsonBlob, _ := json.Marshal(fpList) + fmt.Println(string(jsonBlob)) + + // Output: + // {"apiVersion":"1.0","type":"falsePositiveDataList","jobData":null,"projectData":[{"id":"my-id","comment":"text1","webScan":{"cweId":89,"urlPattern":"https://myapp-*.example.com:80*/rest/*/search?*","methods":["GET","DELETE"]}}]} +} + +func Example_newFalsePositivesListFromBytes_projectDataWithoutCWE() { + /* prepare */ + fpJSON := ` +{ + "apiVersion": "1.0", + "type": "falsePositiveDataList", + "projectData": [ + { + "id": "my-id", + "webScan": { + "urlPattern": "https://myapp-*.example.com/rest/login?*" + } + } + ] +} + ` + + inputfile := []byte(fpJSON) + + /* execute */ + fpList := newFalsePositivesListFromBytes(inputfile) + + /* test */ + jsonBlob, _ := json.Marshal(fpList) + fmt.Println(string(jsonBlob)) + + // Output: + // {"apiVersion":"1.0","type":"falsePositiveDataList","jobData":null,"projectData":[{"id":"my-id","comment":"","webScan":{"cweId":0,"urlPattern":"https://myapp-*.example.com/rest/login?*","methods":null}}]} } diff --git a/sechub-cli/src/mercedes-benz.com/sechub/cli/urlbuilder.go b/sechub-cli/src/mercedes-benz.com/sechub/cli/urlbuilder.go index b1cfe1bc78..07ee3877a0 100644 --- a/sechub-cli/src/mercedes-benz.com/sechub/cli/urlbuilder.go +++ b/sechub-cli/src/mercedes-benz.com/sechub/cli/urlbuilder.go @@ -75,6 +75,11 @@ func buildFalsePositiveAPICall(context *Context) string { return buildAPIUrl(&context.config.server, &apiPart) } +func buildFalsePositiveProjectDataAPICall(context *Context) string { + apiPart := fmt.Sprintf("project/%s/false-positive/project-data", context.config.projectID) + return buildAPIUrl(&context.config.server, &apiPart) +} + func buildAPIUrl(server *string, apiPart *string) string { return fmt.Sprintf("%s/api/%s", *server, *apiPart) } diff --git a/sechub-doc/src/docs/asciidoc/documents/client/02_sechub_client.adoc b/sechub-doc/src/docs/asciidoc/documents/client/02_sechub_client.adoc index d9cf25eea6..7721c524a8 100644 --- a/sechub-doc/src/docs/asciidoc/documents/client/02_sechub_client.adoc +++ b/sechub-doc/src/docs/asciidoc/documents/client/02_sechub_client.adoc @@ -237,7 +237,7 @@ See also <> fo [[section-client-false-positives-unmark]] ===== unmarkFalsePositives -Remove formerly defined false positives. + +Remove formerly defined false positives. + It works similar to `markFalsePositives`: Just define a JSON file, select the file by the `-file` argument and start -action `unmarkFalsePositives` +action `unmarkFalsePositives`. **Minimum call syntax** ---- sechub -file ${json-file} unmarkFalsePositives ---- -*Example JSON:* +The JSON scheme is identical to `markFalsePositives` + +Mandatory fields for unmarkFalsePositives: + +- for jobData: `jobUUID` and `findingId` + +- for projectData: `id` + +*Example JSON with both: jobData and projectData* [source, json] ---- -include::sechub_client_falsepositive_list_example_unmark.json[] +include::sechub_client_falsepositive_list_example_unmark_jobData+projectData.json[] ---- -TIP: <> might be much easier to use +TIP: <> might be easier to use ==== Configuration overview diff --git a/sechub-doc/src/docs/asciidoc/documents/client/sechub_client_falsepositive_list_example_unmark_jobData+projectData.json b/sechub-doc/src/docs/asciidoc/documents/client/sechub_client_falsepositive_list_example_unmark_jobData+projectData.json new file mode 100644 index 0000000000..dea1e98d1d --- /dev/null +++ b/sechub-doc/src/docs/asciidoc/documents/client/sechub_client_falsepositive_list_example_unmark_jobData+projectData.json @@ -0,0 +1,15 @@ +{ + "apiVersion": "1.0", + "type": "falsePositiveDataList", + "jobData": [ + { + "jobUUID": "6cfa2ccf-da13-4dee-b529-0225ed9661bd", + "findingId": 1 + } + ], + "projectData": [ + { + "id": "unique-id" + } + ] +} diff --git a/sechub-doc/src/docs/asciidoc/documents/shared/false-positives/false-positives-REST-API-content-example1.json b/sechub-doc/src/docs/asciidoc/documents/shared/false-positives/false-positives-REST-API-content-example1.json index df2d2553bc..ae76e8ae7c 100644 --- a/sechub-doc/src/docs/asciidoc/documents/shared/false-positives/false-positives-REST-API-content-example1.json +++ b/sechub-doc/src/docs/asciidoc/documents/shared/false-positives/false-positives-REST-API-content-example1.json @@ -5,22 +5,11 @@ { "jobUUID": "6cfa2ccf-da13-4dee-b529-0225ed9661bd", //<4> "findingId": 1, //<5> - "comment": "Absolute Path Traversal, can be ignored because not in deployment" //<6> + "comment": "Can be ignored because not in deployment" //<6> }, { "jobUUID": "6cfa2ccf-da13-4dee-b529-0225ed9661bd", "findingId": 15 } - ], - "projectData": [ //<7> - { - "id": "unique-id", //<8> - "comment": "It was verified that there is no SQL-injection vulnerability at this location", - "webScan": { //<9> - "cweId": 89, //<10> - "urlPattern": "https://*.example.com/rest/products/search*", //<11> - "methods": [ "GET", "DELETE" ] //<12> - } - } ] -} \ No newline at end of file +} diff --git a/sechub-doc/src/docs/asciidoc/documents/shared/false-positives/false-positives-REST-API-content-example2.json b/sechub-doc/src/docs/asciidoc/documents/shared/false-positives/false-positives-REST-API-content-example2.json new file mode 100644 index 0000000000..d7effb39f4 --- /dev/null +++ b/sechub-doc/src/docs/asciidoc/documents/shared/false-positives/false-positives-REST-API-content-example2.json @@ -0,0 +1,15 @@ +{ + "apiVersion": "1.0", //<1> + "type": "falsePositiveDataList", //<2> + "projectData": [ //<3> + { + "id": "unique-id", //<4> + "comment": "It was verified that there is no SQL-injection", + "webScan": { //<5> + "cweId": 89, //<6> + "urlPattern": "https://*.example.com/rest/products/search?*", //<7> + "methods": [ "GET", "DELETE" ] //<8> + } + } + ] +} diff --git a/sechub-doc/src/docs/asciidoc/documents/shared/false-positives/false-positives-howto-define-by-api.adoc b/sechub-doc/src/docs/asciidoc/documents/shared/false-positives/false-positives-howto-define-by-api.adoc index f8cceeb14b..67b13c629a 100644 --- a/sechub-doc/src/docs/asciidoc/documents/shared/false-positives/false-positives-howto-define-by-api.adoc +++ b/sechub-doc/src/docs/asciidoc/documents/shared/false-positives/false-positives-howto-define-by-api.adoc @@ -1,64 +1,66 @@ // SPDX-License-Identifier: MIT [[section-false-positives-define-by-API]] -Defining false positives is done by sending false positive information via `JSON` either +Defining false positives is done by declaring false positive information in a `JSON` file -- by referencing *job results* from former {sechub} job UUID and the corresponding finding entry (by id) or -- by specifying a *project data* section where specific patterns that match false positive findings are declared +- by referencing *<>* from former {sechub} job UUID and the corresponding finding entry (by id) and/or +- by specifying a *<>* section where specific patterns that match false positive findings are declared and post it to the SecHub server REST API. [NOTE] ==== -The `jobData` approach is very easy, generic - and also future-proof: The only dependency is to the job, -`UUID`, for which the report must still exist while the definition is done. Every false-positive in -any kind of scan can be handled like that. - -The `REST` controller logic reads the job result data and creates internally false positive -meta data. +The `jobData` approach is very generic and easy to use: It references a SecHub report. Every false-positive in any kind of scan can be handled like that. ==== [NOTE] ==== -The `projectData` approach is more powerful for the user. -Since it is more powerful with the wildcard approach it requires more intial setup from the user. +The `projectData` approach is more powerful for the user because wildcards can be used. -There are no dependencies because all information necessary to identify certain findings are specified via `REST`. -Each entry can be removed by the given `id`. +Each entry can be updated or removed by the given `id`. + +Declaring a projectData entry with an already existing `id`, will update its content with the new data. ==== -*Example JSON* +[[section-false-positives-defined-via-jobData]] +*Example JSON using job results* [source,json] ---- include::false-positives-REST-API-content-example1.json[] ---- -<1> API version -<2> type - must be `falsePositiveDataList` or the deprecated type `falsePositiveJobDataList`. -<3> List of jobData _(optional)_ that is used to mark a single finding as false positive -<4> SecHub Job-UUID of the report where the finding was -<5> finding-ID which shall be marked as false positive -<6> A comment (optional) describing the reason why this is a false positive -<7> List of projectData _(optional)_ that can be used to mark more than a single finding as false positive. -Currently only available for web scans. This is not necessarily bound to a SecHub report, -but it might be easier to create this type of false positive configuration with a SecHub report after a scan. -<8> `id` that identifies this entry. If the same `id` is used again, +<1> `apiVersion` _(mandatory)_ - API version +<2> `type` _(mandatory)_ - must be `falsePositiveDataList` +<3> `jobData` - List of job data that is used to mark a single finding as a false positive +<4> jobData.`jobUUID` _(mandatory)_ SecHub Job-UUID of the report where the finding was +<5> jobData.`findingId` _(mandatory)_ Finding ID which shall be marked as false positive +<6> jobData.`comment` _(optional)_ A comment describing the reason why this is a false positive + + +[[section-false-positives-defined-via-projectData]] +*Example JSON using project data* + +[source,json] +---- +include::false-positives-REST-API-content-example2.json[] +---- +<1> `apiVersion` _(mandatory)_ - API version +<2> `type` _(mandatory)_ - must be `falsePositiveDataList` +<3> `projectData` - List that can be used to mark more than a single finding as a false positive. Currently only available for web scans. +<4> projectData.`id` that identifies this entry. If the same `id` is used again, the existing false positive entry will be overwritten. The `id` is also mandatory to unmark this entry. -<9> `webScan` _(optional)_ section can be used to define false positive patterns for web scans to provide more possibilities to the user. -<10> `cweId` is used to mark a certain type of finding as false positive. -When handling web scan project data this will be treated as a _mandatory_ field, -but it can be omitted inside this configuration an will then match findings that do not have any `cweId`. -<11> `urlPattern` specifies a URL pattern to identify a false positive. This is a `mandatory` field. -Asterisks can be used as wildcards e.g. if you have different environments like DEV, INT, PROD or you have variable parts like in API calls or query paramaters `https://*.example.com/rest/*/search`. -<12> `methods` _(optional)_ can be used to further restrict the false positive matching, to specific request methods protocols, like GET, POST, etc. -Like any other _optional_ field, if this is missing it is simply ignored. +<5> projectData.`webScan` _(optional)_ section can be used to define false positive patterns for web scans (DAST). It provides more possibilities to the user than above jobData. +<6> projectData.webScan.`cweId` is used to mark a certain type of finding as false positive. + +When handling web scan project data this will be treated as a _mandatory_ field. + +Please insert here the cweId from the original report. + +If there was no cweId in the original report, then it must be omitted or set to zero `"cweId": 0`. +<7> projectData.webScan.`urlPattern` (_mandatory_) specifies an URL pattern to identify a false positive. +Asterisks can be used as wildcards e.g. if you have different environments like DEV, INT, PROD or you have variable parts like in API calls or query paramaters `https://*.example.com/rest/*/search?*`. +<8> projectData.webScan.`methods` _(optional)_ Can be used to further restrict the false positive matching, to specific request methods protocols, like GET, POST, etc. Important information on the wildcard approach in `projectData`, regarding web scans: + -- To be a false positive a finding must match the `cweId` and the `urlPattern` of at least one of the false positive entries. - An _optional_ list of (HTTP) `methods` can be specified to limit the false positive entry to certain `methods`, - e.g if you specify `"methods": [ "GET", "DELETE" ]` like in the example above that means even if the `cweId` and the `urlPattern` are matching, if the finding was found with a `POST` request it would not be a false positive. - If no `methods` are specified, this false positive entry will not be restricted to any method. + +- To be marked as a false positive a finding must match the given `cweId` and the `urlPattern` + - Wildcards (`pass:[*]`) can be used inside `urlPattern`. + - Wildcards match anything until the next NON-wildcard character. + - Multiple wildcards can be used in one `urlPattern`. + +- An _optional_ list of (HTTP) methods can be specified to limit the false positive entry to certain `methods`, e.g if you specify `"methods": [ "GET", "DELETE" ]` like in the example above that means even if the `cweId` and the `urlPattern` are matching, if the finding was found with a `POST` request it would not be a false positive. When leaving `methods` out, this false positive entry apply to any method. + - An `urlPattern` which contains only wildcards (`pass:[*]`) is not allowed. diff --git a/sechub-scan-product-sereco/src/main/java/com/mercedesbenz/sechub/domain/scan/product/sereco/SerecoProjectDataWebScanFalsePositiveSupport.java b/sechub-scan-product-sereco/src/main/java/com/mercedesbenz/sechub/domain/scan/product/sereco/SerecoProjectDataWebScanFalsePositiveSupport.java index 63b2bb6994..cf2e6bd787 100644 --- a/sechub-scan-product-sereco/src/main/java/com/mercedesbenz/sechub/domain/scan/product/sereco/SerecoProjectDataWebScanFalsePositiveSupport.java +++ b/sechub-scan-product-sereco/src/main/java/com/mercedesbenz/sechub/domain/scan/product/sereco/SerecoProjectDataWebScanFalsePositiveSupport.java @@ -7,8 +7,6 @@ import java.util.Map; import java.util.regex.Pattern; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import com.mercedesbenz.sechub.domain.scan.project.WebscanFalsePositiveProjectData; @@ -18,18 +16,16 @@ @Component public class SerecoProjectDataWebScanFalsePositiveSupport { - private static final Logger LOG = LoggerFactory.getLogger(SerecoProjectDataWebScanFalsePositiveSupport.class); - public boolean areBothHavingSameCweIdOrBothNoCweId(WebscanFalsePositiveProjectData webScanData, SerecoVulnerability vulnerability) { notNull(vulnerability, " vulnerability may not be null"); notNull(webScanData, " webscanProjectData may not be null"); - Integer cweIdOrNull = webScanData.getCweId(); + int cweId = webScanData.getCweId(); SerecoClassification serecoClassification = vulnerability.getClassification(); String serecoCWE = serecoClassification.getCwe(); if (serecoCWE == null || serecoCWE.isEmpty()) { - if (cweIdOrNull == null) { + if (cweId == 0) { /* * when not set in meta data and also not in vulnerability, than we assume it is * the same @@ -38,32 +34,17 @@ public boolean areBothHavingSameCweIdOrBothNoCweId(WebscanFalsePositiveProjectDa } return false; } - if (cweIdOrNull == null) { - return false; - } - try { - int serecoCWEint = Integer.parseInt(serecoCWE); - if (cweIdOrNull.intValue() != serecoCWEint) { - /* not same type of common vulnerability enumeration - so skip */ - return false; - } - - } catch (NumberFormatException e) { - LOG.error("Sereco vulnerability type:{} found CWE:{} but not expected integer format!", vulnerability.getType(), serecoCWE); - return false; - - } - return true; + String cweIdAsString = String.valueOf(cweId); + return cweIdAsString.equals(serecoCWE); } /** - * Iterates the given list of hostPatterns and uses each string value as key to - * get the corresponding compiled pattern from the given map. Then tries to - * match the given host against each pattern of the projectDataPatternMap. + * Then tries to match the given targetUrl against each pattern of the + * projectDataPatternMap. * * @param targetUrl * @param projectDataPatternMap - * @return true if the given host matches any of the given patterns + * @return true if the given URL matches any of the given patterns */ public boolean isMatchingUrlPattern(String targetUrl, Map projectDataPatternMap) { notNull(targetUrl, " host may not be null"); diff --git a/sechub-scan-product-sereco/src/main/java/com/mercedesbenz/sechub/domain/scan/product/sereco/WebScanProjectDataFalsePositiveStrategy.java b/sechub-scan-product-sereco/src/main/java/com/mercedesbenz/sechub/domain/scan/product/sereco/WebScanProjectDataFalsePositiveStrategy.java index 52b3ccbca0..ac6b7c6e53 100644 --- a/sechub-scan-product-sereco/src/main/java/com/mercedesbenz/sechub/domain/scan/product/sereco/WebScanProjectDataFalsePositiveStrategy.java +++ b/sechub-scan-product-sereco/src/main/java/com/mercedesbenz/sechub/domain/scan/product/sereco/WebScanProjectDataFalsePositiveStrategy.java @@ -68,7 +68,7 @@ public boolean isFalsePositive(SerecoVulnerability vulnerability, FalsePositiveP } /* ---------------------------------------------------- */ - /* -------------------SERVERS-------------------------- */ + /* ----------------------URL--------------------------- */ /* ---------------------------------------------------- */ if (!webscanFalsePositiveProjectDataSupport.isMatchingUrlPattern(targetUrl, projectDataPatternMap)) { return false; diff --git a/sechub-scan-product-sereco/src/test/java/com/mercedesbenz/sechub/domain/scan/product/sereco/SerecoWebScanFalsePositiveProjectDataSupportTest.java b/sechub-scan-product-sereco/src/test/java/com/mercedesbenz/sechub/domain/scan/product/sereco/SerecoProjectDataWebScanFalsePositiveSupportTest.java similarity index 79% rename from sechub-scan-product-sereco/src/test/java/com/mercedesbenz/sechub/domain/scan/product/sereco/SerecoWebScanFalsePositiveProjectDataSupportTest.java rename to sechub-scan-product-sereco/src/test/java/com/mercedesbenz/sechub/domain/scan/product/sereco/SerecoProjectDataWebScanFalsePositiveSupportTest.java index b9e863f1d4..785433bffe 100644 --- a/sechub-scan-product-sereco/src/test/java/com/mercedesbenz/sechub/domain/scan/product/sereco/SerecoWebScanFalsePositiveProjectDataSupportTest.java +++ b/sechub-scan-product-sereco/src/test/java/com/mercedesbenz/sechub/domain/scan/product/sereco/SerecoProjectDataWebScanFalsePositiveSupportTest.java @@ -22,9 +22,9 @@ import com.mercedesbenz.sechub.domain.scan.project.WebscanFalsePositiveProjectData; import com.mercedesbenz.sechub.sereco.metadata.SerecoVulnerability; -class SerecoWebScanFalsePositiveProjectDataSupportTest { +class SerecoProjectDataWebScanFalsePositiveSupportTest { - private static String matchingUrl = "https://prod.example.com/rest/profile/search"; + private static final String MATCHING_URL = "https://prod.example.com/rest/profile/search"; private static final Pattern MOCKED_PATTERN = mock(); private static final Matcher MOCKED_MATCHER = mock(); @@ -41,13 +41,11 @@ void beforeEach() { patternMap = new HashMap<>(); patternMap.put("id", MOCKED_PATTERN); - when(MOCKED_PATTERN.matcher(matchingUrl)).thenReturn(MOCKED_MATCHER); + when(MOCKED_PATTERN.matcher(MATCHING_URL)).thenReturn(MOCKED_MATCHER); } /*-------------------------------------CWE-IDs----------------------------------------------*/ @ParameterizedTest - @EmptySource - @NullSource @ValueSource(strings = { "1", "-1", "0", "4711" }) void both_having_the_same_cwe_id_returns_true(String cweId) { /* prepare */ @@ -64,8 +62,6 @@ void both_having_the_same_cwe_id_returns_true(String cweId) { } @ParameterizedTest - @EmptySource - @NullSource @ValueSource(strings = { "1", "-1", "0", "4711" }) void cwe_id_of_webscan_data_is_one_more_returns_false(String cweId) { /* prepare */ @@ -83,9 +79,8 @@ void cwe_id_of_webscan_data_is_one_more_returns_false(String cweId) { } @ParameterizedTest - @NullSource @ValueSource(ints = { 1, -1, 0, 4711 }) - void cwe_id_of_vulnerability_is_one_more_returns_false(Integer cweId) { + void cwe_id_of_vulnerability_is_one_more_returns_false(int cweId) { /* prepare */ WebscanFalsePositiveProjectData webScanData = new WebscanFalsePositiveProjectData(); webScanData.setCweId(cweId); @@ -100,6 +95,24 @@ void cwe_id_of_vulnerability_is_one_more_returns_false(Integer cweId) { assertFalse(areBothHavingSameCweIdOrBothNoCweId); } + @ParameterizedTest + @NullSource + @EmptySource + void cwe_id_of_projectData_is_zero_and_cwe_of_sereco_vulnerability_is_unset_returns_true(String serecoCweId) { + /* prepare */ + WebscanFalsePositiveProjectData webScanData = new WebscanFalsePositiveProjectData(); + webScanData.setCweId(0); + + SerecoVulnerability vulnerability = new SerecoVulnerability(); + vulnerability.getClassification().setCwe(serecoCweId); + + /* execute */ + boolean areBothHavingSameCweIdOrBothNoCweId = supportToTest.areBothHavingSameCweIdOrBothNoCweId(webScanData, vulnerability); + + /* test */ + assertTrue(areBothHavingSameCweIdOrBothNoCweId); + } + /*-----------------------------------------------METHODS-----------------------------------------------*/ @Test @@ -152,7 +165,7 @@ void for_urlPattern_not_matching_returns_false() { when(MOCKED_MATCHER.matches()).thenReturn(false); /* execute */ - boolean result = supportToTest.isMatchingUrlPattern(matchingUrl, patternMap); + boolean result = supportToTest.isMatchingUrlPattern(MATCHING_URL, patternMap); /* test */ assertFalse(result); @@ -164,7 +177,7 @@ void for_urlPattern_is_matching_returns_true() { when(MOCKED_MATCHER.matches()).thenReturn(true); /* execute */ - boolean result = supportToTest.isMatchingUrlPattern(matchingUrl, patternMap); + boolean result = supportToTest.isMatchingUrlPattern(MATCHING_URL, patternMap); /* test */ assertTrue(result); @@ -172,29 +185,22 @@ void for_urlPattern_is_matching_returns_true() { /*-------------------------------------HELPERS----------------------------------------------*/ - private Integer createIntegerFromString(String cweId) { + private int createIntegerFromString(String cweId) { if (cweId == null) { - return null; + return 0; } if (cweId.isEmpty()) { - return null; + return 0; } return Integer.parseInt(cweId); } - private String createIntAsStringButPlusOne(Integer cweId) { - if (cweId == null) { - return "1"; - } - int next = cweId.intValue() + 1; - return String.valueOf(next); + private String createIntAsStringButPlusOne(int cweId) { + return String.valueOf(cweId + 1); } - private Integer createAsIntButPlusOne(String cweId) { - Integer intvalue = createIntegerFromString(cweId); - if (intvalue == null) { - return 1; - } + private int createAsIntButPlusOne(String cweId) { + int intvalue = createIntegerFromString(cweId); return intvalue + 1; } diff --git a/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/project/FalsePositiveDataConfigMerger.java b/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/project/FalsePositiveDataConfigMerger.java index fed3eca693..6e88b1915d 100644 --- a/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/project/FalsePositiveDataConfigMerger.java +++ b/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/project/FalsePositiveDataConfigMerger.java @@ -1,6 +1,8 @@ // SPDX-License-Identifier: MIT package com.mercedesbenz.sechub.domain.scan.project; +import java.util.List; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -50,19 +52,27 @@ public void addJobDataWithMetaDataToConfig(ScanSecHubReport report, FalsePositiv } - public void addFalsePositiveProjectDataEntry(FalsePositiveProjectConfiguration config, FalsePositiveProjectData projectData, String userId) { - FalsePositiveEntry existingEntry = findExistingProjectDataFalsePositiveEntryInConfig(config, projectData); - - if (existingEntry != null) { - LOG.warn("False positive projectData entry:'{}' not added, because already existing", projectData.getId()); - return; - } - + public void addFalsePositiveProjectDataEntryOrUpdateExisting(FalsePositiveProjectConfiguration config, FalsePositiveProjectData projectData, + String userId) { FalsePositiveEntry projectDataEntry = new FalsePositiveEntry(); projectDataEntry.setAuthor(userId); projectDataEntry.setProjectData(projectData); - config.getFalsePositives().add(projectDataEntry); + List falsePositives = config.getFalsePositives(); + for (int index = 0; index < falsePositives.size(); index++) { + FalsePositiveEntry existingFPEntry = falsePositives.get(index); + FalsePositiveProjectData projectDataFromEntry = existingFPEntry.getProjectData(); + if (projectDataFromEntry == null) { + LOG.debug("The entry is a jobData entry with metaData so no projectData"); + continue; + } + if (projectDataFromEntry.getId().equals(projectData.getId())) { + LOG.info("False positive project data entry with id: '{}', will be updated with new data!", projectData.getId()); + falsePositives.set(index, projectDataEntry); + return; + } + } + falsePositives.add(projectDataEntry); } public void removeJobDataWithMetaDataFromConfig(FalsePositiveProjectConfiguration config, FalsePositiveJobData jobDataToRemove) { diff --git a/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/project/FalsePositiveDataList.java b/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/project/FalsePositiveDataList.java index fada506771..6fac3c7a08 100644 --- a/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/project/FalsePositiveDataList.java +++ b/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/project/FalsePositiveDataList.java @@ -7,6 +7,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonSetter; import com.mercedesbenz.sechub.commons.core.MustBeKeptStable; import com.mercedesbenz.sechub.commons.model.JSONable; @@ -68,4 +69,14 @@ public static FalsePositiveDataList fromString(String json) { return CONVERTER.fromJSON(json); } + @JsonSetter + public void setJobData(List jobData) { + this.jobData = (jobData != null) ? jobData : new ArrayList<>(); + } + + @JsonSetter + public void setProjectData(List projectData) { + this.projectData = (projectData != null) ? projectData : new ArrayList<>(); + } + } diff --git a/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/project/FalsePositiveDataListValidationImpl.java b/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/project/FalsePositiveDataListValidationImpl.java index 903894c4f7..3b83698e1f 100644 --- a/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/project/FalsePositiveDataListValidationImpl.java +++ b/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/project/FalsePositiveDataListValidationImpl.java @@ -79,7 +79,6 @@ protected void validate(ValidationContext context) { validateJobData(context, target.getJobData()); validateProjectData(context, target.getProjectData()); - } private void validateProjectData(ValidationContext context, List projectDataList) { @@ -104,14 +103,14 @@ private void validateJobDataAndProjectDataSize(ValidationContext jobDataList = target.getJobData(); List projectDataList = target.getProjectData(); - validateNotNull(context, jobDataList, "projectDataList"); + validateNotNull(context, jobDataList, "jobDataList"); validateNotNull(context, projectDataList, "projectDataList"); if (context.isInValid()) { return; } validateMinSize(context, jobDataList, getConfig().minLength, "jobDataList"); - validateMinSize(context, jobDataList, getConfig().minLength, "projectDataList"); + validateMinSize(context, projectDataList, getConfig().minLength, "projectDataList"); int combinedSize = jobDataList.size() + projectDataList.size(); diff --git a/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/project/FalsePositiveDataService.java b/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/project/FalsePositiveDataService.java index af4141cf44..3a4ef42ebe 100644 --- a/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/project/FalsePositiveDataService.java +++ b/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/project/FalsePositiveDataService.java @@ -156,7 +156,7 @@ private void addJobDataListToConfiguration(FalsePositiveProjectConfiguration con List projectDataList = dataList.getProjectData(); for (FalsePositiveProjectData projectData : projectDataList) { - merger.addFalsePositiveProjectDataEntry(config, projectData, userContextService.getUserId()); + merger.addFalsePositiveProjectDataEntryOrUpdateExisting(config, projectData, userContextService.getUserId()); } } diff --git a/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/project/WebscanFalsePositiveProjectData.java b/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/project/WebscanFalsePositiveProjectData.java index 501fcbbbb8..932696c8c2 100644 --- a/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/project/WebscanFalsePositiveProjectData.java +++ b/sechub-scan/src/main/java/com/mercedesbenz/sechub/domain/scan/project/WebscanFalsePositiveProjectData.java @@ -10,15 +10,15 @@ public class WebscanFalsePositiveProjectData implements ProjectData { public static final String PROPERTY_URLPATTERN = "urlPattern"; public static final String PROPERTY_METHODS = "methods"; - private Integer cweId; + private int cweId; private String urlPattern; private List methods; - public Integer getCweId() { + public int getCweId() { return cweId; } - public void setCweId(Integer cweId) { + public void setCweId(int cweId) { this.cweId = cweId; } diff --git a/sechub-scan/src/test/java/com/mercedesbenz/sechub/domain/scan/project/FalsePositiveDataConfigMergerTest.java b/sechub-scan/src/test/java/com/mercedesbenz/sechub/domain/scan/project/FalsePositiveDataConfigMergerTest.java index 00bb809a94..9de95d5dcb 100644 --- a/sechub-scan/src/test/java/com/mercedesbenz/sechub/domain/scan/project/FalsePositiveDataConfigMergerTest.java +++ b/sechub-scan/src/test/java/com/mercedesbenz/sechub/domain/scan/project/FalsePositiveDataConfigMergerTest.java @@ -192,7 +192,7 @@ void add_one_project_data_entry_results_in_one_entry_in_config() { projectData.setWebScan(webScan); /* execute */ - toTest.addFalsePositiveProjectDataEntry(config, projectData, TEST_AUTHOR); + toTest.addFalsePositiveProjectDataEntryOrUpdateExisting(config, projectData, TEST_AUTHOR); /* test */ List falsePositives = config.getFalsePositives(); @@ -207,7 +207,7 @@ void add_one_project_data_entry_results_in_one_entry_in_config() { } @Test - void add_one_project_data_entry_which_already_exists_results_in_entry_with_same_id_not_added_again() { + void add_one_project_data_entry_with_id_which_already_exists_results_in_entry_being_updated() { /* prepare */ String id = "unique-id"; WebscanFalsePositiveProjectData webScan1 = new WebscanFalsePositiveProjectData(); @@ -234,7 +234,7 @@ void add_one_project_data_entry_which_already_exists_results_in_entry_with_same_ projectData2.setWebScan(webScan2); /* execute */ - toTest.addFalsePositiveProjectDataEntry(config, projectData2, TEST_AUTHOR); + toTest.addFalsePositiveProjectDataEntryOrUpdateExisting(config, projectData2, TEST_AUTHOR); /* test */ List falsePositives = config.getFalsePositives(); @@ -245,7 +245,7 @@ void add_one_project_data_entry_which_already_exists_results_in_entry_with_same_ assertNull(existingEntry.getMetaData()); assertEquals(TEST_AUTHOR, existingEntry.getAuthor()); - assertEquals(projectData1, existingEntry.getProjectData()); + assertEquals(projectData2, existingEntry.getProjectData()); } @Test @@ -277,7 +277,7 @@ void add_a_second_project_data_entry_which_has_an_unique_id_results_in_two_entri projectData2.setWebScan(webScan2); /* execute */ - toTest.addFalsePositiveProjectDataEntry(config, projectData2, TEST_AUTHOR); + toTest.addFalsePositiveProjectDataEntryOrUpdateExisting(config, projectData2, TEST_AUTHOR); /* test */ List falsePositives = config.getFalsePositives(); diff --git a/sechub-scan/src/test/java/com/mercedesbenz/sechub/domain/scan/project/FalsePositiveDataListTest.java b/sechub-scan/src/test/java/com/mercedesbenz/sechub/domain/scan/project/FalsePositiveDataListTest.java index 1b39a3e9a3..86eb6c53d4 100644 --- a/sechub-scan/src/test/java/com/mercedesbenz/sechub/domain/scan/project/FalsePositiveDataListTest.java +++ b/sechub-scan/src/test/java/com/mercedesbenz/sechub/domain/scan/project/FalsePositiveDataListTest.java @@ -1,19 +1,20 @@ // SPDX-License-Identifier: MIT package com.mercedesbenz.sechub.domain.scan.project; -import static org.junit.Assert.*; +import static org.junit.Assert.assertNull; +import static org.junit.jupiter.api.Assertions.*; import java.util.Iterator; import java.util.List; -import org.junit.Test; +import org.junit.jupiter.api.Test; import com.mercedesbenz.sechub.domain.scan.ScanDomainTestFileSupport; public class FalsePositiveDataListTest { @Test - public void json_content_as_described_in_example_of_documentation() { + void json_content_as_described_in_example_of_documentation() { /* prepare */ String json = ScanDomainTestFileSupport.getTestfileSupport() .loadTestFileFromRoot("/sechub-doc/src/docs/asciidoc/documents/shared/false-positives/false-positives-REST-API-content-example1.json"); @@ -30,10 +31,30 @@ public void json_content_as_described_in_example_of_documentation() { FalsePositiveJobData jd2 = it.next(); assertEquals(1, jd1.getFindingId()); assertEquals("6cfa2ccf-da13-4dee-b529-0225ed9661bd", jd1.getJobUUID().toString()); - assertEquals("Absolute Path Traversal, can be ignored because not in deployment", jd1.getComment()); + assertEquals("Can be ignored because not in deployment", jd1.getComment()); assertEquals(15, jd2.getFindingId()); assertEquals("6cfa2ccf-da13-4dee-b529-0225ed9661bd", jd2.getJobUUID().toString()); assertNull(jd2.getComment()); } + @Test + void jobData_and_projectData_must_never_be_null() { + /* prepare */ + String json = """ + { + "apiVersion": "1.0", + "type": "falsePositiveDataList", + "jobData": null, + "projectData": null + } + """; + + /* execute */ + FalsePositiveDataList dataList = FalsePositiveDataList.fromString(json); + + /* test */ + assertTrue(dataList.getJobData().isEmpty()); + assertTrue(dataList.getProjectData().isEmpty()); + } + } diff --git a/sechub-scan/src/test/java/com/mercedesbenz/sechub/domain/scan/project/WebscanFalsePositiveProjectDataValidationImplTest.java b/sechub-scan/src/test/java/com/mercedesbenz/sechub/domain/scan/project/WebscanFalsePositiveProjectDataValidationImplTest.java index 5a1d4ac2ab..5cfc9c44a9 100644 --- a/sechub-scan/src/test/java/com/mercedesbenz/sechub/domain/scan/project/WebscanFalsePositiveProjectDataValidationImplTest.java +++ b/sechub-scan/src/test/java/com/mercedesbenz/sechub/domain/scan/project/WebscanFalsePositiveProjectDataValidationImplTest.java @@ -52,6 +52,7 @@ void without_optional_parts_returns_valid_result() { /* test */ assertTrue(result.isValid()); + assertEquals(0, webScan.getCweId()); } @Test diff --git a/sechub-server/src/main/java/com/mercedesbenz/sechub/server/SecHubServerMDCAsyncHandlerInterceptor.java b/sechub-server/src/main/java/com/mercedesbenz/sechub/server/SecHubServerMDCAsyncHandlerInterceptor.java index 602528fcda..2cb68bf947 100644 --- a/sechub-server/src/main/java/com/mercedesbenz/sechub/server/SecHubServerMDCAsyncHandlerInterceptor.java +++ b/sechub-server/src/main/java/com/mercedesbenz/sechub/server/SecHubServerMDCAsyncHandlerInterceptor.java @@ -74,7 +74,10 @@ private void handleJobParameter(String part) { break; } if ("job".equals(split) || "false-positive".equals(split)) { - useNext = true; + // the new alternative way to handle false positives does not contain a job uuid + if (!part.contains("false-positive/project-data")) { + useNext = true; + } } } if (uuid == null) { diff --git a/sechub-server/src/test/java/com/mercedesbenz/sechub/server/SecHubServerMDCAsyncHandlerInterceptorTest.java b/sechub-server/src/test/java/com/mercedesbenz/sechub/server/SecHubServerMDCAsyncHandlerInterceptorTest.java index 01dd7dae5b..29d8dae2bc 100644 --- a/sechub-server/src/test/java/com/mercedesbenz/sechub/server/SecHubServerMDCAsyncHandlerInterceptorTest.java +++ b/sechub-server/src/test/java/com/mercedesbenz/sechub/server/SecHubServerMDCAsyncHandlerInterceptorTest.java @@ -131,6 +131,20 @@ public void when_url_is_user_removes_false_positives_from_project_job_uuid_is_se assertEquals("myprojectId", MDC.get(LogConstants.MDC_SECHUB_PROJECT_ID)); } + @Test + public void when_url_is_user_removes_project_data_false_positives_from_project_no_job_uuid_is_expected() throws Exception { + /* prepare */ + when(request.getRequestURI()) + .thenReturn("https://localhost/api/project/myprojectId/false-positive/project-data/unique-id"); + + /* execute */ + interceptorToTest.preHandle(request, response, handler); + + /* test */ + assertNull(MDC.get(LogConstants.MDC_SECHUB_JOB_UUID)); + assertEquals("myprojectId", MDC.get(LogConstants.MDC_SECHUB_PROJECT_ID)); + } + @Test public void when_url_is_user_buildApproveJobUrluuid_is_set_and_projectId_as_well() throws Exception { /* prepare */