Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SARIF Output Support #946

Merged
merged 2 commits into from
Feb 2, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cmd/app/start/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ func (s *Start) CreateStartCommand() *cobra.Command {
StringP(
"output-format", "o",
s.configs.PrintOutputType,
`Output format of analysis ("text"|"json"|"sonarqube"). For json and sonarqube --json-output-file is required`,
`Output format of analysis ("text"|"json"|"sarif"|"sonarqube"). For json, sarif, and sonarqube --json-output-file is required`,
)

startCmd.PersistentFlags().
Expand Down
23 changes: 23 additions & 0 deletions internal/controllers/printresults/print_results.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,17 @@ import (
sq "github.com/ZupIT/horusec/internal/entities/sonarqube"
"github.com/ZupIT/horusec/internal/enums/outputtype"
"github.com/ZupIT/horusec/internal/helpers/messages"
"github.com/ZupIT/horusec/internal/services/sarif"
"github.com/ZupIT/horusec/internal/services/sonarqube"
"github.com/ZupIT/horusec/internal/utils/file"
)

var ErrOutputJSON = errors.New("{HORUSEC_CLI} error creating and/or writing to the specified file")

type SarifConverter interface {
ConvertVulnerabilityToSarif() sarif.Report
}

type SonarQubeConverter interface {
ConvertVulnerabilityToSonarQube() sq.Report
}
Expand All @@ -54,6 +59,7 @@ type PrintResults struct {
analysis *analysis.Analysis
config *config.Config
totalVulns int
sarifService SarifConverter
sonarqubeService SonarQubeConverter
textOutput string
writer io.Writer
Expand All @@ -64,6 +70,7 @@ func NewPrintResults(entity *analysis.Analysis, cfg *config.Config) *PrintResult
return &PrintResults{
analysis: entity,
config: cfg,
sarifService: sarif.NewSarif(entity),
sonarqubeService: sonarqube.NewSonarQube(entity),
writer: os.Stdout,
totalVulns: 0,
Expand Down Expand Up @@ -95,6 +102,8 @@ func (pr *PrintResults) printByOutputType() error {
switch {
case pr.config.PrintOutputType == outputtype.JSON:
return pr.printResultsJSON()
case pr.config.PrintOutputType == outputtype.Sarif:
return pr.printResultsSarif()
case pr.config.PrintOutputType == outputtype.SonarQube:
return pr.printResultsSonarQube()
default:
Expand Down Expand Up @@ -135,6 +144,20 @@ func (pr *PrintResults) printResultsJSON() error {
return pr.createOutputJSON(b)
}

func (pr *PrintResults) printResultsSarif() error {
logger.LogInfoWithLevel(messages.MsgInfoStartGenerateSARIFFile)

report := pr.sarifService.ConvertVulnerabilityToSarif()

b, err := json.MarshalIndent(report, "", " ")
if err != nil {
logger.LogErrorWithLevel(messages.MsgErrorGenerateJSONFile, err)
return err
}

return pr.createOutputJSON(b)
}

func (pr *PrintResults) printResultsSonarQube() error {
logger.LogInfoWithLevel(messages.MsgInfoStartGenerateSonarQubeFile)

Expand Down
1 change: 1 addition & 0 deletions internal/enums/outputtype/output_type.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,6 @@ package outputtype
const (
Text = "text"
JSON = "json"
Sarif = "sarif"
SonarQube = "sonarqube"
)
1 change: 1 addition & 0 deletions internal/helpers/messages/info.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const (
https://git-scm.com/downloads
`
MsgInfoStartGenerateSonarQubeFile = "{HORUSEC_CLI} Generating SonarQube output..."
MsgInfoStartGenerateSARIFFile = "{HORUSEC_CLI} Generating SARIF output..."
MsgInfoStartWriteFile = "{HORUSEC_CLI} Writing output JSON to file in the path: "
MsgInfoAnalysisLoading = " Scanning code ..."
MsgInfoDockerLowerVersion = "{HORUSEC_CLI} We recommend version 19.03 or higher of the docker." +
Expand Down
191 changes: 191 additions & 0 deletions internal/services/sarif/sarif.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
// Copyright 2022 ZUP IT SERVICOS EM TECNOLOGIA E INOVACAO SA
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package sarif

import (
"strconv"
"strings"

"github.com/ZupIT/horusec-devkit/pkg/entities/analysis"
"github.com/ZupIT/horusec-devkit/pkg/entities/vulnerability"
"github.com/ZupIT/horusec-devkit/pkg/enums/severities"

"github.com/ZupIT/horusec/cmd/app/version"
)

type Sarif struct {
analysiss *analysis.Analysis

resultsByTool map[string][]Result
rulesByToolAndID map[string]map[string]Rule
artifactsByToolAndName map[string]map[string]Artifact
}

func NewSarif(analysiss *analysis.Analysis) *Sarif {
return &Sarif{
analysiss: analysiss,
resultsByTool: make(map[string][]Result),
rulesByToolAndID: make(map[string]map[string]Rule),
artifactsByToolAndName: make(map[string]map[string]Artifact),
}
}

func (s *Sarif) ConvertVulnerabilityToSarif() (report Report) {
report.Runs = []ReportRun{}
s.populateReferenceMaps(&report)
s.buildReportRun(&report)

// SARIF output format version/schema
report.Version = "2.1.0"
report.SchemaURI = "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json"

return report
}

// populateReferenceMaps iterates all vulnerabilities and break them into the maps/associations necessary
// to build the SARIF report; resultsByTool, artifactsByToolAndName, rulesByToolAndID.
// resultsByTool: Groups individual vuln issue types into the tool which created them
// artifactsByToolAndName: Groups areas of the scanned code into the tool/issue type creating them
// rulesByToolAndId: Groups issue types which are presented by the tool, based on the vulns given
// This prevents the need from iterating over the entire vuln list multiple times.
func (s *Sarif) populateReferenceMaps(report *Report) {
runsByTool := make(map[string]ReportRun)
for index := range s.analysiss.AnalysisVulnerabilities {
vuln := &s.analysiss.AnalysisVulnerabilities[index].Vulnerability
if _, exists := runsByTool[string(vuln.SecurityTool)]; !exists {
report.Runs = append(report.Runs, s.initToolStructure(vuln, runsByTool))
}
s.resultsByTool[string(vuln.SecurityTool)] = append(s.resultsByTool[string(vuln.SecurityTool)], s.newResult(vuln))
artifact := s.newArtifact(vuln)
s.artifactsByToolAndName[string(vuln.SecurityTool)][artifact.Location.URI] = artifact
rule := s.newRule(vuln)
s.rulesByToolAndID[string(vuln.SecurityTool)][rule.ID] = rule
}
}

// buildReportRun builds a single "run" for the report. For SARIF, a "run" has a single tool.
// Therefore, each group of vulnerabilities reported by a specific tool are all
// organized in the same "run".
func (s *Sarif) buildReportRun(report *Report) {
for idx, runReport := range report.Runs {
for _, artifact := range s.artifactsByToolAndName[runReport.Tool.Driver.Name] {
report.Runs[idx].Artifacts = append(report.Runs[idx].Artifacts, artifact)
}
for _, rule := range s.rulesByToolAndID[runReport.Tool.Driver.Name] {
report.Runs[idx].Tool.Driver.Rules = append(report.Runs[idx].Tool.Driver.Rules, rule)
}
report.Runs[idx].Results = append(report.Runs[idx].Results, s.resultsByTool[runReport.Tool.Driver.Name]...)
}
}

// initToolStructure initializes the structure for a single report "run", as well as updating
// the association maps in the SARIF object to reflect the run's existence
func (s *Sarif) initToolStructure(
vulnerabilityy *vulnerability.Vulnerability,
runsByTool map[string]ReportRun) ReportRun {
s.rulesByToolAndID[string(vulnerabilityy.SecurityTool)] = make(map[string]Rule)
s.artifactsByToolAndName[string(vulnerabilityy.SecurityTool)] = make(map[string]Artifact)

reportRun := ReportRun{
Tool: s.newTool(vulnerabilityy),
}

runsByTool[string(vulnerabilityy.SecurityTool)] = reportRun

return reportRun
}

func (s *Sarif) convertNonZeroIntStr(str string) int {
newInt, _ := strconv.Atoi(str)
if newInt > 0 {
return newInt
}
return 1
}

func (s *Sarif) newTool(vulnerabilityy *vulnerability.Vulnerability) ScanTool {
return ScanTool{
Driver: ScanToolDriver{
Name: vulnerabilityy.SecurityTool.ToString(),
MoreInformationURI: "https://docs.horusec.io/docs/cli/analysis-tools/overview/",
Version: version.Version,
},
}
}

func (s *Sarif) newRule(vulnerabilityy *vulnerability.Vulnerability) Rule {
return Rule{
ID: vulnerabilityy.RuleID,
ShortDescription: TextDisplayComponent{
Text: vulnerabilityy.Details,
},
FullDescription: TextDisplayComponent{
Text: vulnerabilityy.Details,
},
HelpURI: "https://docs.horusec.io/docs/cli/analysis-tools/overview/",
Name: strings.Split(vulnerabilityy.Details, "\n")[0],
}
}

func (s *Sarif) newArtifact(vulnerabilityy *vulnerability.Vulnerability) Artifact {
return Artifact{
Location: LocationComponent{
URI: vulnerabilityy.File,
},
}
}

func (s *Sarif) newResult(vulnerabilityy *vulnerability.Vulnerability) Result {
return Result{
Message: TextDisplayComponent{
Text: vulnerabilityy.Details,
},
Level: ResultLevel(s.convertHorusecSeverityToSarif(vulnerabilityy.Severity)),
Locations: []Location{s.createLocation(vulnerabilityy)},
RuleID: vulnerabilityy.RuleID,
}
}

func (s *Sarif) createLocation(vulnerabilityy *vulnerability.Vulnerability) Location {
return Location{
PhysicalLocation: PhysicalLocation{
ArtifactLocation: LocationComponent{
URI: vulnerabilityy.File,
},
Region: SnippetRegion{
Snippet: TextDisplayComponent{
Text: vulnerabilityy.Code,
},
StartLine: s.convertNonZeroIntStr(vulnerabilityy.Line),
StartColumn: s.convertNonZeroIntStr(vulnerabilityy.Column),
},
},
}
}

func (s *Sarif) convertHorusecSeverityToSarif(severity severities.Severity) string {
return s.getSarifSeverityMap()[severity]
}

func (s *Sarif) getSarifSeverityMap() map[severities.Severity]string {
return map[severities.Severity]string{
severities.Critical: Error,
severities.High: Error,
severities.Medium: Warning,
severities.Low: Note,
severities.Unknown: Note,
severities.Info: Note,
}
}
98 changes: 98 additions & 0 deletions internal/services/sarif/sarif_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// Copyright 2022 ZUP IT SERVICOS EM TECNOLOGIA E INOVACAO SA
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package sarif

import (
"testing"
"time"

"github.com/ZupIT/horusec-devkit/pkg/entities/analysis"
"github.com/ZupIT/horusec-devkit/pkg/entities/vulnerability"
analysisenum "github.com/ZupIT/horusec-devkit/pkg/enums/analysis"
"github.com/ZupIT/horusec-devkit/pkg/enums/languages"
"github.com/ZupIT/horusec-devkit/pkg/enums/severities"
"github.com/ZupIT/horusec-devkit/pkg/enums/tools"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
)

func TestConvertVulnerabilityDataToSarif(t *testing.T) {
t.Run("should successfully parse analysis to sarif output", func(t *testing.T) {
entity := &analysis.Analysis{
ID: uuid.New(),
CreatedAt: time.Now(),
Status: analysisenum.Success,
AnalysisVulnerabilities: []analysis.AnalysisVulnerabilities{
{
Vulnerability: vulnerability.Vulnerability{
Line: "1",
Column: "1",
Severity: severities.High,
File: "sample.c",
Code: "assert(true == false);",
Details: "Universe failure; please restart reality",
SecurityTool: tools.Bandit,
Language: languages.C,
},
},
},
}

service := NewSarif(entity)

result := service.ConvertVulnerabilityToSarif()
assert.NotEmpty(t, result.Runs)
})

t.Run("field sets should be populated", func(t *testing.T) {
analysis := &analysis.Analysis{
ID: uuid.New(),
CreatedAt: time.Now(),
Status: analysisenum.Success,
AnalysisVulnerabilities: []analysis.AnalysisVulnerabilities{
{
Vulnerability: vulnerability.Vulnerability{
Line: "1",
Column: "1",
Severity: severities.High,
File: "sample.c",
Code: "assert(true == false);",
Details: "Universe failure; please restart reality",
SecurityTool: tools.Bandit,
Language: languages.C,
},
},
},
}

service := NewSarif(analysis)

result := service.ConvertVulnerabilityToSarif()
assert.NotNil(t, result.Runs)
assert.Len(t, result.Runs, 1)
assert.Len(t, result.Runs[0].Results, 1)

assert.EqualValues(t, result.Runs[0].Results[0].Message.Text, "Universe failure; please restart reality")

assert.EqualValues(t, result.Runs[0].Results[0].Locations[0].PhysicalLocation.ArtifactLocation.URI, "sample.c")

assert.EqualValues(t, result.Runs[0].Results[0].Locations[0].PhysicalLocation.Region.Snippet.Text, "assert(true == false);")
assert.EqualValues(t, result.Runs[0].Results[0].Locations[0].PhysicalLocation.Region.StartColumn, 1)
assert.EqualValues(t, result.Runs[0].Results[0].Locations[0].PhysicalLocation.Region.StartLine, 1)

assert.NotNil(t, result.Runs[0].Tool)
assert.EqualValues(t, result.Runs[0].Tool.Driver.Name, "Bandit")
})
}
Loading