package fortify import ( "bytes" "encoding/xml" "errors" "fmt" "os" "path/filepath" "strconv" "strings" "time" "github.com/piper-validation/fortify-client-go/models" "github.com/SAP/jenkins-library/pkg/format" "github.com/SAP/jenkins-library/pkg/log" "github.com/SAP/jenkins-library/pkg/piperutils" ) // FVDL This struct encapsulates everyting in the FVDL document type FVDL struct { XMLName xml.Name `xml:"FVDL"` Xmlns string `xml:"xmlns,attr"` XmlnsXsi string `xml:"xsi,attr"` Version string `xml:"version,attr"` XsiType string `xml:"type,attr"` Created CreatedTS `xml:"CreatedTS"` Uuid UUID `xml:"UUID"` Build Build `xml:"Build"` Vulnerabilities Vulnerabilities `xml:"Vulnerabilities"` ContextPool ContextPool `xml:"ContextPool"` UnifiedNodePool UnifiedNodePool `xml:"UnifiedNodePool"` Description []Description `xml:"Description"` Snippets []Snippet `xml:"Snippets>Snippet"` ProgramData ProgramData `xml:"ProgramData"` EngineData EngineData `xml:"EngineData"` } // CreatedTS type CreatedTS struct { XMLName xml.Name `xml:"CreatedTS"` Date string `xml:"date,attr"` Time string `xml:"time,attr"` } // UUIF type UUID struct { XMLName xml.Name `xml:"UUID"` Uuid string `xml:",innerxml"` } // LOC type LOC struct { XMLName xml.Name `xml:"LOC"` LocType string `xml:"type,attr"` LocValue string `xml:",innerxml"` } // These structures are relevant to the Build object // The Build object transports all build and scan related information type Build struct { XMLName xml.Name `xml:"Build"` Project string `xml:"Project"` Version string `xml:"Version"` Label string `xml:"Label"` BuildID string `xml:"BuildID"` NumberFiles int `xml:"NumberFiles"` Locs []LOC `xml:"LOC"` JavaClassPath string `xml:"JavaClasspath"` SourceBasePath string `xml:"SourceBasePath"` SourceFiles []File `xml:"SourceFiles>File"` Scantime ScanTime `xml:"ScanTime"` } // File type File struct { XMLName xml.Name `xml:"File"` FileSize int `xml:"size,attr"` FileTimestamp string `xml:"timestamp,attr"` FileLoc int `xml:"loc,attr,omitempty"` FileType string `xml:"type,attr"` Encoding string `xml:"encoding,attr"` Name string `xml:"Name"` Locs []LOC `xml:",any,omitempty"` } // ScanTime type ScanTime struct { XMLName xml.Name `xml:"ScanTime"` Value int `xml:"value,attr"` } // Vulnerabilities These structures are relevant to the Vulnerabilities object type Vulnerabilities struct { XMLName xml.Name `xml:"Vulnerabilities"` Vulnerability []Vulnerability `xml:"Vulnerability"` } // Vulnerability type Vulnerability struct { XMLName xml.Name `xml:"Vulnerability"` ClassInfo ClassInfo `xml:"ClassInfo"` InstanceInfo InstanceInfo `xml:"InstanceInfo"` AnalysisInfo AnalysisInfo `xml:"AnalysisInfo>Unified"` } // ClassInfo type ClassInfo struct { XMLName xml.Name `xml:"ClassInfo"` ClassID string `xml:"ClassID"` Kingdom string `xml:"Kingdom,omitempty"` Type string `xml:"Type"` Subtype string `xml:"Subtype,omitempty"` AnalyzerName string `xml:"AnalyzerName"` DefaultSeverity float64 `xml:"DefaultSeverity"` } // InstanceInfo type InstanceInfo struct { XMLName xml.Name `xml:"InstanceInfo"` InstanceID string `xml:"InstanceID"` InstanceSeverity float64 `xml:"InstanceSeverity"` Confidence string `xml:"Confidence"` } // AnalysisInfo type AnalysisInfo struct { //Note that this is directly the "Unified" object Context Context ReplacementDefinitions ReplacementDefinitions `xml:"ReplacementDefinitions"` Trace []Trace `xml:"Trace"` } // Context type Context struct { XMLName xml.Name `xml:"Context"` ContextId string `xml:"id,attr,omitempty"` Function Function `xml:"Function"` FDSL FunctionDeclarationSourceLocation `xml:"FunctionDeclarationSourceLocation"` } // Function type Function struct { XMLName xml.Name `xml:"Function"` FunctionName string `xml:"name,attr"` FunctionNamespace string `xml:"namespace,attr"` FunctionEnclosingClass string `xml:"enclosingClass,attr"` } // FunctionDeclarationSourceLocation type FunctionDeclarationSourceLocation struct { XMLName xml.Name `xml:"FunctionDeclarationSourceLocation"` FDSLPath string `xml:"path,attr"` FDSLLine string `xml:"line,attr"` FDSLLineEnd string `xml:"lineEnd,attr"` FDSLColStart string `xml:"colStart,attr"` FDSLColEnd string `xml:"colEnd,attr"` } // ReplacementDefinitions type ReplacementDefinitions struct { XMLName xml.Name `xml:"ReplacementDefinitions"` Def []Def `xml:"Def"` LocationDef []LocationDef `xml:"LocationDef"` } // Def type Def struct { XMLName xml.Name `xml:"Def"` DefKey string `xml:"key,attr"` DefValue string `xml:"value,attr"` } // LocationDef type LocationDef struct { XMLName xml.Name `xml:"LocationDef"` Path string `xml:"path,attr"` Line int `xml:"line,attr"` LineEnd int `xml:"lineEnd,attr"` ColStart int `xml:"colStart,attr"` ColEnd int `xml:"colEnd,attr"` Key string `xml:"key,attr"` } // Trace type Trace struct { XMLName xml.Name `xml:"Trace"` Primary Primary `xml:"Primary"` } // Primary type Primary struct { XMLName xml.Name `xml:"Primary"` Entry []Entry `xml:"Entry"` } // Entry type Entry struct { XMLName xml.Name `xml:"Entry"` NodeRef NodeRef `xml:"NodeRef,omitempty"` Node Node `xml:"Node,omitempty"` } // NodeRef type NodeRef struct { XMLName xml.Name `xml:"NodeRef"` RefId int `xml:"id,attr"` } // Node 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"` Knowledge Knowledge `xml:"Knowledge,omitempty"` } // SourceLocation type SourceLocation struct { XMLName xml.Name `xml:"SourceLocation"` Path string `xml:"path,attr"` Line int `xml:"line,attr"` LineEnd int `xml:"lineEnd,attr"` ColStart int `xml:"colStart,attr"` ColEnd int `xml:"colEnd,attr"` ContextId string `xml:"contextId,attr"` Snippet string `xml:"snippet,attr"` } // Action type Action struct { XMLName xml.Name `xml:"Action"` Type string `xml:"type,attr"` ActionData string `xml:",innerxml"` } // Reason type Reason struct { XMLName xml.Name `xml:"Reason"` Rule Rule `xml:"Rule,omitempty"` Trace Trace `xml:"Trace,omitempty"` } // Rule type Rule struct { XMLName xml.Name `xml:"Rule"` RuleID string `xml:"ruleID,attr"` } // Group type Group struct { XMLName xml.Name `xml:"Group"` Name string `xml:"name,attr"` Data string `xml:",innerxml"` } // Knowledge type Knowledge struct { XMLName xml.Name `xml:"Knowledge"` Facts []Fact `xml:"Fact"` } // Fact type Fact struct { XMLName xml.Name `xml:"Fact"` Primary string `xml:"primary,attr"` Type string `xml:"type,attr,omitempty"` FactData string `xml:",innerxml"` } // ContextPool These structures are relevant to the ContextPool object type ContextPool struct { XMLName xml.Name `xml:"ContextPool"` Context []Context `xml:"Context"` } // UnifiedNodePool These structures are relevant to the UnifiedNodePool object type UnifiedNodePool struct { XMLName xml.Name `xml:"UnifiedNodePool"` Node []Node `xml:"Node"` } // Description These structures are relevant to the Description object type Description struct { XMLName xml.Name `xml:"Description"` ContentType string `xml:"contentType,attr"` ClassID string `xml:"classID,attr"` Abstract Abstract `xml:"Abstract"` Explanation Explanation `xml:"Explanation"` Recommendations Recommendations `xml:"Recommendations"` Tips []Tip `xml:"Tips>Tip,omitempty"` References []Reference `xml:"References>Reference"` CustomDescription CustomDescription `xml:"CustomDescription,omitempty"` } // Abstract type Abstract struct { XMLName xml.Name `xml:"Abstract"` Text string `xml:",innerxml"` } // Explanation type Explanation struct { XMLName xml.Name `xml:"Explanation"` Text string `xml:",innerxml"` } // Recommendations type Recommendations struct { XMLName xml.Name `xml:"Recommendations"` Text string `xml:",innerxml"` } // Reference type Reference struct { XMLName xml.Name `xml:"Reference"` Title string `xml:"Title"` Author string `xml:"Author"` } // Tip type Tip struct { XMLName xml.Name `xml:"Tip"` Tip string `xml:",innerxml"` } // CustomDescription type CustomDescription struct { XMLName xml.Name `xml:"CustomDescription"` ContentType string `xml:"contentType,attr"` RuleID string `xml:"ruleID,attr"` Explanation Explanation `xml:"Explanation"` Recommendations Recommendations `xml:"Recommendations"` References []Reference `xml:"References>Reference"` } // Snippet These structures are relevant to the Snippets object type Snippet struct { XMLName xml.Name `xml:"Snippet"` SnippetId string `xml:"id,attr"` File string `xml:"File"` StartLine int `xml:"StartLine"` EndLine int `xml:"EndLine"` Text string `xml:"Text"` } // ProgramData These structures are relevant to the ProgramData object type ProgramData struct { XMLName xml.Name `xml:"ProgramData"` Sources []SourceInstance `xml:"Sources>SourceInstance"` Sinks []SinkInstance `xml:"Sinks>SinkInstance"` CalledWithNoDef []Function `xml:"CalledWithNoDef>Function"` } // SourceInstance type SourceInstance struct { XMLName xml.Name `xml:"SourceInstance"` RuleID string `xml:"ruleID,attr"` FunctionCall FunctionCall `xml:"FunctionCall,omitempty"` FunctionEntry FunctionEntry `xml:"FunctionEntry,omitempty"` SourceLocation SourceLocation `xml:"SourceLocation,omitempty"` TaintFlags TaintFlags `xml:"TaintFlags"` } // FunctionCall type FunctionCall struct { XMLName xml.Name `xml:"FunctionCall"` SourceLocation SourceLocation `xml:"SourceLocation"` Function Function `xml:"Function"` } // FunctionEntry type FunctionEntry struct { XMLName xml.Name `xml:"FunctionEntry"` SourceLocation SourceLocation `xml:"SourceLocation"` Function Function `xml:"Function"` } // TaintFlags type TaintFlags struct { XMLName xml.Name `xml:"TaintFlags"` TaintFlag []TaintFlag `xml:"TaintFlag"` } // TaintFlag type TaintFlag struct { XMLName xml.Name `xml:"TaintFlag"` TaintFlagName string `xml:"name,attr"` } // SinkInstance type SinkInstance struct { XMLName xml.Name `xml:"SinkInstance"` RuleID string `xml:"ruleID,attr"` FunctionCall FunctionCall `xml:"FunctionCall,omitempty"` SourceLocation SourceLocation `xml:"SourceLocation,omitempty"` } // EngineData These structures are relevant to the EngineData object type EngineData struct { XMLName xml.Name `xml:"EngineData"` EngineVersion string `xml:"EngineVersion"` RulePacks []RulePack `xml:"RulePacks>RulePack"` Properties []Properties `xml:"Properties"` CLArguments []string `xml:"CommandLine>Argument"` Errors []Error `xml:"Errors>Error"` MachineInfo MachineInfo `xml:"MachineInfo"` FilterResult FilterResult `xml:"FilterResult"` RuleInfo []RuleInfo `xml:"RuleInfo>Rule"` LicenseInfo LicenseInfo `xml:"LicenseInfo"` } // RulePack type RulePack struct { XMLName xml.Name `xml:"RulePack"` RulePackID string `xml:"RulePackID"` SKU string `xml:"SKU"` Name string `xml:"Name"` Version string `xml:"Version"` MAC string `xml:"MAC"` } // Properties type Properties struct { XMLName xml.Name `xml:"Properties"` PropertiesType string `xml:"type,attr"` Property []Property `xml:"Property"` } // Property type Property struct { XMLName xml.Name `xml:"Property"` Name string `xml:"name"` Value string `xml:"value"` } // Error type Error struct { XMLName xml.Name `xml:"Error"` ErrorCode string `xml:"code,attr"` ErrorMessage string `xml:",innerxml"` } // MachineInfo type MachineInfo struct { XMLName xml.Name `xml:"MachineInfo"` Hostname string `xml:"Hostname"` Username string `xml:"Username"` Platform string `xml:"Platform"` } // FilterResult type FilterResult struct { XMLName xml.Name `xml:"FilterResult"` //Todo? No data in sample audit file } // RuleInfo type RuleInfo struct { XMLName xml.Name `xml:"Rule"` RuleID string `xml:"id,attr"` MetaInfoGroup []Group `xml:"MetaInfo>Group,omitempty"` } // LicenseInfo type LicenseInfo struct { XMLName xml.Name `xml:"LicenseInfo"` Metadata []Metadata `xml:"Metadata"` Capability []Capability `xml:"Capability"` } // Metadata type Metadata struct { XMLName xml.Name `xml:"Metadata"` Name string `xml:"name"` Value string `xml:"value"` } // Capability type Capability struct { XMLName xml.Name `xml:"Capability"` Name string `xml:"Name"` Expiration string `xml:"Expiration"` Attribute Attribute `xml:"Attribute"` } // Attribute type Attribute struct { XMLName xml.Name `xml:"Attribute"` Name string `xml:"name"` Value string `xml:"value"` } // ConvertFprToSarif converts the FPR file contents into SARIF format func ConvertFprToSarif(sys System, projectVersion *models.ProjectVersion, resultFilePath string, filterSet *models.FilterSet) (format.SARIF, format.SARIF, error) { log.Entry().Debug("Extracting FPR.") var sarif format.SARIF var sarifSimplified format.SARIF tmpFolder, err := os.MkdirTemp(".", "temp-") defer os.RemoveAll(tmpFolder) if err != nil { log.Entry().WithError(err).WithField("path", tmpFolder).Debug("Creating temp directory failed") return sarif, sarifSimplified, err } _, err = piperutils.Unzip(resultFilePath, tmpFolder) if err != nil { return sarif, sarifSimplified, err } log.Entry().Debug("Reading audit file.") data, err := os.ReadFile(filepath.Join(tmpFolder, "audit.fvdl")) if err != nil { return sarif, sarifSimplified, err } if len(data) == 0 { log.Entry().Error("Error reading audit file at " + filepath.Join(tmpFolder, "audit.fvdl") + ". This might be that the file is missing, corrupted, or too large. Aborting procedure.") err := errors.New("cannot read audit file") return sarif, sarifSimplified, err } log.Entry().Debug("Calling Parse.") return Parse(sys, projectVersion, data, filterSet) } // Parse parses the FPR file func Parse(sys System, projectVersion *models.ProjectVersion, data []byte, filterSet *models.FilterSet) (format.SARIF, format.SARIF, error) { //To read XML data, Unmarshal or Decode can be used, here we use Decode to work on the stream reader := bytes.NewReader(data) decoder := xml.NewDecoder(reader) start := time.Now() // For the conversion start time var fvdl FVDL err := decoder.Decode(&fvdl) if err != nil { return format.SARIF{}, format.SARIF{}, err } //Create an object containing all audit data log.Entry().Debug("Querying Fortify SSC for batch audit data") oneRequestPerIssueMode := false var auditData []*models.ProjectVersionIssue maxretries := 5 // Maximum number of requests allowed to fail before stopping them if sys != nil && projectVersion != nil { auditData, err = sys.GetAllIssueDetails(projectVersion.ID) if err != nil || len(auditData) == 0 { // It's reasonable to admit that with a length of 0, something went wrong log.Entry().WithError(err).Error("failed to get all audit data, defaulting to one-request-per-issue basis") oneRequestPerIssueMode = true // We do not lower maxretries here in case a "real" bug happened } else { log.Entry().Debug("Request successful, data frame size: ", len(auditData), " audits") } } else { log.Entry().Error("no system instance or project version found, lookup impossible") oneRequestPerIssueMode = true maxretries = 1 // Set to 1 if the sys instance isn't defined: chances are it couldn't be created, we'll live a chance if there was an unknown bug log.Entry().Debug("request failed: remaining retries ", maxretries) } //Now, we handle the sarif var sarif format.SARIF sarif.Schema = "https://docs.oasis-open.org/sarif/sarif/v2.1.0/cos02/schemas/sarif-schema-2.1.0.json" sarif.Version = "2.1.0" var fortifyRun format.Runs fortifyRun.ColumnKind = "utf16CodeUnits" cweIdsForTaxonomies := make(map[string]string) //Defining this here and filling it in the course of the program helps filling the Taxonomies object easily. Map because easy to check for keys sarif.Runs = append(sarif.Runs, fortifyRun) // Initialize the simplified version var sarifSimplified format.SARIF sarifSimplified.Schema = sarif.Schema sarifSimplified.Version = sarif.Version sarifSimplified.Runs = append(sarifSimplified.Runs, fortifyRun) // Handle results/vulnerabilities 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 // 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, "/") result.Kind = "fail" // Default value, Level must not be set if kind is not fail // This is an "easy" treatment of result.Level. It does not follow the spec exactly, but the idea is there // An exact processing algorithm can be found here https://docs.oasis-open.org/sarif/sarif/v2.1.0/os/sarif-v2.1.0-os.html#_Toc34317648 if fvdl.Vulnerabilities.Vulnerability[i].InstanceInfo.InstanceSeverity >= 3.0 { result.Level = "error" } else if fvdl.Vulnerabilities.Vulnerability[i].InstanceInfo.InstanceSeverity >= 1.5 { result.Level = "warning" } else if fvdl.Vulnerabilities.Vulnerability[i].InstanceInfo.InstanceSeverity < 1.5 { result.Level = "note" } else { result.Level = "none" } //get message for j := 0; j < len(fvdl.Description); j++ { 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, "", fvdl.Vulnerabilities.Vulnerability[i].AnalysisInfo.ReplacementDefinitions.Def[l].DefValue) } msg := new(format.Message) msg.Text = rawMessage result.Message = msg break } } // Handle all locations items location := *new(format.Location) //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 //Each trace represents a codeflow, each entry represents a location in threadflow codeFlow := *new(format.CodeFlow) 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 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]) // 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 { 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 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 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 { tfloc.PhysicalLocation.ContextRegion = new(format.ContextRegion) tfloc.PhysicalLocation.ContextRegion.StartLine = fvdl.Snippets[j].StartLine tfloc.PhysicalLocation.ContextRegion.EndLine = fvdl.Snippets[j].EndLine break } } // 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 } } 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 } } // 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 = new(format.ContextRegion) nintfloc.PhysicalLocation.ContextRegion.StartLine = fvdl.Snippets[j].StartLine nintfloc.PhysicalLocation.ContextRegion.EndLine = fvdl.Snippets[j].EndLine break } } // 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) } // END edge case } else { //is not a main threadflow: just register NodeRef index in threadFlowLocation // 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) } threadFlow.Locations = append(threadFlow.Locations, tfla...) } codeFlow.ThreadFlows = append(codeFlow.ThreadFlows, threadFlow) result.CodeFlows = append(result.CodeFlows, codeFlow) } //For some reason, the principal object only has 1 location: here we keep the last one //Void message location.Message = nil result.Locations = append(result.Locations, location) //handle relatedLocation relatedLocation := *new(format.RelatedLocation) relatedLocation.ID = 1 relatedLocation.PhysicalLocation = *new(format.RelatedPhysicalLocation) relatedLocation.PhysicalLocation.ArtifactLocation = location.PhysicalLocation.ArtifactLocation relatedLocation.PhysicalLocation.Region = *new(format.RelatedRegion) relatedLocation.PhysicalLocation.Region.StartLine = location.PhysicalLocation.Region.StartLine 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 = strconv.FormatFloat(fvdl.Vulnerabilities.Vulnerability[i].InstanceInfo.InstanceSeverity, 'f', 1, 64) prop.Confidence = fvdl.Vulnerabilities.Vulnerability[i].InstanceInfo.Confidence prop.InstanceID = fvdl.Vulnerabilities.Vulnerability[i].InstanceInfo.InstanceID prop.RuleGUID = fvdl.Vulnerabilities.Vulnerability[i].ClassInfo.ClassID //Get the audit data if err := integrateAuditData(prop, fvdl.Vulnerabilities.Vulnerability[i].InstanceInfo.InstanceID, sys, projectVersion, auditData, filterSet, oneRequestPerIssueMode, maxretries); err != nil { log.Entry().Debug(err) maxretries = maxretries - 1 if maxretries >= 0 { log.Entry().Debug("request failed: remaining retries ", maxretries) } } result.Properties = prop sarif.Runs[0].Results = append(sarif.Runs[0].Results, result) // Handle simplified version of a result resultSimplified := *new(format.Results) resultSimplified.RuleID = result.RuleID resultSimplified.Kind = result.Kind resultSimplified.Level = result.Level resultSimplified.Message = result.Message resultSimplified.Properties = result.Properties sarifSimplified.Runs[0].Results = append(sarifSimplified.Runs[0].Results, resultSimplified) } //handle the tool object log.Entry().Debug("[SARIF] Now handling driver object.") tool := *new(format.Tool) tool.Driver = *new(format.Driver) tool.Driver.Name = "MicroFocus Fortify SCA" tool.Driver.Version = fvdl.EngineData.EngineVersion tool.Driver.InformationUri = "https://www.microfocus.com/documentation/fortify-static-code-analyzer-and-tools/2020/SCA_Guide_20.2.0.pdf" //handle the simplified tool object toolSimplified := *new(format.Tool) toolSimplified.Driver = tool.Driver //handles rules for i := 0; i < len(fvdl.EngineData.RuleInfo); i++ { //i iterates on rules sarifRule := *new(format.SarifRule) sarifRule.ID = fvdl.EngineData.RuleInfo[i].RuleID sarifRule.GUID = fvdl.EngineData.RuleInfo[i].RuleID 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 != "" { //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] = piperutils.Title(strings.ToLower(element)) } nameArray = append(nameArray, words...) } if 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] = piperutils.Title(strings.ToLower(element)) } nameArray = append(nameArray, words...) } if 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] = piperutils.Title(strings.ToLower(element)) } nameArray = append(nameArray, words...) } sarifRule.ID = "fortify-" + strings.Join(idArray, "/") sarifRule.Name = strings.Join(nameArray, "") defaultConfig := new(format.DefaultConfiguration) defaultConfig.Level = "warning" // Default value defaultConfig.Enabled = true // Default value defaultConfig.Rank = -1.0 // Default value defaultConfig.Properties.DefaultSeverity = strconv.FormatFloat(fvdl.Vulnerabilities.Vulnerability[j].ClassInfo.DefaultSeverity, 'f', 1, 64) sarifRule.DefaultConfiguration = defaultConfig //Descriptions for j := 0; j < len(fvdl.Description); j++ { if fvdl.Description[j].ClassID == sarifRule.GUID { //rawAbstract := strings.Join(idArray, "/") 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; " + unescapeXML(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 } } 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 ruleProp = new(format.SarifRuleProperties) if len(propArray) != 0 { 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] } } } // Add each part of the "name" in the tags if fvdl.Vulnerabilities.Vulnerability[j].ClassInfo.Kingdom != "" { ruleProp.Tags = append(ruleProp.Tags, fvdl.Vulnerabilities.Vulnerability[j].ClassInfo.Kingdom) } if fvdl.Vulnerabilities.Vulnerability[j].ClassInfo.Type != "" { ruleProp.Tags = append(ruleProp.Tags, fvdl.Vulnerabilities.Vulnerability[j].ClassInfo.Type) } if fvdl.Vulnerabilities.Vulnerability[j].ClassInfo.Subtype != "" { ruleProp.Tags = append(ruleProp.Tags, fvdl.Vulnerabilities.Vulnerability[j].ClassInfo.Subtype) } //Add the SecuritySeverity parameter for GHAS tagging ruleProp.SecuritySeverity = strconv.FormatFloat(2*fvdl.Vulnerabilities.Vulnerability[j].InstanceInfo.InstanceSeverity, 'f', 1, 64) sarifRule.Properties = ruleProp //relationships: will most likely require some expansion //One relationship per CWE id for j := 0; j < len(cweIds); j++ { if cweIds[j] == "None" { continue } 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" //This might not be exact, it is taken from the Microsoft tool converter 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) // Handle simplified version of tool sarifRuleSimplified := *new(format.SarifRule) sarifRuleSimplified.ID = sarifRule.ID sarifRuleSimplified.GUID = sarifRule.GUID sarifRuleSimplified.Name = sarifRule.Name sarifRuleSimplified.DefaultConfiguration = sarifRule.DefaultConfiguration sarifRuleSimplified.Properties = sarifRule.Properties toolSimplified.Driver.Rules = append(toolSimplified.Driver.Rules, sarifRuleSimplified) // A rule vuln has been found for this rule, no need to keep iterating break } } } //supportedTaxonomies sTax := *new(format.SupportedTaxonomies) //This object seems fixed, but it will have to be checked sTax.Name = "CWE" sTax.Index = 1 sTax.Guid = "25F72D7E-8A92-459D-AD67-64853F788765" tool.Driver.SupportedTaxonomies = append(tool.Driver.SupportedTaxonomies, sTax) //Add additional rulepacks for pack := 0; pack < len(fvdl.EngineData.RulePacks); pack++ { extension := *new(format.Driver) extension.Name = fvdl.EngineData.RulePacks[pack].Name extension.Version = fvdl.EngineData.RulePacks[pack].Version extension.GUID = fvdl.EngineData.RulePacks[pack].RulePackID tool.Extensions = append(tool.Extensions, extension) } //Finalize: tool sarif.Runs[0].Tool = tool sarifSimplified.Runs[0].Tool = toolSimplified //handle invocations object log.Entry().Debug("[SARIF] Now handling invocation.") invocation := *new(format.Invocation) for i := 0; i < len(fvdl.EngineData.Properties); i++ { //i selects the properties type if fvdl.EngineData.Properties[i].PropertiesType == "Fortify" { // This is the correct type, now iterate on props for j := 0; j < len(fvdl.EngineData.Properties[i].Property); j++ { if fvdl.EngineData.Properties[i].Property[j].Name == "com.fortify.SCAExecutablePath" { splitPath := strings.Split(fvdl.EngineData.Properties[i].Property[j].Value, "/") invocation.CommandLine = splitPath[len(splitPath)-1] break } } break } } invocation.CommandLine = strings.Join(append([]string{invocation.CommandLine}, fvdl.EngineData.CLArguments...), " ") invocation.StartTimeUtc = strings.Join([]string{fvdl.Created.Date, fvdl.Created.Time}, "T") + ".000Z" for i := 0; i < len(fvdl.EngineData.Errors); i++ { ten := *new(format.ToolExecutionNotifications) ten.Message.Text = fvdl.EngineData.Errors[i].ErrorMessage ten.Descriptor.Id = fvdl.EngineData.Errors[i].ErrorCode invocation.ToolExecutionNotifications = append(invocation.ToolExecutionNotifications, ten) } invocation.ExecutionSuccessful = true //fvdl doesn't seem to plan for this setting invocation.Machine = fvdl.EngineData.MachineInfo.Hostname invocation.Account = fvdl.EngineData.MachineInfo.Username invocProp := new(format.InvocationProperties) invocProp.Platform = fvdl.EngineData.MachineInfo.Platform invocation.Properties = invocProp sarif.Runs[0].Invocations = append(sarif.Runs[0].Invocations, invocation) //handle originalUriBaseIds if fvdl.Build.SourceBasePath != "" { oubi := new(format.OriginalUriBaseIds) 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 } else { log.Entry().Warn("SourceBaesPath is empty") } //handle artifacts log.Entry().Debug("[SARIF] Now handling artifacts.") for i := 0; i < len(fvdl.Build.SourceFiles); i++ { //i iterates on source files artifact := *new(format.Artifact) artifact.Location.Uri = fvdl.Build.SourceFiles[i].Name artifact.Location.UriBaseId = "%SRCROOT%" artifact.Length = fvdl.Build.SourceFiles[i].FileSize switch fvdl.Build.SourceFiles[i].FileType { case "java": artifact.MimeType = "text/x-java-source" case "xml": artifact.MimeType = "text/xml" default: artifact.MimeType = "text" } artifact.Encoding = fvdl.Build.SourceFiles[i].Encoding sarif.Runs[0].Artifacts = append(sarif.Runs[0].Artifacts, artifact) } //handle automationDetails sarif.Runs[0].AutomationDetails = &format.AutomationDetails{Id: fvdl.Build.BuildID} //handle threadFlowLocations log.Entry().Debug("[SARIF] Now handling threadFlowLocations.") threadFlowLocationsObject := []format.Locations{} //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++ { 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 + 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.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 { loc.PhysicalLocation.ContextRegion = new(format.ContextRegion) loc.PhysicalLocation.ContextRegion.StartLine = fvdl.Snippets[j].StartLine loc.PhysicalLocation.ContextRegion.EndLine = fvdl.Snippets[j].EndLine break } } loc.Message = new(format.Message) loc.Message.Text = fvdl.UnifiedNodePool.Node[i].Action.ActionData 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{} // Add a conversion object to highlight this isn't native SARIF conversion := new(format.Conversion) conversion.Tool.Driver.Name = "Piper FPR to SARIF converter" conversion.Tool.Driver.InformationUri = "https://github.com/SAP/jenkins-library" conversion.Invocation.ExecutionSuccessful = true conversion.Invocation.StartTimeUtc = fmt.Sprintf("%s", start.Format("2006-01-02T15:04:05.000Z")) // "YYYY-MM-DDThh:mm:ss.sZ" on 2006-01-02 15:04:05 conversion.Invocation.Machine = fvdl.EngineData.MachineInfo.Hostname conversion.Invocation.Account = fvdl.EngineData.MachineInfo.Username convInvocProp := new(format.InvocationProperties) convInvocProp.Platform = fvdl.EngineData.MachineInfo.Platform conversion.Invocation.Properties = convInvocProp sarif.Runs[0].Conversion = conversion //handle taxonomies //Only one exists apparently: CWE. It is fixed taxonomy := *new(format.Taxonomies) taxonomy.GUID = "25F72D7E-8A92-459D-AD67-64853F788765" taxonomy.Name = "CWE" taxonomy.Organization = "MITRE" taxonomy.ShortDescription.Text = "The MITRE Common Weakness Enumeration" for key := range cweIdsForTaxonomies { taxa := *new(format.Taxa) taxa.Id = key taxonomy.Taxa = append(taxonomy.Taxa, taxa) } sarif.Runs[0].Taxonomies = append(sarif.Runs[0].Taxonomies, taxonomy) return sarif, sarifSimplified, nil } func integrateAuditData(ruleProp *format.SarifProperties, issueInstanceID string, sys System, projectVersion *models.ProjectVersion, auditData []*models.ProjectVersionIssue, filterSet *models.FilterSet, oneRequestPerIssue bool, maxretries int) error { // Set default values ruleProp.Audited = false ruleProp.FortifyCategory = "Unknown" ruleProp.ToolSeverity = "Unknown" ruleProp.ToolState = "Unknown" ruleProp.ToolAuditMessage = "Error fetching audit state" // We set this as default for the error phase, then reset it to nothing ruleProp.ToolSeverityIndex = 0 ruleProp.ToolStateIndex = 0 ruleProp.AuditRequirementIndex = 0 ruleProp.AuditRequirement = "Unknown" // These default values allow for the property bag to be filled even if an error happens later. They all should be overwritten by a normal course of the progrma. if maxretries == 0 { // Max retries reached, we stop there to avoid a longer execution time err := errors.New("request failed: maximum number of retries reached, placeholder values will be set from now on for audit data") return err } else if maxretries < 0 { return nil // Avoid spamming logfile } if sys == nil { ruleProp.ToolAuditMessage = "Cannot fetch audit state: no sys instance" err := errors.New("no system instance, lookup impossible for " + issueInstanceID) return err } if projectVersion == nil { err := errors.New("project or projectVersion is undefined: lookup aborted for " + issueInstanceID) return err } // Reset the audit message ruleProp.ToolAuditMessage = "" var data []*models.ProjectVersionIssue var err error if oneRequestPerIssue { log.Entry().Debug("operating in one-request-per-issue mode: looking up audit state of " + issueInstanceID) data, err = sys.GetIssueDetails(projectVersion.ID, issueInstanceID) if err != nil { return err } } else { for i := 0; i < len(auditData); i++ { if issueInstanceID == *auditData[i].IssueInstanceID { data = append(data, auditData[i]) break } } } if len(data) != 1 { //issueInstanceID is supposedly unique so len(data) = 1 return errors.New("not exactly 1 issue found, found " + fmt.Sprint(len(data))) } if filterSet != nil { for i := 0; i < len(filterSet.Folders); i++ { if filterSet.Folders[i].GUID == *data[0].FolderGUID { ruleProp.FortifyCategory = filterSet.Folders[i].Name // classify into audit groups switch ruleProp.FortifyCategory { case "Corporate Security Requirements", "Audit All": ruleProp.AuditRequirementIndex = format.AUDIT_REQUIREMENT_GROUP_1_INDEX ruleProp.AuditRequirement = format.AUDIT_REQUIREMENT_GROUP_1_DESC case "Spot Checks of Each Category": ruleProp.AuditRequirementIndex = format.AUDIT_REQUIREMENT_GROUP_2_INDEX ruleProp.AuditRequirement = format.AUDIT_REQUIREMENT_GROUP_2_DESC case "Optional": ruleProp.AuditRequirementIndex = format.AUDIT_REQUIREMENT_GROUP_3_INDEX ruleProp.AuditRequirement = format.AUDIT_REQUIREMENT_GROUP_3_DESC } break } } } else { err := errors.New("no filter set defined, category will be missing from " + issueInstanceID) return err } ruleProp.Audited = data[0].Audited ruleProp.ToolSeverity = *data[0].Friority switch ruleProp.ToolSeverity { case "Critical": ruleProp.ToolSeverityIndex = 5 case "Urgent": ruleProp.ToolSeverityIndex = 4 case "High": ruleProp.ToolSeverityIndex = 3 case "Medium": ruleProp.ToolSeverityIndex = 2 case "Low": ruleProp.ToolSeverityIndex = 1 } if ruleProp.Audited { ruleProp.ToolState = *data[0].PrimaryTag switch ruleProp.ToolState { //This is as easy as it can get, seeing that the index is not in the response. case "Exploitable": ruleProp.ToolStateIndex = 5 case "Suspicious": ruleProp.ToolStateIndex = 4 case "Bad Practice": ruleProp.ToolStateIndex = 3 case "Reliability Issue": ruleProp.ToolStateIndex = 2 case "Not an Issue": ruleProp.ToolStateIndex = 1 } } else { ruleProp.ToolState = "Unreviewed" } if *data[0].HasComments { //fetch latest message if comments exist //Fetch the ID parentID := data[0].ID commentData, err := sys.GetIssueComments(parentID) if err != nil { return err } ruleProp.ToolAuditMessage = unescapeXML(*commentData[0].Comment) } return nil } // Factorizes some code used to obtain the relevant value for a snippet based on the type given by Fortify // Note: snippet text is no longer part of .sarif due to size issue. // This function however is helpful to explain how to get snippet out of FPR 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 }