2022-04-04 16:12:35 +02:00
package checkmarx
import (
"bytes"
"encoding/xml"
2022-06-16 15:24:23 +02:00
"fmt"
2022-04-04 16:12:35 +02:00
"io/ioutil"
"strconv"
"strings"
2022-06-16 15:24:23 +02:00
"time"
2022-04-04 16:12:35 +02:00
"github.com/SAP/jenkins-library/pkg/format"
"github.com/SAP/jenkins-library/pkg/log"
2022-05-20 18:50:03 +02:00
"github.com/SAP/jenkins-library/pkg/piperutils"
2022-04-04 16:12:35 +02:00
"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
2022-06-01 15:48:56 +02:00
func ConvertCxxmlToSarif ( sys System , xmlReportName string , scanID int ) ( format . SARIF , error ) {
2022-04-04 16:12:35 +02:00
var sarif format . SARIF
2022-04-26 12:34:54 +02:00
log . Entry ( ) . Debug ( "Reading audit file." )
2022-04-04 16:12:35 +02:00
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." )
2022-06-01 15:48:56 +02:00
return Parse ( sys , data , scanID )
2022-04-04 16:12:35 +02:00
}
// Parse function
2022-06-01 15:48:56 +02:00
func Parse ( sys System , data [ ] byte , scanID int ) ( format . SARIF , error ) {
2022-04-04 16:12:35 +02:00
reader := bytes . NewReader ( data )
decoder := xml . NewDecoder ( reader )
2022-06-16 15:24:23 +02:00
start := time . Now ( ) // For the conversion start time
2022-04-04 16:12:35 +02:00
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 { }
2022-05-19 14:57:13 +02:00
baseURL := "https://" + strings . Split ( cxxml . DeepLink , "/" ) [ 2 ] + "/CxWebClient/ScanQueryDescription.aspx?"
2022-04-04 16:12:35 +02:00
cweIdsForTaxonomies := make ( map [ string ] int ) //use a map to avoid duplicates
cweCounter := 0
2022-06-09 10:32:08 +02:00
//maxretries := 5
2022-04-04 16:12:35 +02:00
//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")
2022-04-26 12:34:54 +02:00
log . Entry ( ) . Debug ( "[SARIF] Now handling results." )
2022-04-04 16:12:35 +02:00
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 ++ {
2022-06-09 10:32:08 +02:00
var apiDescription string
2022-04-04 16:12:35 +02:00
result := * new ( format . Results )
2022-06-09 10:32:08 +02:00
// COMMENTED UNTIL CHECKMARX API WORK AS INTENDED
2022-06-01 15:48:56 +02:00
// For rules later, fetch description
2022-06-09 10:32:08 +02:00
/ * 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 {
2022-06-01 15:48:56 +02:00
if sys != nil {
apiShortDescription , err := sys . GetShortDescription ( scanID , cxxml . Query [ i ] . Result [ j ] . Path . PathID )
if err != nil {
2022-06-09 10:32:08 +02:00
maxretries = maxretries - 1
log . Entry ( ) . Debug ( "request failed: remaining retries " , maxretries )
2022-06-01 15:48:56 +02:00
log . Entry ( ) . Error ( err )
} else {
apiDescription = apiShortDescription . Text
}
2022-06-09 10:32:08 +02:00
} else {
maxretries = maxretries - 1
log . Entry ( ) . Debug ( "request failed: no system instance, remaining retries " , maxretries )
2022-06-01 15:48:56 +02:00
}
2022-06-09 10:32:08 +02:00
} * /
2022-06-01 15:48:56 +02:00
2022-04-04 16:12:35 +02:00
//General
2022-06-17 08:53:44 +02:00
result . RuleID = "checkmarx-" + cxxml . Query [ i ] . Language + "/" + cxxml . Query [ i ] . ID
2022-04-04 16:12:35 +02:00
result . RuleIndex = cweIdsForTaxonomies [ cxxml . Query [ i ] . CweID ]
result . Level = "none"
msg := new ( format . Message )
2022-05-19 14:57:13 +02:00
//msg.Text = cxxml.Query[i].Name + ": " + cxxml.Query[i].Categories
2022-06-09 10:32:08 +02:00
if apiDescription != "" {
msg . Text = apiDescription
} else {
msg . Text = cxxml . Query [ i ] . Name
}
2022-04-04 16:12:35 +02:00
result . Message = msg
2022-05-19 14:57:13 +02:00
2022-04-04 16:12:35 +02:00
//Locations
2022-06-22 08:54:24 +02:00
codeflow := * new ( format . CodeFlow )
threadflow := * new ( format . ThreadFlow )
locationSaved := false
2022-04-04 16:12:35 +02:00
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
2022-05-19 14:57:13 +02:00
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
2022-04-04 16:12:35 +02:00
snip := new ( format . SnippetSarif )
snip . Text = cxxml . Query [ i ] . Result [ j ] . Path . PathNode [ k ] . Snippet . Line . Code
loc . PhysicalLocation . Region . Snippet = snip
2022-06-22 08:54:24 +02:00
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
}
2022-04-04 16:12:35 +02:00
//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 )
2022-06-22 08:54:24 +02:00
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 )
2022-04-04 16:12:35 +02:00
}
2022-06-22 08:54:24 +02:00
codeflow . ThreadFlows = append ( codeflow . ThreadFlows , threadflow )
result . CodeFlows = append ( result . CodeFlows , codeflow )
2022-04-04 16:12:35 +02:00
2022-05-19 14:57:13 +02:00
result . PartialFingerprints . CheckmarxSimilarityID = cxxml . Query [ i ] . Result [ j ] . Path . SimilarityID
result . PartialFingerprints . PrimaryLocationLineHash = cxxml . Query [ i ] . Result [ j ] . Path . SimilarityID
2022-04-04 16:12:35 +02:00
//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
2022-07-21 11:15:55 +02:00
// 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
}
2022-04-04 16:12:35 +02:00
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 " )
}
2022-06-17 08:53:44 +02:00
props . RuleGUID = cxxml . Query [ i ] . ID
2022-04-04 16:12:35 +02:00
props . UnifiedAuditState = ""
result . Properties = props
//Finalize
sarif . Runs [ 0 ] . Results = append ( sarif . Runs [ 0 ] . Results , result )
}
//handle the rules array
rule := * new ( format . SarifRule )
2022-06-17 08:53:44 +02:00
rule . ID = "checkmarx-" + cxxml . Query [ i ] . Language + "/" + cxxml . Query [ i ] . ID
2022-05-19 14:57:13 +02:00
words := strings . Split ( cxxml . Query [ i ] . Name , "_" )
for w := 0 ; w < len ( words ) ; w ++ {
2022-05-20 18:50:03 +02:00
words [ w ] = piperutils . Title ( strings . ToLower ( words [ w ] ) )
2022-05-19 14:57:13 +02:00
}
rule . Name = strings . Join ( words , "" )
2022-04-04 16:12:35 +02:00
rule . HelpURI = baseURL + "queryID=" + cxxml . Query [ i ] . ID + "&queryVersionCode=" + cxxml . Query [ i ] . QueryVersionCode + "&queryTitle=" + cxxml . Query [ i ] . Name
2022-05-19 14:57:13 +02:00
rule . Help = new ( format . Help )
rule . Help . Text = rule . HelpURI
rule . ShortDescription = new ( format . Message )
rule . ShortDescription . Text = cxxml . Query [ i ] . Name
2022-06-09 10:32:08 +02:00
rule . Properties = new ( format . SarifRuleProperties )
/ * if apiDescription != "" {
2022-06-01 15:48:56 +02:00
rule . FullDescription = new ( format . Message )
rule . FullDescription . Text = apiDescription
2022-06-09 10:32:08 +02:00
} else * / if cxxml . Query [ i ] . Categories != "" {
2022-05-19 14:57:13 +02:00
rule . FullDescription = new ( format . Message )
rule . FullDescription . Text = cxxml . Query [ i ] . Categories
2022-06-09 10:32:08 +02:00
//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 ] )
}
2022-05-19 14:57:13 +02:00
}
2022-06-28 15:39:17 +02:00
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"
}
2022-06-09 10:32:08 +02:00
2022-05-19 14:57:13 +02:00
if cxxml . Query [ i ] . CweID != "" {
rule . Properties . Tags = append ( rule . Properties . Tags , "external/cwe/cwe-" + cxxml . Query [ i ] . CweID )
}
2022-04-04 16:12:35 +02:00
rulesArray = append ( rulesArray , rule )
}
// Handle driver object
2022-04-26 12:34:54 +02:00
log . Entry ( ) . Debug ( "[SARIF] Now handling driver object." )
2022-04-04 16:12:35 +02:00
tool := * new ( format . Tool )
tool . Driver = * new ( format . Driver )
tool . Driver . Name = "Checkmarx SCA"
2022-05-19 14:57:13 +02:00
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"
2022-04-04 16:12:35 +02:00
tool . Driver . Rules = rulesArray
sarif . Runs [ 0 ] . Tool = tool
//handle automationDetails
2022-10-10 10:06:20 +02:00
sarif . Runs [ 0 ] . AutomationDetails = & format . AutomationDetails { Id : cxxml . DeepLink } // Use deeplink to pass a maximum of information
2022-04-04 16:12:35 +02:00
//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 )
2022-06-16 15:24:23 +02:00
// 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
2022-04-04 16:12:35 +02:00
return sarif , nil
}