github.com/khulnasoft-lab/defsec@v1.0.5-0.20230827010352-5e9f46893d95/pkg/formatters/junit.go (about) 1 package formatters 2 3 import ( 4 "encoding/xml" 5 "fmt" 6 "os" 7 "path/filepath" 8 9 "github.com/khulnasoft-lab/defsec/pkg/scan" 10 ) 11 12 // see https://github.com/windyroad/JUnit-Schema/blob/master/JUnit.xsd 13 // tested with CircleCI 14 15 // jUnitTestSuite is a single JUnit test suite which may contain many 16 // testcases. 17 type jUnitTestSuite struct { 18 XMLName xml.Name `xml:"testsuite"` 19 Name string `xml:"name,attr"` 20 Failures string `xml:"failures,attr"` 21 Skipped string `xml:"skipped,attr,omitempty"` 22 Tests string `xml:"tests,attr"` 23 TestCases []jUnitTestCase `xml:"testcase"` 24 } 25 26 // jUnitTestCase is a single test case with its result. 27 type jUnitTestCase struct { 28 XMLName xml.Name `xml:"testcase"` 29 Classname string `xml:"classname,attr"` 30 Name string `xml:"name,attr"` 31 Time string `xml:"time,attr"` 32 Failure *jUnitFailure `xml:"failure,omitempty"` 33 Skipped *jUnitSkipped `xml:"skipped,omitempty"` 34 } 35 36 // jUnitFailure contains data related to a failed test. 37 type jUnitFailure struct { 38 Message string `xml:"message,attr"` 39 Type string `xml:"type,attr"` 40 Contents string `xml:",chardata"` 41 } 42 43 // jUnitSkipped defines a not executed test. 44 type jUnitSkipped struct { 45 Message string `xml:"message,attr,omitempty"` 46 } 47 48 func outputJUnit(b ConfigurableFormatter, results scan.Results) error { 49 output := jUnitTestSuite{ 50 Name: filepath.Base(os.Args[0]), 51 Failures: fmt.Sprintf("%d", countWithStatus(results, scan.StatusFailed)), 52 Tests: fmt.Sprintf("%d", len(results)), 53 } 54 55 skipped := countWithStatus(results, scan.StatusIgnored) 56 if skipped > 0 { 57 output.Skipped = fmt.Sprintf("%d", skipped) 58 } 59 60 for _, res := range results { 61 switch res.Status() { 62 case scan.StatusIgnored: 63 if !b.IncludeIgnored() { 64 continue 65 } 66 case scan.StatusPassed: 67 if !b.IncludePassed() { 68 continue 69 } 70 } 71 path := b.Path(res, res.Metadata()) 72 output.TestCases = append(output.TestCases, 73 jUnitTestCase{ 74 Classname: path, 75 Name: fmt.Sprintf("[%s][%s] - %s", res.Rule().LongID(), res.Severity(), res.Description()), 76 Time: "0", 77 Failure: buildFailure(b, res), 78 Skipped: buildSkipped(res), 79 }, 80 ) 81 } 82 83 if _, err := b.Writer().Write([]byte(xml.Header)); err != nil { 84 return err 85 } 86 87 xmlEncoder := xml.NewEncoder(b.Writer()) 88 xmlEncoder.Indent("", "\t") 89 90 return xmlEncoder.Encode(output) 91 } 92 93 // highlight the lines of code which caused a problem, if available 94 func highlightCodeJunit(res scan.Result) string { 95 code, err := res.GetCode() 96 if err != nil { 97 return "" 98 } 99 var output string 100 for _, line := range code.Lines { 101 if line.IsCause { 102 output += fmt.Sprintf("%s\n", line.Content) 103 } 104 } 105 return output 106 } 107 108 func buildFailure(b ConfigurableFormatter, res scan.Result) *jUnitFailure { 109 if res.Status() != scan.StatusFailed { 110 return nil 111 } 112 113 var link string 114 links := b.GetLinks(res) 115 if len(links) > 0 { 116 link = links[0] 117 } 118 119 lineInfo := fmt.Sprintf("%d-%d", res.Range().GetStartLine(), res.Range().GetEndLine()) 120 if !res.Range().IsMultiLine() { 121 lineInfo = fmt.Sprintf("%d", res.Range().GetStartLine()) 122 } 123 location := fmt.Sprintf("%s:%s", b.Path(res, res.Metadata()), lineInfo) 124 125 return &jUnitFailure{ 126 Message: res.Description(), 127 Contents: fmt.Sprintf("%s\n\n%s\n\nSee %s", 128 location, 129 highlightCodeJunit(res), 130 link, 131 ), 132 } 133 } 134 135 func buildSkipped(res scan.Result) *jUnitSkipped { 136 if res.Status() != scan.StatusIgnored { 137 return nil 138 } 139 return &jUnitSkipped{ 140 Message: res.Description(), 141 } 142 } 143 144 func countWithStatus(results []scan.Result, status scan.Status) int { 145 count := 0 146 147 for _, res := range results { 148 if res.Status() == status { 149 count++ 150 } 151 } 152 153 return count 154 }