diff --git a/Dockerfile b/Dockerfile index 6e64b7d..4654d78 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,7 +16,7 @@ RUN go build -o /app/2ms . # Runtime image FROM cgr.dev/chainguard/git@sha256:02660563e96b553d6aeb4093e3fcc3e91b2ad3a86e05c65b233f37f035e5044e -RUN apk add --no-cache bash=5.2.21-r1 git=2.45.1-r0 git-lfs=3.5.1-r8 libcurl-openssl4=8.10.0-r0 glibc=2.39-r5 glibc-locale-posix=2.39-r5 ld-linux==2.39-r5 libcrypt1=2.39-r5 && git config --global --add safe.directory /repo +RUN apk add --no-cache bash=5.2.21-r1 git=2.45.1-r0 git-lfs=3.5.1-r8 libcurl-openssl4=8.10.0-r0 glibc=2.39-r5 glibc-locale-posix=2.39-r5 ld-linux==2.39-r5 libcrypt1=2.39-r5 libcrypto3=3.3.2-r2 libssl3=3.3.2-r2 && git config --global --add safe.directory /repo COPY --from=builder /app/2ms . diff --git a/engine/engine.go b/engine/engine.go index 322014b..fe1a4c6 100644 --- a/engine/engine.go +++ b/engine/engine.go @@ -90,15 +90,16 @@ func (e *Engine) Detect(item plugins.ISourceItem, secretsChannel chan *secrets.S endLine = value.EndLine } secret := &secrets.Secret{ - ID: itemId, - Source: item.GetSource(), - RuleID: value.RuleID, - StartLine: startLine, - StartColumn: value.StartColumn, - EndLine: endLine, - EndColumn: value.EndColumn, - Value: value.Secret, - LineContent: value.Line, + ID: itemId, + Source: item.GetSource(), + RuleID: value.RuleID, + StartLine: startLine, + StartColumn: value.StartColumn, + EndLine: endLine, + EndColumn: value.EndColumn, + Value: value.Secret, + LineContent: value.Line, + RuleDescription: value.Description, } if !isSecretIgnored(secret, &e.ignoredIds, &e.allowedValues) { secretsChannel <- secret diff --git a/lib/reporting/report_test.go b/lib/reporting/report_test.go index d282c92..946059c 100644 --- a/lib/reporting/report_test.go +++ b/lib/reporting/report_test.go @@ -1,13 +1,172 @@ package reporting import ( + "encoding/json" "os" "path/filepath" "reflect" + "sort" + "strings" "testing" "github.com/checkmarx/2ms/lib/config" "github.com/checkmarx/2ms/lib/secrets" + "github.com/stretchr/testify/assert" +) + +// test input results +var ( + ruleID1 = "ruleID1" + ruleID2 = "ruleID2" + result1 = &secrets.Secret{ + ID: "ID1", + Source: "file1", + RuleID: ruleID1, + StartLine: 150, + EndLine: 150, + LineContent: "line content", + StartColumn: 31, + EndColumn: 150, + Value: "value", + ValidationStatus: secrets.ValidResult, + RuleDescription: "Rule Description", + } + // this result has a different rule than result1 + result2 = &secrets.Secret{ + ID: "ID2", + Source: "file2", + RuleID: ruleID2, + StartLine: 10, + EndLine: 10, + LineContent: "line content2", + StartColumn: 41, + EndColumn: 160, + Value: "value 2", + ValidationStatus: secrets.InvalidResult, + RuleDescription: "Rule Description2", + } + // this result has the same rule as result1 + result3 = &secrets.Secret{ + ID: "ID3", + Source: "file3", + RuleID: ruleID1, + StartLine: 16, + EndLine: 16, + LineContent: "line content3", + StartColumn: 11, + EndColumn: 130, + Value: "value 3", + ValidationStatus: secrets.UnknownResult, + RuleDescription: "Rule Description", + } +) + +// test expected outputs +var ( + // sarif rules + rule1Sarif = &SarifRule{ + ID: ruleID1, + FullDescription: &Message{ + Text: result1.RuleDescription, + }, + } + rule2Sarif = &SarifRule{ + ID: ruleID2, + FullDescription: &Message{ + Text: result2.RuleDescription, + }, + } + // sarif results + result1Sarif = Results{ + Message: Message{ + Text: messageText(result1.RuleID, result1.Source), + }, + RuleId: ruleID1, + Locations: []Locations{ + { + PhysicalLocation: PhysicalLocation{ + ArtifactLocation: ArtifactLocation{ + URI: result1.Source, + }, + Region: Region{ + StartLine: result1.StartLine, + StartColumn: result1.StartColumn, + EndLine: result1.EndLine, + EndColumn: result1.EndColumn, + Snippet: Snippet{ + Text: result1.Value, + Properties: Properties{ + "lineContent": strings.TrimSpace(result1.LineContent), + }, + }, + }, + }, + }, + }, + Properties: Properties{ + "validationStatus": string(result1.ValidationStatus), + }, + } + result2Sarif = Results{ + Message: Message{ + Text: messageText(result2.RuleID, result2.Source), + }, + RuleId: ruleID2, + Locations: []Locations{ + { + PhysicalLocation: PhysicalLocation{ + ArtifactLocation: ArtifactLocation{ + URI: result2.Source, + }, + Region: Region{ + StartLine: result2.StartLine, + StartColumn: result2.StartColumn, + EndLine: result2.EndLine, + EndColumn: result2.EndColumn, + Snippet: Snippet{ + Text: result2.Value, + Properties: Properties{ + "lineContent": strings.TrimSpace(result2.LineContent), + }, + }, + }, + }, + }, + }, + Properties: Properties{ + "validationStatus": string(result2.ValidationStatus), + }, + } + result3Sarif = Results{ + Message: Message{ + Text: messageText(result3.RuleID, result3.Source), + }, + RuleId: ruleID1, + Locations: []Locations{ + { + PhysicalLocation: PhysicalLocation{ + ArtifactLocation: ArtifactLocation{ + URI: result3.Source, + }, + Region: Region{ + StartLine: result3.StartLine, + StartColumn: result3.StartColumn, + EndLine: result3.EndLine, + EndColumn: result3.EndColumn, + Snippet: Snippet{ + Text: result3.Value, + Properties: Properties{ + "lineContent": strings.TrimSpace(result3.LineContent), + }, + }, + }, + }, + }, + }, + Properties: Properties{ + "validationStatus": string(result3.ValidationStatus), + }, + } ) func TestAddSecretToFile(t *testing.T) { @@ -57,3 +216,115 @@ func TestWriteReportInNonExistingDir(t *testing.T) { os.RemoveAll(filepath.Join(tempDir, "test_temp_dir")) } + +func TestGetOutputSarif(t *testing.T) { + tests := []struct { + name string + arg Report + want []Runs + wantErr bool + }{ + { + name: "two_results_same_rule_want_one_rule_in_report", + arg: Report{ + TotalItemsScanned: 2, + TotalSecretsFound: 2, + Results: map[string][]*secrets.Secret{ + "secret1": {result1}, + "secret3": {result3}, + }, + }, + wantErr: false, + want: []Runs{ + { + Tool: Tool{ + Driver: Driver{ + Name: "report", + SemanticVersion: "1", + Rules: []*SarifRule{ + rule1Sarif, + }, + }, + }, + Results: []Results{ + result1Sarif, + result3Sarif, + }, + }, + }, + }, + { + name: "two_results_two_rules_want_two_rules_in_report", + arg: Report{ + TotalItemsScanned: 2, + TotalSecretsFound: 2, + Results: map[string][]*secrets.Secret{ + "secret1": {result1}, + "secret2": {result2}, + }, + }, + wantErr: false, + want: []Runs{ + { + Tool: Tool{ + Driver: Driver{ + Name: "report", + SemanticVersion: "1", + Rules: []*SarifRule{ + rule1Sarif, + rule2Sarif, + }, + }, + }, + Results: []Results{ + result1Sarif, + result2Sarif, + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.arg.getOutput(sarifFormat, &config.Config{Name: "report", Version: "1"}) + if tt.wantErr { + assert.NotNil(t, err) + return + } + var gotReport Sarif + err = json.Unmarshal([]byte(got), &gotReport) + assert.Nil(t, err) + SortSarifReports(&gotReport, &Sarif{Runs: tt.want}) + assert.Equal(t, tt.want, gotReport.Runs) + }) + } +} + +// SortProject Sorts two sarif reports +func SortSarifReports(run1, run2 *Sarif) { + // Sort Rules + SortRules(run1.Runs[0].Tool.Driver.Rules, run2.Runs[0].Tool.Driver.Rules) + SortResults(run1.Runs[0].Results, run2.Runs[0].Results) + +} + +func SortRules(rules1, rules2 []*SarifRule) { + // Sort both slices + sort.Slice(rules1, func(i, j int) bool { + return rules1[i].ID < rules1[j].ID + }) + sort.Slice(rules2, func(i, j int) bool { + return rules2[i].ID < rules2[j].ID + }) +} + +func SortResults(results1, results2 []Results) { + // Sort both slices + sort.Slice(results1, func(i, j int) bool { + return results1[i].Message.Text < results1[j].Message.Text + }) + sort.Slice(results2, func(i, j int) bool { + return results2[i].Message.Text < results2[j].Message.Text + }) +} diff --git a/lib/reporting/sarif.go b/lib/reporting/sarif.go index 67793bf..090931a 100644 --- a/lib/reporting/sarif.go +++ b/lib/reporting/sarif.go @@ -3,9 +3,10 @@ package reporting import ( "encoding/json" "fmt" + "strings" + "github.com/checkmarx/2ms/lib/config" "github.com/checkmarx/2ms/lib/secrets" - "strings" ) func writeSarif(report Report, cfg *config.Config) (string, error) { @@ -26,23 +27,43 @@ func writeSarif(report Report, cfg *config.Config) (string, error) { func getRuns(report Report, cfg *config.Config) []Runs { return []Runs{ { - Tool: getTool(cfg), + Tool: getTool(report, cfg), Results: getResults(report), }, } } -func getTool(cfg *config.Config) Tool { +func getTool(report Report, cfg *config.Config) Tool { tool := Tool{ Driver: Driver{ Name: cfg.Name, SemanticVersion: cfg.Version, + Rules: getRules(report), }, } return tool } +func getRules(report Report) []*SarifRule { + uniqueRulesMap := make(map[string]*SarifRule) + var reportRules []*SarifRule + for _, reportSecrets := range report.Results { + for _, secret := range reportSecrets { + if _, exists := uniqueRulesMap[secret.RuleID]; !exists { + uniqueRulesMap[secret.RuleID] = &SarifRule{ + ID: secret.RuleID, + FullDescription: &Message{ + Text: secret.RuleDescription, + }, + } + reportRules = append(reportRules, uniqueRulesMap[secret.RuleID]) + } + } + } + return reportRules +} + func hasNoResults(report Report) bool { return len(report.Results) == 0 } @@ -112,14 +133,20 @@ type ShortDescription struct { } type Driver struct { - Name string `json:"name"` - SemanticVersion string `json:"semanticVersion"` + Name string `json:"name"` + SemanticVersion string `json:"semanticVersion"` + Rules []*SarifRule `json:"rules,omitempty"` } type Tool struct { Driver Driver `json:"driver"` } +type SarifRule struct { + ID string `json:"id"` + FullDescription *Message `json:"fullDescription,omitempty"` +} + type Message struct { Text string `json:"text"` } diff --git a/lib/secrets/secret.go b/lib/secrets/secret.go index 0ca0996..2485d12 100644 --- a/lib/secrets/secret.go +++ b/lib/secrets/secret.go @@ -43,5 +43,6 @@ type Secret struct { EndColumn int `json:"endColumn"` Value string `json:"value"` ValidationStatus ValidationResult `json:"validationStatus,omitempty"` + RuleDescription string `json:"ruleDescription,omitempty"` ExtraDetails map[string]interface{} `json:"extraDetails,omitempty"` }