scorecard/finding/finding.go
Spencer Schrock ca944e8169
🌱 Change finding Values to map[string]string (#3837)
* make values map string -> string

Signed-off-by: Spencer Schrock <sschrock@google.com>

* fixup branch protection probes

Signed-off-by: Spencer Schrock <sschrock@google.com>

* fix sast probe

Signed-off-by: Spencer Schrock <sschrock@google.com>

* fix signed-releases probes

Signed-off-by: Spencer Schrock <sschrock@google.com>

* fix maintained probes

Signed-off-by: Spencer Schrock <sschrock@google.com>

* fix cii-best-practices probes

Signed-off-by: Spencer Schrock <sschrock@google.com>

* fix cii-best-practices eval

Signed-off-by: Spencer Schrock <sschrock@google.com>

* fix signed-releases eval

Signed-off-by: Spencer Schrock <sschrock@google.com>

* fix sast eval

Signed-off-by: Spencer Schrock <sschrock@google.com>

* fix maintained eval

Signed-off-by: Spencer Schrock <sschrock@google.com>

* fix permissions eval

Signed-off-by: Spencer Schrock <sschrock@google.com>

* appease the linter

Signed-off-by: Spencer Schrock <sschrock@google.com>

* standardize maintained key names

Signed-off-by: Spencer Schrock <sschrock@google.com>

* set lookback days value regardless of outcome

Signed-off-by: Spencer Schrock <sschrock@google.com>

---------

Signed-off-by: Spencer Schrock <sschrock@google.com>
2024-02-07 10:55:16 -08:00

303 lines
7.9 KiB
Go

// Copyright 2023 OpenSSF Scorecard 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 finding
import (
"embed"
"errors"
"fmt"
"reflect"
"strings"
"gopkg.in/yaml.v3"
"github.com/ossf/scorecard/v4/finding/probe"
)
// FileType is the type of a file.
type FileType int
const (
// FileTypeNone must be `0`.
FileTypeNone FileType = iota
// FileTypeSource is for source code files.
FileTypeSource
// FileTypeBinary is for binary files.
FileTypeBinary
// FileTypeText is for text files.
FileTypeText
// FileTypeURL for URLs.
FileTypeURL
// FileTypeBinaryVerified for verified binary files.
FileTypeBinaryVerified
)
// Location represents the location of a finding.
type Location struct {
LineStart *uint `json:"lineStart,omitempty"`
LineEnd *uint `json:"lineEnd,omitempty"`
Snippet *string `json:"snippet,omitempty"`
Path string `json:"path"`
Type FileType `json:"type"`
}
// Outcome is the result of a finding.
type Outcome int
// TODO(#2928): re-visit the finding definitions.
const (
// NOTE: The additional '_' are intended for future use.
// This allows adding outcomes without breaking the values
// of existing outcomes.
// OutcomeNegative indicates a negative outcome.
OutcomeNegative Outcome = iota
_
_
_
// OutcomeNotAvailable indicates an unavailable outcome,
// typically because an API call did not return an answer.
OutcomeNotAvailable
_
_
_
// OutcomeError indicates an errors while running.
// The results could not be determined.
OutcomeError
_
_
_
// OutcomePositive indicates a positive outcome.
OutcomePositive
_
_
_
// OutcomeNotSupported indicates a non-supported outcome.
OutcomeNotSupported
_
_
_
// OutcomeNotApplicable indicates if a finding should not
// be considered in evaluation.
OutcomeNotApplicable
)
// Finding represents a finding.
type Finding struct {
Location *Location `json:"location,omitempty"`
Remediation *probe.Remediation `json:"remediation,omitempty"`
Values map[string]string `json:"values,omitempty"`
Probe string `json:"probe"`
Message string `json:"message"`
Outcome Outcome `json:"outcome"`
}
// AnonymousFinding is a finding without a corresponding probe ID.
type AnonymousFinding struct {
Probe string `json:"probe,omitempty"`
Finding
}
var errInvalid = errors.New("invalid")
// FromBytes creates a finding for a probe given its config file's content.
func FromBytes(content []byte, probeID string) (*Finding, error) {
p, err := probe.FromBytes(content, probeID)
if err != nil {
//nolint:wrapcheck
return nil, err
}
f := &Finding{
Probe: p.ID,
Outcome: OutcomeNegative,
Remediation: p.Remediation,
}
return f, nil
}
// New creates a new finding.
func New(loc embed.FS, probeID string) (*Finding, error) {
p, err := probe.New(loc, probeID)
if err != nil {
return nil, fmt.Errorf("%w", err)
}
f := &Finding{
Probe: p.ID,
Outcome: OutcomeNegative,
Remediation: p.Remediation,
}
return f, nil
}
// NewWith create a finding with the desired location and outcome.
func NewWith(efs embed.FS, probeID, text string, loc *Location,
o Outcome,
) (*Finding, error) {
f, err := New(efs, probeID)
if err != nil {
return nil, fmt.Errorf("finding.New: %w", err)
}
f = f.WithMessage(text).WithOutcome(o).WithLocation(loc)
return f, nil
}
// NewWith create a negative finding with the desired location.
func NewNegative(efs embed.FS, probeID, text string, loc *Location,
) (*Finding, error) {
return NewWith(efs, probeID, text, loc, OutcomeNegative)
}
// NewNotAvailable create a finding with a NotAvailable outcome and the desired location.
func NewNotAvailable(efs embed.FS, probeID, text string, loc *Location,
) (*Finding, error) {
return NewWith(efs, probeID, text, loc, OutcomeNotAvailable)
}
// NewPositive create a positive finding with the desired location.
func NewPositive(efs embed.FS, probeID, text string, loc *Location,
) (*Finding, error) {
return NewWith(efs, probeID, text, loc, OutcomePositive)
}
// Anonymize removes the probe ID and outcome
// from the finding. It is a temporary solution
// to integrate the code in the details without exposing
// too much information.
func (f *Finding) Anonymize() *AnonymousFinding {
return &AnonymousFinding{Finding: *f}
}
// WithMessage adds a message to an existing finding.
// No copy is made.
func (f *Finding) WithMessage(text string) *Finding {
f.Message = text
return f
}
// UniqueProbesEqual checks the probe names present in a list of findings
// and compare them against an expected list.
func UniqueProbesEqual(findings []Finding, probes []string) bool {
// Collect unique probes from findings.
fm := make(map[string]bool)
for i := range findings {
f := &findings[i]
fm[f.Probe] = true
}
// Collect probes from list.
pm := make(map[string]bool)
for i := range probes {
p := &probes[i]
pm[*p] = true
}
return reflect.DeepEqual(pm, fm)
}
// WithLocation adds a location to an existing finding.
// No copy is made.
func (f *Finding) WithLocation(loc *Location) *Finding {
f.Location = loc
if f.Remediation != nil && f.Location != nil {
// Replace location data.
f.Remediation.Text = strings.Replace(f.Remediation.Text,
"${{ finding.location.path }}", f.Location.Path, -1)
f.Remediation.Markdown = strings.Replace(f.Remediation.Markdown,
"${{ finding.location.path }}", f.Location.Path, -1)
}
return f
}
// WithValues sets the values to an existing finding.
// No copy is made.
func (f *Finding) WithValues(values map[string]string) *Finding {
f.Values = values
return f
}
// WithPatch adds a patch to an existing finding.
// No copy is made.
func (f *Finding) WithPatch(patch *string) *Finding {
f.Remediation.Patch = patch
// NOTE: we will update the remediation section
// using patch information, e.g. ${{ patch.content }}.
return f
}
// WithOutcome adds an outcome to an existing finding.
// No copy is made.
// WARNING: this function should be called at most once for a finding.
func (f *Finding) WithOutcome(o Outcome) *Finding {
f.Outcome = o
// Positive is not negative, remove the remediation.
if o != OutcomeNegative {
f.Remediation = nil
}
return f
}
// WithRemediationMetadata adds remediation metadata to an existing finding.
// No copy is made.
func (f *Finding) WithRemediationMetadata(values map[string]string) *Finding {
if f.Remediation != nil {
// Replace all dynamic values.
for k, v := range values {
// Replace metadata.
f.Remediation.Text = strings.Replace(f.Remediation.Text,
fmt.Sprintf("${{ metadata.%s }}", k), v, -1)
f.Remediation.Markdown = strings.Replace(f.Remediation.Markdown,
fmt.Sprintf("${{ metadata.%s }}", k), v, -1)
}
}
return f
}
// WithValue adds a value to f.Values.
// No copy is made.
func (f *Finding) WithValue(k, v string) *Finding {
if f.Values == nil {
f.Values = make(map[string]string)
}
f.Values[k] = v
return f
}
// UnmarshalYAML is a custom unmarshalling function
// to transform the string into an enum.
func (o *Outcome) UnmarshalYAML(n *yaml.Node) error {
var str string
if err := n.Decode(&str); err != nil {
return fmt.Errorf("decode: %w", err)
}
switch n.Value {
case "Negative":
*o = OutcomeNegative
case "Positive":
*o = OutcomePositive
case "NotAvailable":
*o = OutcomeNotAvailable
case "NotSupported":
*o = OutcomeNotSupported
case "NotApplicable":
*o = OutcomeNotApplicable
case "Error":
*o = OutcomeError
default:
return fmt.Errorf("%w: %q", errInvalid, str)
}
return nil
}