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  }