package checkmarx import ( "bytes" "encoding/xml" "fmt" "io/ioutil" "strconv" "strings" "time" "github.com/SAP/jenkins-library/pkg/format" "github.com/SAP/jenkins-library/pkg/log" "github.com/SAP/jenkins-library/pkg/piperutils" "github.com/pkg/errors" ) // CxXMLResults : This struct encapsulates everyting in the Cx XML document type CxXMLResults struct { XMLName xml.Name `xml:"CxXMLResults"` InitiatorName string `xml:"InitiatorName,attr"` Owner string `xml:"Owner,attr"` ScanID string `xml:"ScanId,attr"` ProjectID string `xml:"ProjectId,attr"` ProjectName string `xml:"ProjectName,attr"` TeamFullPathOnReportDate string `xml:"TeamFullPathOnReportDate,attr"` DeepLink string `xml:"DeepLink,attr"` ScanStart string `xml:"ScanStart,attr"` Preset string `xml:"Preset,attr"` ScanTime string `xml:"ScanTime,attr"` LinesOfCodeScanned string `xml:"LinesOfCodeScanned,attr"` FilesScanned string `xml:"FilesScanned,attr"` ReportCreationTime string `xml:"ReportCreationTime,attr"` Team string `xml:"Team,attr"` CheckmarxVersion string `xml:"CheckmarxVersion,attr"` ScanComments string `xml:"ScanComments,attr"` ScanType string `xml:"ScanType,attr"` SourceOrigin string `xml:"SourceOrigin,attr"` Visibility string `xml:"Visibility,attr"` Query []CxxmlQuery `xml:"Query"` } // CxxmlQuery CxxmlQuery type CxxmlQuery struct { XMLName xml.Name `xml:"Query"` ID string `xml:"id,attr"` Categories string `xml:"categories,attr"` CweID string `xml:"cweId,attr"` Name string `xml:"name,attr"` Group string `xml:"group,attr"` Severity string `xml:"Severity,attr"` Language string `xml:"Language,attr"` LanguageHash string `xml:"LanguageHash,attr"` LanguageChangeDate string `xml:"LanguageChangeDate,attr"` SeverityIndex int `xml:"SeverityIndex,attr"` QueryPath string `xml:"QueryPath,attr"` QueryVersionCode string `xml:"QueryVersionCode,attr"` Result []CxxmlResult `xml:"Result"` } // CxxmlResult CxxmlResult type CxxmlResult struct { XMLName xml.Name `xml:"Result"` NodeID string `xml:"NodeId,attr"` FileName string `xml:"FileName,attr"` Status string `xml:"Status,attr"` Line int `xml:"Line,attr"` Column int `xml:"Column,attr"` FalsePositive bool `xml:"FalsePositive,attr"` Severity string `xml:"Severity,attr"` AssignToUser string `xml:"AssignToUser,attr"` State int `xml:"state,attr"` Remark string `xml:"Remark,attr"` DeepLink string `xml:"DeepLink,attr"` SeverityIndex int `xml:"SeverityIndex,attr"` StatusIndex int `xml:"StatusIndex,attr"` DetectionDate string `xml:"DetectionDate,attr"` Path Path `xml:"Path"` } // Path Path type Path struct { XMLName xml.Name `xml:"Path"` ResultID string `xml:"ResultId,attr"` PathID int `xml:"PathId,attr"` SimilarityID string `xml:"SimilarityId,attr"` SourceMethod string `xml:"SourceMethod,attr"` DestinationMethod string `xml:"DestinationMethod,attr"` PathNode []PathNode `xml:"PathNode"` } // PathNode PathNode type PathNode struct { XMLName xml.Name `xml:"PathNode"` FileName string `xml:"FileName"` Line int `xml:"Line"` Column int `xml:"Column"` NodeID int `xml:"NodeId"` Name string `xml:"Name"` Type string `xml:"Type"` Length int `xml:"Length"` Snippet Snippet `xml:"Snippet"` } // Snippet Snippet type Snippet struct { XMLName xml.Name `xml:"Snippet"` Line Line `xml:"Line"` } // Line Line type Line struct { XMLName xml.Name `xml:"Line"` Number int `xml:"Number"` Code string `xml:"Code"` } // ConvertCxxmlToSarif is the entrypoint for the Parse function func ConvertCxxmlToSarif(sys System, xmlReportName string, scanID int) (format.SARIF, error) { var sarif format.SARIF log.Entry().Debug("Reading audit file.") data, err := ioutil.ReadFile(xmlReportName) if err != nil { return sarif, err } if len(data) == 0 { log.Entry().Error("Error reading audit file at " + xmlReportName + ". This might be that the file is missing, corrupted, or too large. Aborting procedure.") err := errors.New("cannot read audit file") return sarif, err } log.Entry().Debug("Calling Parse.") return Parse(sys, data, scanID) } // Parse function func Parse(sys System, data []byte, scanID int) (format.SARIF, error) { reader := bytes.NewReader(data) decoder := xml.NewDecoder(reader) start := time.Now() // For the conversion start time var cxxml CxXMLResults err := decoder.Decode(&cxxml) if err != nil { return format.SARIF{}, err } // Process 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 checkmarxRun format.Runs checkmarxRun.ColumnKind = "utf16CodeUnits" sarif.Runs = append(sarif.Runs, checkmarxRun) rulesArray := []format.SarifRule{} baseURL := "https://" + strings.Split(cxxml.DeepLink, "/")[2] + "/CxWebClient/ScanQueryDescription.aspx?" cweIdsForTaxonomies := make(map[string]int) //use a map to avoid duplicates cweCounter := 0 //maxretries := 5 //CxXML files contain a CxXMLResults > Query object, which represents a broken rule or type of vuln //This Query object contains a list of Result objects, each representing an occurence //Each Result object contains a ResultPath, which represents the exact location of the occurence (the "Snippet") log.Entry().Debug("[SARIF] Now handling results.") for i := 0; i < len(cxxml.Query); i++ { //add cweid to array cweIdsForTaxonomies[cxxml.Query[i].CweID] = cweCounter cweCounter = cweCounter + 1 for j := 0; j < len(cxxml.Query[i].Result); j++ { var apiDescription string result := *new(format.Results) // COMMENTED UNTIL CHECKMARX API WORK AS INTENDED // For rules later, fetch description /*if maxretries == 0 { // Don't spam logfile: only enter the loop if maxretries is positive, and only display the error if it hits 0 log.Entry().Error("request failed: maximum number of retries reached, descriptions will no longer be fetched") maxretries = maxretries - 1 } else if maxretries > 0 { if sys != nil { apiShortDescription, err := sys.GetShortDescription(scanID, cxxml.Query[i].Result[j].Path.PathID) if err != nil { maxretries = maxretries - 1 log.Entry().Debug("request failed: remaining retries ", maxretries) log.Entry().Error(err) } else { apiDescription = apiShortDescription.Text } } else { maxretries = maxretries - 1 log.Entry().Debug("request failed: no system instance, remaining retries ", maxretries) } }*/ //General result.RuleID = "checkmarx-" + cxxml.Query[i].Language + "/" + cxxml.Query[i].ID result.RuleIndex = cweIdsForTaxonomies[cxxml.Query[i].CweID] result.Level = "none" msg := new(format.Message) //msg.Text = cxxml.Query[i].Name + ": " + cxxml.Query[i].Categories if apiDescription != "" { msg.Text = apiDescription } else { msg.Text = cxxml.Query[i].Name } result.Message = msg //Locations codeflow := *new(format.CodeFlow) threadflow := *new(format.ThreadFlow) locationSaved := false for k := 0; k < len(cxxml.Query[i].Result[j].Path.PathNode); k++ { loc := *new(format.Location) loc.PhysicalLocation.ArtifactLocation.URI = cxxml.Query[i].Result[j].FileName loc.PhysicalLocation.Region.StartLine = cxxml.Query[i].Result[j].Path.PathNode[k].Line loc.PhysicalLocation.Region.EndLine = cxxml.Query[i].Result[j].Path.PathNode[k].Line loc.PhysicalLocation.Region.StartColumn = cxxml.Query[i].Result[j].Path.PathNode[k].Column snip := new(format.SnippetSarif) snip.Text = cxxml.Query[i].Result[j].Path.PathNode[k].Snippet.Line.Code loc.PhysicalLocation.Region.Snippet = snip if !locationSaved { // To avoid overloading log file, we only save the 1st location, or source, as in the webview result.Locations = append(result.Locations, loc) locationSaved = true } //Related Locations relatedLocation := *new(format.RelatedLocation) relatedLocation.ID = k + 1 relatedLocation.PhysicalLocation = *new(format.RelatedPhysicalLocation) relatedLocation.PhysicalLocation.ArtifactLocation = loc.PhysicalLocation.ArtifactLocation relatedLocation.PhysicalLocation.Region = *new(format.RelatedRegion) relatedLocation.PhysicalLocation.Region.StartLine = loc.PhysicalLocation.Region.StartLine relatedLocation.PhysicalLocation.Region.StartColumn = cxxml.Query[i].Result[j].Path.PathNode[k].Column result.RelatedLocations = append(result.RelatedLocations, relatedLocation) threadFlowLocation := *new(format.Locations) tfloc := new(format.Location) tfloc.PhysicalLocation.ArtifactLocation.URI = cxxml.Query[i].Result[j].FileName tfloc.PhysicalLocation.Region.StartLine = cxxml.Query[i].Result[j].Path.PathNode[k].Line tfloc.PhysicalLocation.Region.EndLine = cxxml.Query[i].Result[j].Path.PathNode[k].Line tfloc.PhysicalLocation.Region.StartColumn = cxxml.Query[i].Result[j].Path.PathNode[k].Column tfloc.PhysicalLocation.Region.Snippet = snip threadFlowLocation.Location = tfloc threadflow.Locations = append(threadflow.Locations, threadFlowLocation) } codeflow.ThreadFlows = append(codeflow.ThreadFlows, threadflow) result.CodeFlows = append(result.CodeFlows, codeflow) result.PartialFingerprints.CheckmarxSimilarityID = cxxml.Query[i].Result[j].Path.SimilarityID result.PartialFingerprints.PrimaryLocationLineHash = cxxml.Query[i].Result[j].Path.SimilarityID //Properties props := new(format.SarifProperties) props.Audited = false if cxxml.Query[i].Result[j].Remark != "" { props.Audited = true } props.CheckmarxSimilarityID = cxxml.Query[i].Result[j].Path.SimilarityID props.InstanceID = cxxml.Query[i].Result[j].Path.ResultID + "-" + strconv.Itoa(cxxml.Query[i].Result[j].Path.PathID) props.ToolSeverity = cxxml.Query[i].Result[j].Severity props.ToolSeverityIndex = cxxml.Query[i].Result[j].SeverityIndex // classify into audit groups switch cxxml.Query[i].Result[j].Severity { case "High", "Medium": props.AuditRequirement = format.AUDIT_REQUIREMENT_GROUP_1_DESC props.AuditRequirementIndex = format.AUDIT_REQUIREMENT_GROUP_1_INDEX break case "Low": props.AuditRequirement = format.AUDIT_REQUIREMENT_GROUP_2_DESC props.AuditRequirementIndex = format.AUDIT_REQUIREMENT_GROUP_2_INDEX break case "Information": props.AuditRequirement = format.AUDIT_REQUIREMENT_GROUP_3_DESC props.AuditRequirementIndex = format.AUDIT_REQUIREMENT_GROUP_3_INDEX break } props.ToolStateIndex = cxxml.Query[i].Result[j].State switch cxxml.Query[i].Result[j].State { case 1: props.ToolState = "NotExploitable" break case 2: props.ToolState = "Confirmed" break case 3: props.ToolState = "Urgent" break case 4: props.ToolState = "ProposedNotExploitable" break default: props.ToolState = "ToVerify" // Includes case 0 break } props.ToolAuditMessage = "" if cxxml.Query[i].Result[j].Remark != "" { remarks := strings.Split(cxxml.Query[i].Result[j].Remark, "\n") messageCandidates := []string{} for cnd := 0; cnd < len(remarks); cnd++ { candidate := strings.Split(remarks[cnd], "]: ") if len(candidate) == 1 { if len(candidate[0]) != 0 { messageCandidates = append([]string{strings.Trim(candidate[0], "\r\n")}, messageCandidates...) } continue } else if len(candidate) == 0 { continue } messageCandidates = append([]string{strings.Trim(candidate[1], "\r\n")}, messageCandidates...) //Append in reverse order, trim to remove extra \r } props.ToolAuditMessage = strings.Join(messageCandidates, " \n ") } props.RuleGUID = cxxml.Query[i].ID props.UnifiedAuditState = "" result.Properties = props //Finalize sarif.Runs[0].Results = append(sarif.Runs[0].Results, result) } //handle the rules array rule := *new(format.SarifRule) rule.ID = "checkmarx-" + cxxml.Query[i].Language + "/" + cxxml.Query[i].ID words := strings.Split(cxxml.Query[i].Name, "_") for w := 0; w < len(words); w++ { words[w] = piperutils.Title(strings.ToLower(words[w])) } rule.Name = strings.Join(words, "") rule.HelpURI = baseURL + "queryID=" + cxxml.Query[i].ID + "&queryVersionCode=" + cxxml.Query[i].QueryVersionCode + "&queryTitle=" + cxxml.Query[i].Name rule.Help = new(format.Help) rule.Help.Text = rule.HelpURI rule.ShortDescription = new(format.Message) rule.ShortDescription.Text = cxxml.Query[i].Name rule.Properties = new(format.SarifRuleProperties) /*if apiDescription != "" { rule.FullDescription = new(format.Message) rule.FullDescription.Text = apiDescription } else */if cxxml.Query[i].Categories != "" { rule.FullDescription = new(format.Message) rule.FullDescription.Text = cxxml.Query[i].Categories //split categories on ; cats := strings.Split(cxxml.Query[i].Categories, ";") for cat := 0; cat < len(cats); cat++ { rule.Properties.Tags = append(rule.Properties.Tags, cats[cat]) } } switch cxxml.Query[i].SeverityIndex { case 0: rule.Properties.SecuritySeverity = "0.0" case 1: rule.Properties.SecuritySeverity = "2.0" case 2: rule.Properties.SecuritySeverity = "5.0" case 3: rule.Properties.SecuritySeverity = "7.0" default: rule.Properties.SecuritySeverity = "10.0" } if cxxml.Query[i].CweID != "" { rule.Properties.Tags = append(rule.Properties.Tags, "external/cwe/cwe-"+cxxml.Query[i].CweID) } rulesArray = append(rulesArray, rule) } // Handle driver object log.Entry().Debug("[SARIF] Now handling driver object.") tool := *new(format.Tool) tool.Driver = *new(format.Driver) tool.Driver.Name = "Checkmarx SCA" versionData := strings.Split(cxxml.CheckmarxVersion, "V ") if len(versionData) > 1 { // Safety check tool.Driver.Version = strings.Split(cxxml.CheckmarxVersion, "V ")[1] } else { tool.Driver.Version = cxxml.CheckmarxVersion // Safe case } tool.Driver.InformationUri = "https://checkmarx.atlassian.net/wiki/spaces/KC/pages/1170245301/Navigating+Scan+Results+v9.0.0+to+v9.2.0" tool.Driver.Rules = rulesArray sarif.Runs[0].Tool = tool //handle automationDetails sarif.Runs[0].AutomationDetails = &format.AutomationDetails{Id: cxxml.DeepLink} // Use deeplink to pass a maximum of information //handle taxonomies //Only one exists apparently: CWE. It is fixed taxonomy := *new(format.Taxonomies) 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) // Add a conversion object to highlight this isn't native SARIF conversion := new(format.Conversion) conversion.Tool.Driver.Name = "Piper Checkmarx XML 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.Account = cxxml.InitiatorName sarif.Runs[0].Conversion = conversion return sarif, nil }