blob: a41efa26f11694b778b2db85aed79dcea366fba2 [file] [log] [blame]
// Copyright 2023 The Shac Authors
//
// 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 reporting
import (
"context"
"io"
"sort"
"sync"
"time"
"go.fuchsia.dev/shac-project/shac/internal/engine"
"go.fuchsia.dev/shac-project/shac/internal/sarif"
"google.golang.org/protobuf/encoding/protojson"
)
// SarifReport converts findings into SARIF JSON output.
type SarifReport struct {
// SARIF output gets written here when Close() is called.
Out io.Writer
mu sync.Mutex
resultsByCheck map[string][]*sarif.Result
}
func (sr *SarifReport) EmitFinding(ctx context.Context, check string, level engine.Level, message, root, file string, s engine.Span, replacements []string) error {
levelMap := map[engine.Level]string{
engine.Notice: sarif.Note,
engine.Warning: sarif.Warning,
engine.Error: sarif.Error,
}
region := &sarif.Region{
StartLine: int32(s.Start.Line),
EndLine: int32(s.End.Line),
StartColumn: int32(s.Start.Col),
EndColumn: int32(s.End.Col),
}
var fixes []*sarif.Fix
for _, repl := range replacements {
fixes = append(fixes, &sarif.Fix{
ArtifactChanges: []*sarif.ArtifactChange{
{
ArtifactLocation: &sarif.ArtifactLocation{Uri: file},
Replacements: []*sarif.Replacement{
{
DeletedRegion: region,
InsertedContent: &sarif.ArtifactContent{Text: repl},
},
},
},
},
})
}
result := &sarif.Result{
// TODO(olivernewman): Set RuleId field. The SARIF specification states
// that ruleId "SHALL" be set, and "Not all existing analysis tools emit
// the equivalent of a ruleId in their output. A SARIF converter which
// converts the output of such an analysis tool to the SARIF format
// SHOULD synthesize ruleId from other information available in the
// analysis tool's output."
// https://docs.oasis-open.org/sarif/sarif/v2.1.0/os/sarif-v2.1.0-os.html#_Toc34317643
Level: levelMap[level],
Message: &sarif.Message{Text: message},
Locations: []*sarif.Location{
{
PhysicalLocation: &sarif.PhysicalLocation{
ArtifactLocation: &sarif.ArtifactLocation{Uri: file},
Region: region,
},
},
},
Fixes: fixes,
}
sr.mu.Lock()
if sr.resultsByCheck == nil {
sr.resultsByCheck = make(map[string][]*sarif.Result)
}
sr.resultsByCheck[check] = append(sr.resultsByCheck[check], result)
sr.mu.Unlock()
return nil
}
func (sr *SarifReport) EmitArtifact(ctx context.Context, root, check, file string, content []byte) error {
// TODO(olivernewman): Emit artifacts via the `artifacts` SARIF property:
// https://docs.oasis-open.org/sarif/sarif/v2.1.0/os/sarif-v2.1.0-os.html#_Toc34317499
return nil
}
func (sr *SarifReport) CheckCompleted(ctx context.Context, check string, start time.Time, d time.Duration, level engine.Level, err error) {
}
func (sr *SarifReport) Print(context.Context, string, string, int, string) {}
func (sr *SarifReport) Close() error {
doc := &sarif.Document{Version: sarif.Version}
// Sort for determinism.
var sortedChecks []string
for check := range sr.resultsByCheck {
sortedChecks = append(sortedChecks, check)
}
sort.Strings(sortedChecks)
for _, check := range sortedChecks {
results := sr.resultsByCheck[check]
doc.Runs = append(doc.Runs, &sarif.Run{
Tool: &sarif.Tool{
Driver: &sarif.ToolComponent{Name: check},
},
Results: results,
})
}
b, err := protojson.MarshalOptions{
Multiline: true,
UseProtoNames: false,
}.Marshal(doc)
if err != nil {
return err
}
_, err = sr.Out.Write(b)
return err
}