diff --git a/pkg/format/sarif.go b/pkg/format/sarif.go index 15ce51ee7..4d5907a88 100644 --- a/pkg/format/sarif.go +++ b/pkg/format/sarif.go @@ -22,15 +22,16 @@ type Runs struct { // Results these structs are relevant to the Results object type Results struct { - RuleID string `json:"ruleId"` - RuleIndex int `json:"ruleIndex"` - Level string `json:"level,omitempty"` - Message *Message `json:"message,omitempty"` - AnalysisTarget *ArtifactLocation `json:"analysisTarget,omitempty"` - Locations []Location `json:"locations,omitempty"` - CodeFlows []CodeFlow `json:"codeFlows,omitempty"` - RelatedLocations []RelatedLocation `json:"relatedLocations,omitempty"` - Properties *SarifProperties `json:"properties"` + RuleID string `json:"ruleId"` + RuleIndex int `json:"ruleIndex"` + Level string `json:"level,omitempty"` + Message *Message `json:"message,omitempty"` + AnalysisTarget *ArtifactLocation `json:"analysisTarget,omitempty"` + Locations []Location `json:"locations,omitempty"` + CodeFlows []CodeFlow `json:"codeFlows,omitempty"` + RelatedLocations []RelatedLocation `json:"relatedLocations,omitempty"` + PartialFingerprints PartialFingerprints `json:"partialFingerprints"` + Properties *SarifProperties `json:"properties"` } // Message to detail the finding @@ -54,8 +55,9 @@ type PhysicalLocation struct { // ArtifactLocation describing the path of the artifact type ArtifactLocation struct { - URI string `json:"uri"` - Index int `json:"index"` + URI string `json:"uri"` + URIBaseId string `json:"uriBaseId"` + Index int `json:"index"` } // Region where the finding was detected @@ -74,6 +76,13 @@ type LogicalLocation struct { FullyQualifiedName string `json:"fullyQualifiedName"` } +// PartialFingerprints +type PartialFingerprints struct { + FortifyInstanceID string `json:"fortifyInstanceID,omitempty"` + CheckmarxSimilarityID string `json:"checkmarxSimilarityID,omitempty"` + PrimaryLocationLineHash string `json:"primaryLocationLineHash,omitempty"` +} + // SarifProperties adding additional information/context to the finding type SarifProperties struct { InstanceID string `json:"instanceID,omitempty"` @@ -167,7 +176,7 @@ type RelatedPhysicalLocation struct { // RelatedRegion type RelatedRegion struct { - StartLine int `json:"startLine"` + StartLine int `json:"startLine,omitempty"` StartColumn int `json:"startColumn,omitempty"` } diff --git a/pkg/fortify/fpr_to_sarif.go b/pkg/fortify/fpr_to_sarif.go index de8fb25f3..e28329355 100644 --- a/pkg/fortify/fpr_to_sarif.go +++ b/pkg/fortify/fpr_to_sarif.go @@ -211,6 +211,7 @@ type Node struct { XMLName xml.Name `xml:"Node"` IsDefault string `xml:"isDefault,attr,omitempty"` NodeLabel string `xml:"label,attr,omitempty"` + ID int `xml:"id,attr,omitempty"` SourceLocation SourceLocation `xml:"SourceLocation"` Action Action `xml:"Action,omitempty"` Reason Reason `xml:"Reason,omitempty"` @@ -493,16 +494,6 @@ type Attribute struct { Value string `xml:"value"` } -// Utils - -func (n Node) isEmpty() bool { - return n.IsDefault == "" -} - -func (a Action) isEmpty() bool { - return a.ActionData == "" -} - // ConvertFprToSarif converts the FPR file contents into SARIF format func ConvertFprToSarif(sys System, project *models.Project, projectVersion *models.ProjectVersion, resultFilePath string, filterSet *models.FilterSet) (format.SARIF, error) { log.Entry().Debug("Extracting FPR.") @@ -576,16 +567,29 @@ func Parse(sys System, project *models.Project, projectVersion *models.ProjectVe log.Entry().Debug("[SARIF] Now handling results.") for i := 0; i < len(fvdl.Vulnerabilities.Vulnerability); i++ { result := *new(format.Results) - result.RuleID = fvdl.Vulnerabilities.Vulnerability[i].ClassInfo.ClassID + //result.RuleID = fvdl.Vulnerabilities.Vulnerability[i].ClassInfo.ClassID + // Handle ruleID the same way than in Rule + idArray := []string{} + if fvdl.Vulnerabilities.Vulnerability[i].ClassInfo.Kingdom != "" { + idArray = append(idArray, fvdl.Vulnerabilities.Vulnerability[i].ClassInfo.Kingdom) + } + if fvdl.Vulnerabilities.Vulnerability[i].ClassInfo.Type != "" { + idArray = append(idArray, fvdl.Vulnerabilities.Vulnerability[i].ClassInfo.Type) + } + if fvdl.Vulnerabilities.Vulnerability[i].ClassInfo.Subtype != "" { + idArray = append(idArray, fvdl.Vulnerabilities.Vulnerability[i].ClassInfo.Subtype) + } + result.RuleID = "fortify-" + strings.Join(idArray, "/") + // end handle result result.Level = "none" //TODO //get message for j := 0; j < len(fvdl.Description); j++ { - if fvdl.Description[j].ClassID == result.RuleID { - result.RuleIndex = j //Seems very abstract - rawMessage := fvdl.Description[j].Abstract.Text + if fvdl.Description[j].ClassID == fvdl.Vulnerabilities.Vulnerability[i].ClassInfo.ClassID { + result.RuleIndex = j + rawMessage := unescapeXML(fvdl.Description[j].Abstract.Text) // Replacement defintions in message for l := 0; l < len(fvdl.Vulnerabilities.Vulnerability[i].AnalysisInfo.ReplacementDefinitions.Def); l++ { - rawMessage = strings.ReplaceAll(rawMessage, "Replace key=\""+fvdl.Vulnerabilities.Vulnerability[i].AnalysisInfo.ReplacementDefinitions.Def[l].DefKey+"\"", fvdl.Vulnerabilities.Vulnerability[i].AnalysisInfo.ReplacementDefinitions.Def[l].DefValue) + rawMessage = strings.ReplaceAll(rawMessage, "", fvdl.Vulnerabilities.Vulnerability[i].AnalysisInfo.ReplacementDefinitions.Def[l].DefValue) } msg := new(format.Message) msg.Text = rawMessage @@ -596,7 +600,6 @@ func Parse(sys System, project *models.Project, projectVersion *models.ProjectVe // Handle all locations items location := *new(format.Location) - var startingColumn int //get location for k := 0; k < len(fvdl.Vulnerabilities.Vulnerability[i].AnalysisInfo.Trace); k++ { // k iterates on traces //In each trace/primary, there can be one or more entries @@ -605,100 +608,142 @@ func Parse(sys System, project *models.Project, projectVersion *models.ProjectVe threadFlow := *new(format.ThreadFlow) //We now iterate on Entries in the trace/primary for l := 0; l < len(fvdl.Vulnerabilities.Vulnerability[i].AnalysisInfo.Trace[k].Primary.Entry); l++ { // l iterates on entries - threadFlowLocation := *new(format.Locations) //One is created regardless - //the default node dictates the interesting threadflow (location, and so on) + tfla := *new([]format.Locations) //threadflowlocationarray. Useful for the node-in-node edge case + threadFlowLocation := *new(format.Locations) //One is created regardless of the path taken afterwards //this will populate both threadFlowLocation AND the parent location object (result.Locations[0]) - if !fvdl.Vulnerabilities.Vulnerability[i].AnalysisInfo.Trace[k].Primary.Entry[l].Node.isEmpty() && fvdl.Vulnerabilities.Vulnerability[i].AnalysisInfo.Trace[k].Primary.Entry[l].Node.IsDefault == "true" { - //initalize threadFlowLocation.Location - threadFlowLocation.Location = new(format.Location) + // We check if a noderef is present: if no (index of ref is the default 0), this is a "real" node. As a measure of safety (in case a node refers to nodeid 0), we add another check: the node must have a label or a isdefault value + if fvdl.Vulnerabilities.Vulnerability[i].AnalysisInfo.Trace[k].Primary.Entry[l].NodeRef.RefId == 0 && (fvdl.Vulnerabilities.Vulnerability[i].AnalysisInfo.Trace[k].Primary.Entry[l].Node.NodeLabel != "" || fvdl.Vulnerabilities.Vulnerability[i].AnalysisInfo.Trace[k].Primary.Entry[l].Node.IsDefault != "") { + //initalize the current location object, it will be added to threadFlowLocation.Location + tfloc := new(format.Location) //get artifact location for j := 0; j < len(fvdl.Build.SourceFiles); j++ { // j iterates on source files if fvdl.Build.SourceFiles[j].Name == fvdl.Vulnerabilities.Vulnerability[i].AnalysisInfo.Trace[k].Primary.Entry[l].Node.SourceLocation.Path { - threadFlowLocation.Location.PhysicalLocation.ArtifactLocation.Index = j + tfloc.PhysicalLocation.ArtifactLocation.Index = j + 1 + tfloc.PhysicalLocation.ArtifactLocation.URI = fvdl.Vulnerabilities.Vulnerability[i].AnalysisInfo.Trace[k].Primary.Entry[l].Node.SourceLocation.Path + tfloc.PhysicalLocation.ArtifactLocation.URIBaseId = "%SRCROOT%" break } } //get region & context region - threadFlowLocation.Location.PhysicalLocation.Region.StartLine = fvdl.Vulnerabilities.Vulnerability[i].AnalysisInfo.Trace[k].Primary.Entry[l].Node.SourceLocation.Line + tfloc.PhysicalLocation.Region.StartLine = fvdl.Vulnerabilities.Vulnerability[i].AnalysisInfo.Trace[k].Primary.Entry[l].Node.SourceLocation.Line + tfloc.PhysicalLocation.Region.EndLine = fvdl.Vulnerabilities.Vulnerability[i].AnalysisInfo.Trace[k].Primary.Entry[l].Node.SourceLocation.LineEnd + tfloc.PhysicalLocation.Region.StartColumn = fvdl.Vulnerabilities.Vulnerability[i].AnalysisInfo.Trace[k].Primary.Entry[l].Node.SourceLocation.ColStart + tfloc.PhysicalLocation.Region.EndColumn = fvdl.Vulnerabilities.Vulnerability[i].AnalysisInfo.Trace[k].Primary.Entry[l].Node.SourceLocation.ColEnd //Snippet is handled last - //threadFlowLocation.Location.PhysicalLocation.Region.Snippet.Text = "foobar" targetSnippetId := fvdl.Vulnerabilities.Vulnerability[i].AnalysisInfo.Trace[k].Primary.Entry[l].Node.SourceLocation.Snippet for j := 0; j < len(fvdl.Snippets); j++ { if fvdl.Snippets[j].SnippetId == targetSnippetId { - threadFlowLocation.Location.PhysicalLocation.ContextRegion.StartLine = fvdl.Snippets[j].StartLine - threadFlowLocation.Location.PhysicalLocation.ContextRegion.EndLine = fvdl.Snippets[j].EndLine + tfloc.PhysicalLocation.ContextRegion.StartLine = fvdl.Snippets[j].StartLine + tfloc.PhysicalLocation.ContextRegion.EndLine = fvdl.Snippets[j].EndLine snippetSarif := new(format.SnippetSarif) snippetSarif.Text = fvdl.Snippets[j].Text - threadFlowLocation.Location.PhysicalLocation.ContextRegion.Snippet = snippetSarif + tfloc.PhysicalLocation.ContextRegion.Snippet = snippetSarif break } } - //parse SourceLocation object for the startColumn value, store it appropriately - startingColumn = fvdl.Vulnerabilities.Vulnerability[i].AnalysisInfo.Trace[k].Primary.Entry[l].Node.SourceLocation.ColStart - //check for existance of action object, and if yes, save message - if !fvdl.Vulnerabilities.Vulnerability[i].AnalysisInfo.Trace[k].Primary.Entry[l].Node.Action.isEmpty() { - threadFlowLocation.Location.Message = new(format.Message) - threadFlowLocation.Location.Message.Text = fvdl.Vulnerabilities.Vulnerability[i].AnalysisInfo.Trace[k].Primary.Entry[l].Node.Action.ActionData - // Handle snippet - snippetTarget := "" - switch fvdl.Vulnerabilities.Vulnerability[i].AnalysisInfo.Trace[k].Primary.Entry[l].Node.Action.Type { - case "Assign": - snippetWords := strings.Split(fvdl.Vulnerabilities.Vulnerability[i].AnalysisInfo.Trace[k].Primary.Entry[l].Node.Action.ActionData, " ") - if snippetWords[0] == "Assignment" { - snippetTarget = snippetWords[2] - } else { - snippetTarget = fvdl.Vulnerabilities.Vulnerability[i].AnalysisInfo.Trace[k].Primary.Entry[l].Node.Action.ActionData + // if a label is passed, put it as message + if fvdl.Vulnerabilities.Vulnerability[i].AnalysisInfo.Trace[k].Primary.Entry[l].Node.NodeLabel != "" { + tfloc.Message = new(format.Message) + tfloc.Message.Text = fvdl.Vulnerabilities.Vulnerability[i].AnalysisInfo.Trace[k].Primary.Entry[l].Node.NodeLabel + } else { + // otherwise check for existance of action object, and if yes, save message + if !(fvdl.Vulnerabilities.Vulnerability[i].AnalysisInfo.Trace[k].Primary.Entry[l].Node.Action.ActionData == "") { + tfloc.Message = new(format.Message) + tfloc.Message.Text = fvdl.Vulnerabilities.Vulnerability[i].AnalysisInfo.Trace[k].Primary.Entry[l].Node.Action.ActionData + + // Handle snippet + snippetTarget := handleSnippet(fvdl.Vulnerabilities.Vulnerability[i].AnalysisInfo.Trace[k].Primary.Entry[l].Node.Action.Type, fvdl.Vulnerabilities.Vulnerability[i].AnalysisInfo.Trace[k].Primary.Entry[l].Node.Action.ActionData) + + if tfloc.PhysicalLocation.ContextRegion.Snippet != nil { + physLocationSnippetLines := strings.Split(tfloc.PhysicalLocation.ContextRegion.Snippet.Text, "\n") + snippetText := "" + for j := 0; j < len(physLocationSnippetLines); j++ { + if strings.Contains(physLocationSnippetLines[j], snippetTarget) { + snippetText = physLocationSnippetLines[j] + break + } + } + snippetSarif := new(format.SnippetSarif) + if snippetText != "" { + snippetSarif.Text = snippetText + } else { + snippetSarif.Text = tfloc.PhysicalLocation.ContextRegion.Snippet.Text + } + tfloc.PhysicalLocation.Region.Snippet = snippetSarif } - case "InCall": - snippetTarget = strings.Split(fvdl.Vulnerabilities.Vulnerability[i].AnalysisInfo.Trace[k].Primary.Entry[l].Node.Action.ActionData, "(")[0] - case "OutCall": - snippetTarget = strings.Split(fvdl.Vulnerabilities.Vulnerability[i].AnalysisInfo.Trace[k].Primary.Entry[l].Node.Action.ActionData, "(")[0] - case "InOutCall": - snippetTarget = strings.Split(fvdl.Vulnerabilities.Vulnerability[i].AnalysisInfo.Trace[k].Primary.Entry[l].Node.Action.ActionData, "(")[0] - case "Return": - snippetTarget = fvdl.Vulnerabilities.Vulnerability[i].AnalysisInfo.Trace[k].Primary.Entry[l].Node.Action.ActionData - case "Read": - snippetWords := strings.Split(fvdl.Vulnerabilities.Vulnerability[i].AnalysisInfo.Trace[k].Primary.Entry[l].Node.Action.ActionData, " ") - if len(snippetWords) > 1 { - snippetTarget = " " + snippetWords[1] - } else { - snippetTarget = snippetWords[0] + } else { + if tfloc.PhysicalLocation.ContextRegion.Snippet != nil { + snippetSarif := new(format.SnippetSarif) + snippetSarif.Text = tfloc.PhysicalLocation.ContextRegion.Snippet.Text + tfloc.PhysicalLocation.Region.Snippet = snippetSarif } - default: - snippetTarget = fvdl.Vulnerabilities.Vulnerability[i].AnalysisInfo.Trace[k].Primary.Entry[l].Node.Action.ActionData } - if threadFlowLocation.Location.PhysicalLocation.ContextRegion.Snippet != nil { - physLocationSnippetLines := strings.Split(threadFlowLocation.Location.PhysicalLocation.ContextRegion.Snippet.Text, "\n") - snippetText := "" - for j := 0; j < len(physLocationSnippetLines); j++ { - if strings.Contains(physLocationSnippetLines[j], snippetTarget) { - snippetText = physLocationSnippetLines[j] + } + location = *tfloc + //set Kinds + threadFlowLocation.Location = tfloc + threadFlowLocation.Kinds = append(threadFlowLocation.Kinds, "review") //TODO + threadFlowLocation.Index = 0 // to be safe? + tfla = append(tfla, threadFlowLocation) + + // "Node-in-node" edge case! in some cases the "Reason" object will contain a "Trace>Primary>Entry>Node" object + // Check for it at depth 1 only, as an in-case + if len(fvdl.Vulnerabilities.Vulnerability[i].AnalysisInfo.Trace[k].Primary.Entry[l].Node.Reason.Trace.Primary.Entry) > 0 { + ninThreadFlowLocation := *new(format.Locations) + if fvdl.Vulnerabilities.Vulnerability[i].AnalysisInfo.Trace[k].Primary.Entry[l].Node.Reason.Trace.Primary.Entry[0].NodeRef.RefId != 0 { + // As usual, only the index for a ref + ninThreadFlowLocation.Index = fvdl.Vulnerabilities.Vulnerability[i].AnalysisInfo.Trace[k].Primary.Entry[l].Node.Reason.Trace.Primary.Entry[0].NodeRef.RefId + 1 + } else { + // Build a new "node-in-node" tfloc, it will be appended to tfla + nintfloc := new(format.Location) + + // artifactlocation + for j := 0; j < len(fvdl.Build.SourceFiles); j++ { + if fvdl.Build.SourceFiles[j].Name == fvdl.Vulnerabilities.Vulnerability[i].AnalysisInfo.Trace[k].Primary.Entry[l].Node.Reason.Trace.Primary.Entry[0].Node.SourceLocation.Path { + nintfloc.PhysicalLocation.ArtifactLocation.Index = j + 1 + nintfloc.PhysicalLocation.ArtifactLocation.URI = fvdl.Vulnerabilities.Vulnerability[i].AnalysisInfo.Trace[k].Primary.Entry[l].Node.Reason.Trace.Primary.Entry[0].Node.SourceLocation.Path + nintfloc.PhysicalLocation.ArtifactLocation.URIBaseId = "%SRCROOT%" break } } - snippetSarif := new(format.SnippetSarif) - if snippetText != "" { - snippetSarif.Text = snippetText - } else { - snippetSarif.Text = threadFlowLocation.Location.PhysicalLocation.ContextRegion.Snippet.Text + + // region & context region + nintfloc.PhysicalLocation.Region.StartLine = fvdl.Vulnerabilities.Vulnerability[i].AnalysisInfo.Trace[k].Primary.Entry[l].Node.Reason.Trace.Primary.Entry[0].Node.SourceLocation.Line + nintfloc.PhysicalLocation.Region.EndLine = fvdl.Vulnerabilities.Vulnerability[i].AnalysisInfo.Trace[k].Primary.Entry[l].Node.Reason.Trace.Primary.Entry[0].Node.SourceLocation.LineEnd + nintfloc.PhysicalLocation.Region.StartColumn = fvdl.Vulnerabilities.Vulnerability[i].AnalysisInfo.Trace[k].Primary.Entry[l].Node.Reason.Trace.Primary.Entry[0].Node.SourceLocation.ColStart + nintfloc.PhysicalLocation.Region.EndColumn = fvdl.Vulnerabilities.Vulnerability[i].AnalysisInfo.Trace[k].Primary.Entry[l].Node.Reason.Trace.Primary.Entry[0].Node.SourceLocation.ColEnd + // snippet + targetSnippetId := fvdl.Vulnerabilities.Vulnerability[i].AnalysisInfo.Trace[k].Primary.Entry[l].Node.Reason.Trace.Primary.Entry[0].Node.SourceLocation.Snippet + for j := 0; j < len(fvdl.Snippets); j++ { + if fvdl.Snippets[j].SnippetId == targetSnippetId { + nintfloc.PhysicalLocation.ContextRegion.StartLine = fvdl.Snippets[j].StartLine + nintfloc.PhysicalLocation.ContextRegion.EndLine = fvdl.Snippets[j].EndLine + snippetSarif := new(format.SnippetSarif) + snippetSarif.Text = fvdl.Snippets[j].Text + nintfloc.PhysicalLocation.ContextRegion.Snippet = snippetSarif + break + } } - threadFlowLocation.Location.PhysicalLocation.Region.Snippet = snippetSarif - } - } else { - if threadFlowLocation.Location.PhysicalLocation.ContextRegion.Snippet != nil { - snippetSarif := new(format.SnippetSarif) - snippetSarif.Text = threadFlowLocation.Location.PhysicalLocation.ContextRegion.Snippet.Text - threadFlowLocation.Location.PhysicalLocation.Region.Snippet = snippetSarif + // label as message + if fvdl.Vulnerabilities.Vulnerability[i].AnalysisInfo.Trace[k].Primary.Entry[l].Node.Reason.Trace.Primary.Entry[0].Node.NodeLabel != "" { + nintfloc.Message = new(format.Message) + nintfloc.Message.Text = fvdl.Vulnerabilities.Vulnerability[i].AnalysisInfo.Trace[k].Primary.Entry[l].Node.Reason.Trace.Primary.Entry[0].Node.NodeLabel + } + + ninThreadFlowLocation.Location = nintfloc + ninThreadFlowLocation.Index = 0 // Safety } + tfla = append(tfla, ninThreadFlowLocation) } - location = *threadFlowLocation.Location - //set Kinds - threadFlowLocation.Kinds = append(threadFlowLocation.Kinds, "unknown") //TODO + // END edge case + } else { //is not a main threadflow: just register NodeRef index in threadFlowLocation - threadFlowLocation.Index = fvdl.Vulnerabilities.Vulnerability[i].AnalysisInfo.Trace[k].Primary.Entry[l].NodeRef.RefId + // Sarif does not provision 0 as a valid array index, so we increment the node ref id + // Each index i serves to reference the i-th object in run.threadFlowLocations + threadFlowLocation.Index = fvdl.Vulnerabilities.Vulnerability[i].AnalysisInfo.Trace[k].Primary.Entry[l].NodeRef.RefId + 1 + tfla = append(tfla, threadFlowLocation) } - //add the threadflowlocation to the list of locations - threadFlow.Locations = append(threadFlow.Locations, threadFlowLocation) + threadFlow.Locations = append(threadFlow.Locations, tfla...) } codeFlow.ThreadFlows = append(codeFlow.ThreadFlows, threadFlow) result.CodeFlows = append(result.CodeFlows, codeFlow) @@ -716,23 +761,20 @@ func Parse(sys System, project *models.Project, projectVersion *models.ProjectVe relatedLocation.PhysicalLocation.ArtifactLocation = location.PhysicalLocation.ArtifactLocation relatedLocation.PhysicalLocation.Region = *new(format.RelatedRegion) relatedLocation.PhysicalLocation.Region.StartLine = location.PhysicalLocation.Region.StartLine - relatedLocation.PhysicalLocation.Region.StartColumn = startingColumn + relatedLocation.PhysicalLocation.Region.StartColumn = location.PhysicalLocation.Region.StartColumn result.RelatedLocations = append(result.RelatedLocations, relatedLocation) + //handle partialFingerprints + result.PartialFingerprints.FortifyInstanceID = fvdl.Vulnerabilities.Vulnerability[i].InstanceInfo.InstanceID + result.PartialFingerprints.PrimaryLocationLineHash = fvdl.Vulnerabilities.Vulnerability[i].InstanceInfo.InstanceID //Fixit + //handle properties prop := new(format.SarifProperties) prop.InstanceSeverity = fvdl.Vulnerabilities.Vulnerability[i].InstanceInfo.InstanceSeverity prop.Confidence = fvdl.Vulnerabilities.Vulnerability[i].InstanceInfo.Confidence prop.InstanceID = fvdl.Vulnerabilities.Vulnerability[i].InstanceInfo.InstanceID - //Use a query to get the audit data - // B5C0FEFD-CCB2-4F21-A9D7-87AE600A5885 is "custom rules": handle differently? - if result.RuleID == "B5C0FEFD-CCB2-4F21-A9D7-87AE600A5885" { - // Custom Rules has no audit value: it's notificaiton in the FVDL only. - prop.Audited = true - prop.ToolAuditMessage = "Custom Rules: not a vuln" - prop.ToolState = "Not an Issue" - prop.ToolStateIndex = 1 - } else if sys != nil { + //Get the audit data + if sys != nil { if err := integrateAuditData(prop, fvdl.Vulnerabilities.Vulnerability[i].InstanceInfo.InstanceID, sys, project, projectVersion, auditData, filterSet, oneRequestPerIssueMode); err != nil { log.Entry().Debug(err) prop.Audited = false @@ -765,121 +807,141 @@ func Parse(sys System, project *models.Project, projectVersion *models.ProjectVe for j := 0; j < len(fvdl.Vulnerabilities.Vulnerability); j++ { //j iterates on vulns to find the name if fvdl.Vulnerabilities.Vulnerability[j].ClassInfo.ClassID == fvdl.EngineData.RuleInfo[i].RuleID { var nameArray []string + var idArray []string if fvdl.Vulnerabilities.Vulnerability[j].ClassInfo.Kingdom != "" { - nameArray = append(nameArray, fvdl.Vulnerabilities.Vulnerability[j].ClassInfo.Kingdom) + idArray = append(idArray, fvdl.Vulnerabilities.Vulnerability[j].ClassInfo.Kingdom) + words := strings.Split(fvdl.Vulnerabilities.Vulnerability[j].ClassInfo.Kingdom, " ") + for index, element := range words { // These are required to ensure that titlecase is respected in titles, part of sarif "friendly name" rules + words[index] = strings.Title(strings.ToLower(element)) + } + nameArray = append(nameArray, words...) } if fvdl.Vulnerabilities.Vulnerability[j].ClassInfo.Type != "" { - nameArray = append(nameArray, fvdl.Vulnerabilities.Vulnerability[j].ClassInfo.Type) + idArray = append(idArray, fvdl.Vulnerabilities.Vulnerability[j].ClassInfo.Type) + words := strings.Split(fvdl.Vulnerabilities.Vulnerability[j].ClassInfo.Type, " ") + for index, element := range words { + words[index] = strings.Title(strings.ToLower(element)) + } + nameArray = append(nameArray, words...) } if fvdl.Vulnerabilities.Vulnerability[j].ClassInfo.Subtype != "" { - nameArray = append(nameArray, fvdl.Vulnerabilities.Vulnerability[j].ClassInfo.Subtype) + idArray = append(idArray, fvdl.Vulnerabilities.Vulnerability[j].ClassInfo.Subtype) + words := strings.Split(fvdl.Vulnerabilities.Vulnerability[j].ClassInfo.Subtype, " ") + for index, element := range words { + words[index] = strings.Title(strings.ToLower(element)) + } + nameArray = append(nameArray, words...) } - sarifRule.Name = strings.Join(nameArray, "/") + sarifRule.ID = "fortify-" + strings.Join(idArray, "/") + sarifRule.Name = strings.Join(nameArray, "") defaultConfig := new(format.DefaultConfiguration) + defaultConfig.Level = "warning" // Default value defaultConfig.Properties.DefaultSeverity = fvdl.Vulnerabilities.Vulnerability[j].ClassInfo.DefaultSeverity sarifRule.DefaultConfiguration = defaultConfig - break - } - } - //Descriptions - for j := 0; j < len(fvdl.Description); j++ { - if fvdl.Description[j].ClassID == sarifRule.ID { - rawAbstract := fvdl.Description[j].Abstract.Text - rawExplanation := fvdl.Description[j].Explanation.Text - // Replacement defintions in abstract/explanation - for k := 0; k < len(fvdl.Vulnerabilities.Vulnerability); k++ { // Iterate on vulns to find the correct one (where ReplacementDefinitions are) - if fvdl.Vulnerabilities.Vulnerability[k].ClassInfo.ClassID == fvdl.Description[j].ClassID { - for l := 0; l < len(fvdl.Vulnerabilities.Vulnerability[k].AnalysisInfo.ReplacementDefinitions.Def); l++ { - rawAbstract = strings.ReplaceAll(rawAbstract, "Replace key=\""+fvdl.Vulnerabilities.Vulnerability[k].AnalysisInfo.ReplacementDefinitions.Def[l].DefKey+"\"", fvdl.Vulnerabilities.Vulnerability[k].AnalysisInfo.ReplacementDefinitions.Def[l].DefValue) - rawExplanation = strings.ReplaceAll(rawExplanation, "Replace key=\""+fvdl.Vulnerabilities.Vulnerability[k].AnalysisInfo.ReplacementDefinitions.Def[l].DefKey+"\"", fvdl.Vulnerabilities.Vulnerability[k].AnalysisInfo.ReplacementDefinitions.Def[l].DefValue) + + //Descriptions + for j := 0; j < len(fvdl.Description); j++ { + if fvdl.Description[j].ClassID == sarifRule.GUID { + rawAbstract := unescapeXML(fvdl.Description[j].Abstract.Text) + rawExplanation := unescapeXML(fvdl.Description[j].Explanation.Text) + + // Replacement defintions in abstract/explanation + for k := 0; k < len(fvdl.Vulnerabilities.Vulnerability); k++ { // Iterate on vulns to find the correct one (where ReplacementDefinitions are) + if fvdl.Vulnerabilities.Vulnerability[k].ClassInfo.ClassID == fvdl.Description[j].ClassID { + for l := 0; l < len(fvdl.Vulnerabilities.Vulnerability[k].AnalysisInfo.ReplacementDefinitions.Def); l++ { + rawAbstract = strings.ReplaceAll(rawAbstract, "", fvdl.Vulnerabilities.Vulnerability[k].AnalysisInfo.ReplacementDefinitions.Def[l].DefValue) + rawExplanation = strings.ReplaceAll(rawExplanation, "", fvdl.Vulnerabilities.Vulnerability[k].AnalysisInfo.ReplacementDefinitions.Def[l].DefValue) + } + // Replacement locationdef in explanation + for l := 0; l < len(fvdl.Vulnerabilities.Vulnerability[k].AnalysisInfo.ReplacementDefinitions.LocationDef); l++ { + rawExplanation = strings.ReplaceAll(rawExplanation, fvdl.Vulnerabilities.Vulnerability[k].AnalysisInfo.ReplacementDefinitions.LocationDef[l].Key, fvdl.Vulnerabilities.Vulnerability[k].AnalysisInfo.ReplacementDefinitions.LocationDef[l].Path) + } + // If Description has a CustomDescription, add it for good measure + if fvdl.Description[j].CustomDescription.RuleID != "" { + rawExplanation = rawExplanation + "\n;" + fvdl.Description[j].CustomDescription.Explanation.Text + } + sd := new(format.Message) + sd.Text = rawAbstract + sarifRule.ShortDescription = sd + fd := new(format.Message) + fd.Text = rawExplanation + sarifRule.FullDescription = fd + break + } } - // Replacement locationdef in explanation - for l := 0; l < len(fvdl.Vulnerabilities.Vulnerability[k].AnalysisInfo.ReplacementDefinitions.LocationDef); l++ { - rawExplanation = strings.ReplaceAll(rawExplanation, fvdl.Vulnerabilities.Vulnerability[k].AnalysisInfo.ReplacementDefinitions.LocationDef[l].Key, fvdl.Vulnerabilities.Vulnerability[k].AnalysisInfo.ReplacementDefinitions.LocationDef[l].Path) - } - // If Description has a CustomDescription, add it for good measure - if fvdl.Description[j].CustomDescription.RuleID != "" { - rawExplanation = rawExplanation + "\n;" + fvdl.Description[j].CustomDescription.Explanation.Text - } - sd := new(format.Message) - sd.Text = rawAbstract - sarifRule.ShortDescription = sd - fd := new(format.Message) - fd.Text = rawExplanation - sarifRule.FullDescription = fd break } } + + //properties + //Prepare a CWE id object as an in-case + cweIds := []string{} + //scan for the properties we want: + var propArray [][]string + for j := 0; j < len(fvdl.EngineData.RuleInfo[i].MetaInfoGroup); j++ { + if (fvdl.EngineData.RuleInfo[i].MetaInfoGroup[j].Name == "Accuracy") || (fvdl.EngineData.RuleInfo[i].MetaInfoGroup[j].Name == "Impact") || (fvdl.EngineData.RuleInfo[i].MetaInfoGroup[j].Name == "Probability") { + propArray = append(propArray, []string{fvdl.EngineData.RuleInfo[i].MetaInfoGroup[j].Name, fvdl.EngineData.RuleInfo[i].MetaInfoGroup[j].Data}) + } else if fvdl.EngineData.RuleInfo[i].MetaInfoGroup[j].Name == "altcategoryCWE" { + //Get all CWE IDs. First, split on ", " + rawCweIds := strings.Split(fvdl.EngineData.RuleInfo[i].MetaInfoGroup[j].Data, ", ") + //If not "None", split each string on " " and add its 2nd index + if rawCweIds[0] != "None" { + for k := 0; k < len(rawCweIds); k++ { + cweId := strings.Split(rawCweIds[k], " ")[2] + //Fill the cweIdsForTaxonomies map if not already in + if _, isIn := cweIdsForTaxonomies[cweId]; !isIn { + cweIdsForTaxonomies[cweId] = cweId + } + cweIds = append(cweIds, cweId) + } + } else { + cweIds = append(cweIds, rawCweIds[0]) + } + } + } + var ruleProp *format.SarifRuleProperties + if len(propArray) != 0 { + ruleProp = new(format.SarifRuleProperties) + for j := 0; j < len(propArray); j++ { + if propArray[j][0] == "Accuracy" { + ruleProp.Accuracy = propArray[j][1] + } else if propArray[j][0] == "Impact" { + ruleProp.Impact = propArray[j][1] + } else if propArray[j][0] == "Probability" { + ruleProp.Probability = propArray[j][1] + } + } + } + sarifRule.Properties = ruleProp + + //relationships: will most likely require some expansion + //One relationship per CWE id + for j := 0; j < len(cweIds); j++ { + sarifRule.Properties.Tags = append(sarifRule.Properties.Tags, "external/cwe/cwe-"+cweIds[j]) + + rls := *new(format.Relationships) + rls.Target.Id = cweIds[j] + rls.Target.ToolComponent.Name = "CWE" + rls.Target.ToolComponent.Guid = "25F72D7E-8A92-459D-AD67-64853F788765" + rls.Kinds = append(rls.Kinds, "relevant") + sarifRule.Relationships = append(sarifRule.Relationships, rls) + } + + // Add a helpURI as some processors require it + sarifRule.HelpURI = "https://vulncat.fortify.com/en/weakness" + + //Finalize: append the rule + tool.Driver.Rules = append(tool.Driver.Rules, sarifRule) + + // A rule vuln has been found for this rule, no need to keep iterating break } } - // Avoid empty descriptions to respect standard - //if sarifRule.ShortDescription.Text == "" { - // sarifRule.ShortDescription.Text = "None." - //} - //if sarifRule.FullDescription.Text == "" { // OR USE OMITEMPTY - // sarifRule.FullDescription.Text = "None." - //} - - //properties - //Prepare a CWE id object as an in-case - cweIds := []string{} - //scan for the properties we want: - var propArray [][]string - for j := 0; j < len(fvdl.EngineData.RuleInfo[i].MetaInfoGroup); j++ { - if (fvdl.EngineData.RuleInfo[i].MetaInfoGroup[j].Name == "Accuracy") || (fvdl.EngineData.RuleInfo[i].MetaInfoGroup[j].Name == "Impact") || (fvdl.EngineData.RuleInfo[i].MetaInfoGroup[j].Name == "Probability") { - propArray = append(propArray, []string{fvdl.EngineData.RuleInfo[i].MetaInfoGroup[j].Name, fvdl.EngineData.RuleInfo[i].MetaInfoGroup[j].Data}) - } else if fvdl.EngineData.RuleInfo[i].MetaInfoGroup[j].Name == "altcategoryCWE" { - //Get all CWE IDs. First, split on ", " - rawCweIds := strings.Split(fvdl.EngineData.RuleInfo[i].MetaInfoGroup[j].Data, ", ") - //If not "None", split each string on " " and add its 2nd index - if rawCweIds[0] != "None" { - for k := 0; k < len(rawCweIds); k++ { - cweId := strings.Split(rawCweIds[k], " ")[2] - //Fill the cweIdsForTaxonomies map if not already in - if _, isIn := cweIdsForTaxonomies[cweId]; !isIn { - cweIdsForTaxonomies[cweId] = cweId - } - cweIds = append(cweIds, cweId) - } - } else { - cweIds = append(cweIds, rawCweIds[0]) - } - } - } - var ruleProp *format.SarifRuleProperties - if len(propArray) != 0 { - ruleProp = new(format.SarifRuleProperties) - for j := 0; j < len(propArray); j++ { - if propArray[j][0] == "Accuracy" { - ruleProp.Accuracy = propArray[j][1] - } else if propArray[j][0] == "Impact" { - ruleProp.Impact = propArray[j][1] - } else if propArray[j][0] == "Probability" { - ruleProp.Probability = propArray[j][1] - } - } - } - sarifRule.Properties = ruleProp - - //relationships: will most likely require some expansion - //One relationship per CWE id - for j := 0; j < len(cweIds); j++ { - rls := *new(format.Relationships) - rls.Target.Id = cweIds[j] - rls.Target.ToolComponent.Name = "CWE" - rls.Target.ToolComponent.Guid = "25F72D7E-8A92-459D-AD67-64853F788765" - rls.Kinds = append(rls.Kinds, "relevant") - sarifRule.Relationships = append(sarifRule.Relationships, rls) - } - - //Finalize: append the rule - tool.Driver.Rules = append(tool.Driver.Rules, sarifRule) } //supportedTaxonomies sTax := *new(format.SupportedTaxonomies) //This object seems fixed, but it will have to be checked sTax.Name = "CWE" - sTax.Index = 0 + sTax.Index = 1 sTax.Guid = "25F72D7E-8A92-459D-AD67-64853F788765" tool.Driver.SupportedTaxonomies = append(tool.Driver.SupportedTaxonomies, sTax) @@ -917,7 +979,12 @@ func Parse(sys System, project *models.Project, projectVersion *models.ProjectVe //handle originalUriBaseIds oubi := new(format.OriginalUriBaseIds) - oubi.SrcRoot.Uri = "file:///" + fvdl.Build.SourceBasePath + "/" + prefix := "file://" + if fvdl.Build.SourceBasePath[0] == '/' { + oubi.SrcRoot.Uri = prefix + fvdl.Build.SourceBasePath + "/" + } else { + oubi.SrcRoot.Uri = prefix + "/" + fvdl.Build.SourceBasePath + "/" + } sarif.Runs[0].OriginalUriBaseIds = oubi //handle artifacts @@ -945,31 +1012,25 @@ func Parse(sys System, project *models.Project, projectVersion *models.ProjectVe //handle threadFlowLocations log.Entry().Debug("[SARIF] Now handling threadFlowLocations.") threadFlowLocationsObject := []format.Locations{} - //prepare a check object + //to ensure an exact replacement in case a threadFlowLocation object refers to another, we prepare a map + threadFlowIndexMap := make(map[int]([]int)) // This will store indexes, we will work with it only to reduce item copies to a minimum for i := 0; i < len(fvdl.UnifiedNodePool.Node); i++ { - unique := true - //Uniqueness Check - for check := 0; check < i; check++ { - if fvdl.UnifiedNodePool.Node[i].SourceLocation.Snippet == fvdl.UnifiedNodePool.Node[check].SourceLocation.Snippet && - fvdl.UnifiedNodePool.Node[i].Action.ActionData == fvdl.UnifiedNodePool.Node[check].Action.ActionData { - unique = false - } - } - if !unique { - continue - } - locations := *new(format.Locations) + threadFlowIndexMap[i+1] = append(threadFlowIndexMap[i+1], i+1) loc := new(format.Location) //get artifact location for j := 0; j < len(fvdl.Build.SourceFiles); j++ { // j iterates on source files if fvdl.Build.SourceFiles[j].Name == fvdl.UnifiedNodePool.Node[i].SourceLocation.Path { - loc.PhysicalLocation.ArtifactLocation.Index = j + loc.PhysicalLocation.ArtifactLocation.Index = j + 1 + loc.PhysicalLocation.ArtifactLocation.URI = fvdl.UnifiedNodePool.Node[i].SourceLocation.Path + loc.PhysicalLocation.ArtifactLocation.URIBaseId = "%SRCROOT%" break } } //get region & context region loc.PhysicalLocation.Region.StartLine = fvdl.UnifiedNodePool.Node[i].SourceLocation.Line - //loc.PhysicalLocation.Region.Snippet.Text = "foobar" //TODO + loc.PhysicalLocation.Region.EndLine = fvdl.UnifiedNodePool.Node[i].SourceLocation.LineEnd + loc.PhysicalLocation.Region.StartColumn = fvdl.UnifiedNodePool.Node[i].SourceLocation.ColStart + loc.PhysicalLocation.Region.EndColumn = fvdl.UnifiedNodePool.Node[i].SourceLocation.ColEnd targetSnippetId := fvdl.UnifiedNodePool.Node[i].SourceLocation.Snippet for j := 0; j < len(fvdl.Snippets); j++ { if fvdl.Snippets[j].SnippetId == targetSnippetId { @@ -983,34 +1044,10 @@ func Parse(sys System, project *models.Project, projectVersion *models.ProjectVe } loc.Message = new(format.Message) loc.Message.Text = fvdl.UnifiedNodePool.Node[i].Action.ActionData + // Handle snippet - snippetTarget := "" - switch fvdl.UnifiedNodePool.Node[i].Action.Type { - case "Assign": - snippetWords := strings.Split(fvdl.UnifiedNodePool.Node[i].Action.ActionData, " ") - if snippetWords[0] == "Assignment" { - snippetTarget = snippetWords[2] - } else { - snippetTarget = fvdl.UnifiedNodePool.Node[i].Action.ActionData - } - case "InCall": - snippetTarget = strings.Split(fvdl.UnifiedNodePool.Node[i].Action.ActionData, "(")[0] - case "OutCall": - snippetTarget = strings.Split(fvdl.UnifiedNodePool.Node[i].Action.ActionData, "(")[0] - case "InOutCall": - snippetTarget = strings.Split(fvdl.UnifiedNodePool.Node[i].Action.ActionData, "(")[0] - case "Return": - snippetTarget = fvdl.UnifiedNodePool.Node[i].Action.ActionData - case "Read": - snippetWords := strings.Split(fvdl.UnifiedNodePool.Node[i].Action.ActionData, " ") - if len(snippetWords) > 1 { - snippetTarget = " " + snippetWords[1] - } else { - snippetTarget = snippetWords[0] - } - default: - snippetTarget = fvdl.UnifiedNodePool.Node[i].Action.ActionData - } + snippetTarget := handleSnippet(fvdl.UnifiedNodePool.Node[i].Action.Type, fvdl.UnifiedNodePool.Node[i].Action.ActionData) + if loc.PhysicalLocation.ContextRegion.Snippet != nil { physLocationSnippetLines := strings.Split(loc.PhysicalLocation.ContextRegion.Snippet.Text, "\n") snippetText := "" @@ -1028,13 +1065,40 @@ func Parse(sys System, project *models.Project, projectVersion *models.ProjectVe } loc.PhysicalLocation.Region.Snippet = snippetSarif } - locations.Location = loc - locations.Kinds = append(locations.Kinds, "unknown") - threadFlowLocationsObject = append(threadFlowLocationsObject, locations) + log.Entry().Debug("Compute eventual sub-nodes") + threadFlowIndexMap[i+1] = computeLocationPath(fvdl, i+1) // Recursively traverse array + locs := format.Locations{Location: loc} + threadFlowLocationsObject = append(threadFlowLocationsObject, locs) } sarif.Runs[0].ThreadFlowLocations = threadFlowLocationsObject + // Now, iterate on threadflows in each result, and replace eventual indexes... + for i := 0; i < len(sarif.Runs[0].Results); i++ { + for cf := 0; cf < len(sarif.Runs[0].Results[i].CodeFlows); cf++ { + for tf := 0; tf < len(sarif.Runs[0].Results[i].CodeFlows[cf].ThreadFlows); tf++ { + log.Entry().Debug("Handling tf: ", tf, "from instance ", sarif.Runs[0].Results[i].PartialFingerprints.FortifyInstanceID) + newLocations := *new([]format.Locations) + for j := 0; j < len(sarif.Runs[0].Results[i].CodeFlows[cf].ThreadFlows[tf].Locations); j++ { + if sarif.Runs[0].Results[i].CodeFlows[cf].ThreadFlows[tf].Locations[j].Index != 0 { + indexes := threadFlowIndexMap[sarif.Runs[0].Results[i].CodeFlows[cf].ThreadFlows[tf].Locations[j].Index] + log.Entry().Debug("Indexes found: ", indexes) + for rep := 0; rep < len(indexes); rep++ { + newLocations = append(newLocations, sarif.Runs[0].ThreadFlowLocations[indexes[rep]-1]) + newLocations[rep].Index = 0 // void index + } + } else { + newLocations = append(newLocations, sarif.Runs[0].Results[i].CodeFlows[cf].ThreadFlows[tf].Locations[j]) + } + } + sarif.Runs[0].Results[i].CodeFlows[cf].ThreadFlows[tf].Locations = newLocations + } + } + } + + // Threadflowlocations is no loger useful: voiding it will make for smaller reports + sarif.Runs[0].ThreadFlowLocations = []format.Locations{} + //handle taxonomies //Only one exists apparently: CWE. It is fixed taxonomy := *new(format.Taxonomies) @@ -1078,8 +1142,7 @@ func integrateAuditData(ruleProp *format.SarifProperties, issueInstanceID string } } if len(data) != 1 { //issueInstanceID is supposedly unique so len(data) = 1 - //log.Entry().Error("not exactly 1 issue found, found " + fmt.Sprint(len(data))) - return errors.New("not exactly 1 issue found for instance ID " + issueInstanceID + ", found " + fmt.Sprint(len(data))) + return errors.New("not exactly 1 issue found, found " + fmt.Sprint(len(data))) } ruleProp.Audited = data[0].Audited ruleProp.ToolSeverity = *data[0].Friority @@ -1119,7 +1182,7 @@ func integrateAuditData(ruleProp *format.SarifProperties, issueInstanceID string if err != nil { return err } - ruleProp.ToolAuditMessage = *commentData[0].Comment + ruleProp.ToolAuditMessage = unescapeXML(*commentData[0].Comment) } if filterSet != nil { for i := 0; i < len(filterSet.Folders); i++ { @@ -1134,3 +1197,66 @@ func integrateAuditData(ruleProp *format.SarifProperties, issueInstanceID string } return nil } + +// Factorizes some code used to obtain the relevant value for a snippet based on the type given by Fortify +func handleSnippet(snippetType string, snippet string) string { + snippetTarget := "" + switch snippetType { + case "Assign": + snippetWords := strings.Split(snippet, " ") + if snippetWords[0] == "Assignment" { + snippetTarget = snippetWords[2] + } else { + snippetTarget = snippet + } + case "InCall": + snippetTarget = strings.Split(snippet, "(")[0] + case "OutCall": + snippetTarget = strings.Split(snippet, "(")[0] + case "InOutCall": + snippetTarget = strings.Split(snippet, "(")[0] + case "Return": + snippetTarget = snippet + case "Read": + snippetWords := strings.Split(snippet, " ") + if len(snippetWords) > 1 { + snippetTarget = " " + snippetWords[1] + } else { + snippetTarget = snippetWords[0] + } + default: + snippetTarget = snippet + } + return snippetTarget +} + +func unescapeXML(input string) string { + raw := input + // Post-treat string to change the XML escaping generated by Unmarshal + raw = strings.ReplaceAll(raw, "<", "<") + raw = strings.ReplaceAll(raw, ">", ">") + raw = strings.ReplaceAll(raw, "&", "&") + raw = strings.ReplaceAll(raw, "'", "'") + raw = strings.ReplaceAll(raw, """, "\"") + return raw +} + +// Used to build a reference array of index for the successors of each node in the UnifiedNodePool +func computeLocationPath(fvdl FVDL, input int) []int { + log.Entry().Debug("Computing for ID ", input) + // Find the successors of input + var subnodes []int + var result []int + for j := 0; j < len(fvdl.UnifiedNodePool.Node[input-1].Reason.Trace.Primary.Entry); j++ { + if fvdl.UnifiedNodePool.Node[input-1].Reason.Trace.Primary.Entry[j].NodeRef.RefId != 0 && fvdl.UnifiedNodePool.Node[input-1].Reason.Trace.Primary.Entry[j].NodeRef.RefId != (input-1) { + subnodes = append(subnodes, fvdl.UnifiedNodePool.Node[input-1].Reason.Trace.Primary.Entry[j].NodeRef.RefId+1) + } + } + result = append(result, input) + log.Entry().Debug("Successors: ", subnodes) + for j := 0; j < len(subnodes); j++ { + result = append(result, computeLocationPath(fvdl, subnodes[j])...) + } + log.Entry().Debug("Finishing computing for ID ", input) + return result +}