diff --git a/models/artifact.go b/models/artifact.go new file mode 100644 index 0000000..2000534 --- /dev/null +++ b/models/artifact.go @@ -0,0 +1,72 @@ +package models + +type artifact struct { // https://docs.oasis-open.org/sarif/sarif/v2.1.0/csprd01/sarif-v2.1.0-csprd01.html#_Toc10541049 + Location *artifactLocation `json:"location,omitempty"` + ParentIndex *uint `json:"parentIndex,omitempty"` + Offset *uint `json:"offset,omitempty"` + Length int `json:"length"` + Roles []string `json:"roles,omitempty"` + MimeType *string `json:"mimeType,omitempty"` + Contents *artifactContent `json:"contents,omitempty"` + Encoding *string `json:"encoding,omitempty"` + SourceLanguage *string `json:"sourceLanguage,omitempty"` + Hashes map[string]string `json:"hashes,omitempty"` + LastModifiedTimeUtc *string `json:"lastModifiedTimeUtc,omitempty"` + Description *Message `json:"description,omitempty"` +} + +type artifactLocation struct { // https://docs.oasis-open.org/sarif/sarif/v2.1.0/csprd01/sarif-v2.1.0-csprd01.html#_Toc10540865 + URI *string `json:"uri,omitempty"` + URIBaseId *string `json:"uriBaseId,omitempty"` + Index *uint `json:"index,omitempty"` + Description *Message `json:"description,omitempty"` +} + +type ArtifactLocationBuilder struct { + artifactLocation *artifactLocation +} + +func (alb *ArtifactLocationBuilder) WithUri(uri string) *ArtifactLocationBuilder { + alb.artifactLocation.URI = &uri + return alb +} + +func (alb *ArtifactLocationBuilder) WithIndex(index uint) *ArtifactLocationBuilder { + alb.artifactLocation.Index = &index + return alb +} + +func (alb *ArtifactLocationBuilder) WithUriBaseId(uriBaseId string) *ArtifactLocationBuilder { + alb.artifactLocation.URIBaseId = &uriBaseId + return alb +} + +func (alb *ArtifactLocationBuilder) WithDescription(messageBuilder MessageBuilder) *ArtifactLocationBuilder { + alb.artifactLocation.Description = messageBuilder.Get() + return alb +} + +type artifactContent struct { // https://docs.oasis-open.org/sarif/sarif/v2.1.0/csprd01/sarif-v2.1.0-csprd01.html#_Toc10540860 + Text *string `json:"text,omitempty"` + Binary *string `json:"binary,omitempty"` + Rendered *multiformatMessageString `json:"rendered,omitempty"` +} + +type ArtifactBuilder struct { + run *Run + artifact *artifact +} + +func (run *Run) NewArtifactBuilder() *ArtifactBuilder { + return &ArtifactBuilder{ + run: run, + artifact: &artifact{ + Length: -1, + }, + } +} + +func (ab *ArtifactBuilder) Add() *Run { + ab.run.Artifacts = append(ab.run.Artifacts, ab.artifact) + return ab.run +} diff --git a/models/location.go b/models/location.go new file mode 100644 index 0000000..ff4c496 --- /dev/null +++ b/models/location.go @@ -0,0 +1,33 @@ +package models + +type location struct { + Id *uint `json:"id,omitempty"` + PhysicalLocation *physicalLocation `json:"physicalLocation,omitempty"` + LogicalLocations []*logicalLocation `json:"logicalLocations,omitempty"` + Message *Message `json:"message,omitempty"` + Annotations []*region `json:"annotations,omitempty"` + Relationships []*locationRelationship `json:"relationships,omitempty"` +} + +type physicalLocation struct { + ArtifactLocation *artifactLocation `json:"artifactLocation,omitempty"` + Region *region `json:"region,omitempty"` + ContextRegion *region `json:"contextRegion,omitempty"` + Address *address `json:"address,omitempty"` +} + +type logicalLocation struct { + Index *uint `json:"index,omitempty"` + Name *string `json:"name,omitempty"` + FullyQualifiedName *string `json:"fullyQualifiedName,omitempty"` + DecoratedName *string `json:"decoratedName,omitempty"` + Kind *string `json:"kind,omitempty"` + ParentIndex *uint `json:"parentIndex,omitempty"` +} + +type locationRelationship struct { + Target uint `json:"target"` + Kinds []string `json:"kinds,omitempty"` + Description *Message `json:"description,omitempty"` +} + diff --git a/models/message.go b/models/message.go new file mode 100644 index 0000000..db914d0 --- /dev/null +++ b/models/message.go @@ -0,0 +1,29 @@ +package models + +type MessageBuilder struct { + message *Message +} + +func (m *MessageBuilder) WithText(text string) *MessageBuilder { + m.message.Text = &text + return m +} + +func (m *MessageBuilder) WithMarkdown(markdown string) *MessageBuilder { + m.message.Markdown = &markdown + return m +} + +func (m *MessageBuilder) WithId(id string) *MessageBuilder { + m.message.Id = &id + return m +} + +func (m *MessageBuilder) WithArguments(args []string) *MessageBuilder { + m.message.Arguments = args + return m +} + +func (m *MessageBuilder) Get() *Message { + return m.message +} diff --git a/models/region.go b/models/region.go new file mode 100644 index 0000000..7c33ab1 --- /dev/null +++ b/models/region.go @@ -0,0 +1,29 @@ +package models + +type region struct { // https://docs.oasis-open.org/sarif/sarif/v2.1.0/csprd01/sarif-v2.1.0-csprd01.html#_Toc10541123 + StartLine *int `json:"startLine,omitempty"` + StartColumn *int `json:"startColumn,omitempty"` + EndLine *int `json:"endLine,omitempty"` + EndColumn *int `json:"endColumn,omitempty"` + CharOffset *int `json:"charOffset,omitempty"` + CharLength *int `json:"charLength,omitempty"` + ByteOffset *int `json:"byteOffset,omitempty"` + ByteLength *int `json:"byteLength,omitempty"` + Snippet *artifactContent `json:"snippet,omitempty"` + Message *Message `json:"message,omitempty"` + SourceLanguage *string `json:"sourceLanguage,omitempty"` +} + +type RegionBuilder struct { + region *region +} + +func NewRegionBuilder() *RegionBuilder { + return &RegionBuilder{ + region: ®ion{}, + } +} + +func (rb *RegionBuilder) Get() *region { + return rb.region +} diff --git a/models/result.go b/models/result.go index dab8f51..95cda4c 100644 --- a/models/result.go +++ b/models/result.go @@ -2,69 +2,142 @@ package models // Result represents the results block in the sarif report type Result struct { - Level string `json:"level"` - Message *textBlock `json:"message"` - RuleID string `json:"ruleId"` - RuleIndex int `json:"ruleIndex"` - Locations []*resultLocation `json:"locations,omitempty"` + Guid *string `json:"guid,omitempty"` + CorrelationGuid *string `json:"correlationGuid,omitempty"` + RuleID *string `json:"ruleId,omitempty"` + RuleIndex *uint `json:"ruleIndex,omitempty"` + Rule *reportingDescriptorReference `json:"rule,omitempty"` + Taxa []*reportingDescriptorReference `json:"taxa,omitempty"` + Kind *string `json:"kind,omitempty"` + Level *string `json:"level,omitempty"` + Message Message `json:"message"` + Locations []*location `json:"locations,omitempty"` + AnalysisTarget *artifactLocation `json:"analysisTarget,omitempty"` + // WebRequest *webRequest `json:"webRequest,omitempty"` + // WebResponse *webResponse `json:"webResponse,omitempty"` + Fingerprints map[string]interface{} `json:"fingerprints,omitempty"` + PartialFingerprints map[string]interface{} `json:"partialFingerprints,omitempty"` + // CodeFlows []*codeFlows `json:"codeFlows,omitempty"` + // Graphs []*graphs `json:"graphs,omitempty"` + // GraphTraversals []*graphTraversals `json:"graphTraversals,omitempty"` + // Stacks []*stack `json:"stacks,omitempty"` + RelatedLocations []*location `json:"relatedLocations,omitempty"` + Suppressions []*suppression `json:"suppressions,omitempty"` + BaselineState *string `json:"baselineState,omitempty"` + Rank *float32 `json:"rank,omitempty"` + // Attachments []*attachment `json:"attachments,omitempty"` + WorkItemUris []string `json:"workItemUris,omitempty"` // can be null + HostedViewerUri *string `json:"hostedViewerUri,omitempty"` + // Provenance *resultProvenance `json:"provenance,omitempty"` + Fixes []*fix `json:"fixes,omitempty"` + OccurrenceCount *uint `json:"occurrenceCount,omitempty"` } -type resultLocation struct { - PhysicalLocation *physicalLocation `json:"physicalLocation,omitempty"` +type reportingDescriptorReference struct { + Id *string `json:"id,omitempty"` + Index *uint `json:"index,omitempty"` + Guid *string `json:"guid,omitempty"` + ToolComponent *toolComponentReference `json:"toolComponent,omitempty"` } -type physicalLocation struct { - ArtifactLocation *artifactLocation `json:"artifactLocation"` - Region *region `json:"region"` +type toolComponentReference struct { + Name *string `json:"name"` + Index *uint `json:"index"` + Guid *string `json:"guid"` } -type region struct { - StartLine int `json:"startLine"` - StartColumn int `json:"startColumn"` +type Message struct { // https://docs.oasis-open.org/sarif/sarif/v2.1.0/csprd01/sarif-v2.1.0-csprd01.html#_Toc10540897 + Text *string `json:"text,omitempty"` + Markdown *string `json:"markdown,omitempty"` + Id *string `json:"id,omitempty"` + Arguments []string `json:"arguments,omitempty"` } -type artifactLocation struct { - URI string `json:"uri"` - Index int `json:"index"` + + + + + + +type multiformatMessageString struct { + Text string `json:"text"` + Markdown *string `json:"markdown,omitempty"` +} + +type address struct { + Index *uint `json:"index,omitempty"` + AbsoluteAddress *uint `json:"absoluteAddress,omitempty"` + RelativeAddress *int `json:"relativeAddress,omitempty"` + OffsetFromParent *int `json:"offsetFromParent,omitempty"` + Length *int `json:"length,omitempty"` + Name *string `json:"name,omitempty"` + FullyQualifiedName *string `json:"fullyQualifiedName,omitempty"` + Kind *string `json:"kind,omitempty"` + ParentIndex *uint `json:"parentIndex,omitempty"` +} + + + +type suppression struct { + Kind string `json:"kind"` + Status *string `json:"status"` + Location *location `json:"location"` + Guid *string `json:"guid"` + Justification *string `json:"justification"` +} + +type fix struct { + Description *Message `json:"description,omitempty"` + ArtifactChanges []*artifactChange `json:"artifactChanges"` // required +} + +type artifactChange struct { + ArtifactLocation artifactLocation `json:"artifactLocation"` + Replacements []*replacement `json:"replacements"` //required } -type location struct { - URI string `json:"uri"` +type replacement struct { + DeletedRegion region `json:"deletedRegion"` + InsertedContent *artifactContent `json:"insertedContent,omitempty"` } func newRuleResult(ruleID string) *Result { return &Result{ - RuleID: ruleID, + RuleID: &ruleID, } } // WithLevel specifies the level of the finding, error, warning for a result and returns the updated result func (result *Result) WithLevel(level string) *Result { - result.Level = level + result.Level = &level return result } // WithMessage specifies the message for a result and returns the updated result func (result *Result) WithMessage(message string) *Result { - result.Message = &textBlock{ - Text: message, - } + result.Message.Text = &message return result } +func (result *Result) NewMessageBuilder() *MessageBuilder { + return &MessageBuilder{ + message: &result.Message, + } +} + // WithLocationDetails specifies the location details of the Result and returns the update result func (result *Result) WithLocationDetails(path string, startLine, startColumn int) *Result { - location := &physicalLocation{ + physicalLocation := &physicalLocation{ ArtifactLocation: &artifactLocation{ - URI: path, + URI: &path, }, Region: ®ion{ - StartLine: startLine, - StartColumn: startColumn, + StartLine: &startLine, + StartColumn: &startColumn, }, } - result.Locations = append(result.Locations, &resultLocation{ - PhysicalLocation: location, + result.Locations = append(result.Locations, &location{ + PhysicalLocation: physicalLocation, }) return result } diff --git a/models/run.go b/models/run.go index 2fe7451..068e5b6 100644 --- a/models/run.go +++ b/models/run.go @@ -5,46 +5,44 @@ import ( ) // Run type represents a run of a tool -type Run struct { - Tool *tool `json:"tool"` - Artifacts []*LocationWrapper `json:"artifacts,omitempty"` - Results []*Result `json:"results,omitempty"` +type Run struct { // https://docs.oasis-open.org/sarif/sarif/v2.1.0/csprd01/sarif-v2.1.0-csprd01.html#_Toc10540922 + Tool tool `json:"tool"` + Artifacts []*artifact `json:"artifacts,omitempty"` + Results []*Result `json:"results,omitempty"` // can be null } -// LocationWrapper reprents the location details of a run -type LocationWrapper struct { - Location *location `json:"location,omitempty"` -} + // NewRun allows the creation of a new Run func NewRun(toolName, informationURI string) *Run { - tool := &tool{ - Driver: &driver{ - Name: toolName, - InformationURI: informationURI, - }, - } run := &Run{ - Tool: tool, + Tool: tool{ + Driver: &driver{ + Name: toolName, + InformationURI: informationURI, + }, + }, } return run } // AddArtifact returns the index of an existing artefact, the newly added artifactLocation -func (run *Run) AddArtifact(artifactLocation string) int { +func (run *Run) AddArtifact(uri string) uint { for i, l := range run.Artifacts { - if l.Location.URI == artifactLocation { - return i + if *l.Location.URI == uri { + return uint(i) } } - run.Artifacts = append(run.Artifacts, &LocationWrapper{ - Location: &location{ - URI: artifactLocation, + run.Artifacts = append(run.Artifacts, &artifact{ + Location: &artifactLocation{ + URI: &uri, }, }) - return len(run.Artifacts) - 1 + return uint(len(run.Artifacts) - 1) } + + // AddRule returns an existing Rule for the ruleID or creates a new Rule and returns it func (run *Run) AddRule(ruleID string) *Rule { for _, rule := range run.Tool.Driver.Rules { @@ -60,7 +58,7 @@ func (run *Run) AddRule(ruleID string) *Rule { // AddResult returns an existing Result or creates a new one and returns it func (run *Run) AddResult(ruleID string) *Result { for _, result := range run.Results { - if result.RuleID == ruleID { + if *result.RuleID == ruleID { return result } } @@ -72,22 +70,22 @@ func (run *Run) AddResult(ruleID string) *Result { // AddResultDetails adds rules to the driver and artifact locations if they are missing. It adds the result to the result block as well func (run *Run) AddResultDetails(rule *Rule, result *Result, location string) { ruleIndex := run.Tool.Driver.getOrCreateRule(rule) - result.RuleIndex = ruleIndex + result.RuleIndex = &ruleIndex locationIndex := run.AddArtifact(location) updateResultLocationIndex(result, location, locationIndex) } -func updateResultLocationIndex(result *Result, location string, index int) { +func updateResultLocationIndex(result *Result, location string, index uint) { for _, resultLocation := range result.Locations { - if resultLocation.PhysicalLocation.ArtifactLocation.URI == location { - resultLocation.PhysicalLocation.ArtifactLocation.Index = index + if *resultLocation.PhysicalLocation.ArtifactLocation.URI == location { + resultLocation.PhysicalLocation.ArtifactLocation.Index = &index break } } } func (run *Run) GetRuleById(ruleId string) (*Rule, error) { - if run.Tool != nil || run.Tool.Driver != nil { + if run.Tool.Driver != nil { for _, rule := range run.Tool.Driver.Rules { if rule.ID == ruleId { return rule, nil diff --git a/models/tool.go b/models/tool.go index 1412cb7..9cb40e6 100644 --- a/models/tool.go +++ b/models/tool.go @@ -19,14 +19,14 @@ type Rule struct { Properties map[string]string `json:"properties,omitempty"` } -func (driver *driver) getOrCreateRule(rule *Rule) int { +func (driver *driver) getOrCreateRule(rule *Rule) uint { for i, r := range driver.Rules { if r.ID == rule.ID { - return i + return uint(i) } } driver.Rules = append(driver.Rules, rule) - return len(driver.Rules) - 1 + return uint(len(driver.Rules) - 1) } func newRule(ruleID string) *Rule { diff --git a/test/result_stage_test.go b/test/result_stage_test.go index ff808cd..b22a7e7 100644 --- a/test/result_stage_test.go +++ b/test/result_stage_test.go @@ -22,8 +22,9 @@ func createNewResultTest(t *testing.T) (*resultTest, *resultTest, *resultTest) { } func (rt *resultTest) aNewResult() { + id := "test-rule" rt.result = &models.Result{ - RuleID: "test-rule", + RuleID: &id, } rt.result.WithLevel("error"). diff --git a/test/result_test.go b/test/result_test.go index 8802653..e547873 100644 --- a/test/result_test.go +++ b/test/result_test.go @@ -7,7 +7,7 @@ import ( func Test_a_new_result_is_created_as_expected(t *testing.T) { given, when, then := createNewResultTest(t) - expected := `{"level":"error","message":{"text":"there was an error"},"ruleId":"test-rule","ruleIndex":0}` + expected := `{"ruleId":"test-rule","level":"error","message":{"text":"there was an error"}}` given.aNewResult() when.theResultIsDisplayedConvertedAString() @@ -17,7 +17,7 @@ func Test_a_new_result_is_created_as_expected(t *testing.T) { func Test_a_new_result_is_created_with_a_location(t *testing.T) { given, when, then := createNewResultTest(t) - expected := `{"level":"error","message":{"text":"there was an error"},"ruleId":"test-rule","ruleIndex":0,"locations":[{"physicalLocation":{"artifactLocation":{"uri":"/tmp/code/location","index":0},"region":{"startLine":1,"startColumn":1}}}]}` + expected := `{"ruleId":"test-rule","level":"error","message":{"text":"there was an error"},"locations":[{"physicalLocation":{"artifactLocation":{"uri":"/tmp/code/location"},"region":{"startLine":1,"startColumn":1}}}]}` given.aNewResult() when.theResultHasALocationAdded(). diff --git a/test/run_stage_test.go b/test/run_stage_test.go index f3f2dbb..ce07ff5 100644 --- a/test/run_stage_test.go +++ b/test/run_stage_test.go @@ -47,7 +47,7 @@ func (rt *runTest) and() *runTest { return rt } -func (rt *runTest) theIndexOfLocationIs(locationURI string, expectedIndex int) *runTest { +func (rt *runTest) theIndexOfLocationIs(locationURI string, expectedIndex uint) *runTest { locationIndex := rt.run.AddArtifact(locationURI) assert.Equal(rt.t, expectedIndex, locationIndex) return rt diff --git a/test/run_test.go b/test/run_test.go index 047f43b..a9b27fc 100644 --- a/test/run_test.go +++ b/test/run_test.go @@ -17,7 +17,7 @@ func Test_create_new_empty_run_looks_as_expected(t *testing.T) { func Test_create_run_with_artifact_as_expected(t *testing.T) { given, when, then := createNewRunTest(t) - expected := `{"tool":{"driver":{"name":"tfsec","informationUri":"https://tfsec.dev"}},"artifacts":[{"location":{"uri":"/tmp/code/location"}}]}` + expected := `{"tool":{"driver":{"name":"tfsec","informationUri":"https://tfsec.dev"}},"artifacts":[{"location":{"uri":"/tmp/code/location"},"length":0}]}` given.aNewRunIsCreated() when.anArtifactIsAddedToTheRun("/tmp/code/location"). @@ -42,7 +42,7 @@ func Test_getting_the_location_index_for_an_existing_run(t *testing.T) { func Test_create_a_run_with_a_result_added(t *testing.T) { given, when, then := createNewRunTest(t) - expected := `{"tool":{"driver":{"name":"tfsec","informationUri":"https://tfsec.dev","rules":[{"id":"AWS001","shortDescription":{"text":"S3 Bucket has an ACL defined which allows public access."},"helpUri":"https://www.tfsec.dev/docs/aws/AWS001","properties":{"propertyName":"propertyValue"}}]}},"artifacts":[{"location":{"uri":"/tmp/result/code"}}],"results":[{"level":"error","message":{"text":"Resource 'my_bucket' has an ACL which allows public access."},"ruleId":"AWS001","ruleIndex":0,"locations":[{"physicalLocation":{"artifactLocation":{"uri":"/tmp/result/code","index":0},"region":{"startLine":1,"startColumn":1}}}]}]}` + expected := `{"tool":{"driver":{"name":"tfsec","informationUri":"https://tfsec.dev","rules":[{"id":"AWS001","shortDescription":{"text":"S3 Bucket has an ACL defined which allows public access."},"helpUri":"https://www.tfsec.dev/docs/aws/AWS001","properties":{"propertyName":"propertyValue"}}]}},"artifacts":[{"location":{"uri":"/tmp/result/code"},"length":0}],"results":[{"ruleId":"AWS001","ruleIndex":0,"level":"error","message":{"text":"Resource 'my_bucket' has an ACL which allows public access."},"locations":[{"physicalLocation":{"artifactLocation":{"uri":"/tmp/result/code","index":0},"region":{"startLine":1,"startColumn":1}}}]}]}` given.aNewRunIsCreated() when.aResultIsAddedToTheRun().and(). @@ -53,7 +53,7 @@ func Test_create_a_run_with_a_result_added(t *testing.T) { func Test_create_a_run_with_a_result_added_and_help_text_provided(t *testing.T) { given, when, then := createNewRunTest(t) - expected := `{"tool":{"driver":{"name":"tfsec","informationUri":"https://tfsec.dev","rules":[{"id":"AWS001","shortDescription":{"text":"S3 Bucket has an ACL defined which allows public access."},"help":{"text":"you can learn more about this check https://www.tfsec.dev/docs/aws/AWS001"},"properties":{"propertyName":"propertyValue"}}]}},"artifacts":[{"location":{"uri":"/tmp/result/code"}}],"results":[{"level":"error","message":{"text":"Resource 'my_bucket' has an ACL which allows public access."},"ruleId":"AWS001","ruleIndex":0,"locations":[{"physicalLocation":{"artifactLocation":{"uri":"/tmp/result/code","index":0},"region":{"startLine":1,"startColumn":1}}}]}]}` + expected := `{"tool":{"driver":{"name":"tfsec","informationUri":"https://tfsec.dev","rules":[{"id":"AWS001","shortDescription":{"text":"S3 Bucket has an ACL defined which allows public access."},"help":{"text":"you can learn more about this check https://www.tfsec.dev/docs/aws/AWS001"},"properties":{"propertyName":"propertyValue"}}]}},"artifacts":[{"location":{"uri":"/tmp/result/code"},"length":0}],"results":[{"ruleId":"AWS001","ruleIndex":0,"level":"error","message":{"text":"Resource 'my_bucket' has an ACL which allows public access."},"locations":[{"physicalLocation":{"artifactLocation":{"uri":"/tmp/result/code","index":0},"region":{"startLine":1,"startColumn":1}}}]}]}` given.aNewRunIsCreated() when.aResultIsAddedToTheRunWithHelpText().and().