blob: 6c52a0c2d09a8d121b1f3a69b12e6d7362fdfb9a [file] [log] [blame]
package report
import (
"errors"
"fmt"
"regexp"
"golang.org/x/mod/module"
"golang.org/x/mod/semver"
)
type VersionRange struct {
Introduced string
Fixed string
}
type Report struct {
Package string
// TODO: could also be GoToolchain, but we might want
// this for other things?
//
// could we also automate this by just looking for
// things prefixed with cmd/go?
DoNotExport bool `json:"do_not_export"`
// TODO: how does this interact with Versions etc?
Stdlib bool `json:"stdlib"`
// TODO: the most common usage of additional package should
// really be replaced with 'aliases', we'll still need
// additional packages for some cases, but it's too heavy
// for most
AdditionalPackages []struct {
Package string
Symbols []string
Versions []VersionRange
} `toml:"additional_packages"`
Versions []VersionRange
Description string
Severity string
CVE string
Credit string
Symbols []string
OS []string
Arch []string
Links struct {
PR string
Commit string
Context []string
}
CVEMetadata *struct {
ID string
CWE string
Description string
} `toml:"cve_metadata"`
}
var cveRegex = regexp.MustCompile(`^CVE-\d{4}-\d{4,}$`)
func (vuln *Report) Lint() error {
if vuln.Package == "" {
return errors.New("missing package")
}
if err := module.CheckImportPath(vuln.Package); err != nil {
return err
}
for _, additionalPackage := range vuln.AdditionalPackages {
if err := module.CheckImportPath(additionalPackage.Package); err != nil {
return err
}
}
for _, version := range vuln.Versions {
if version.Introduced != "" {
if !semver.IsValid(version.Introduced) {
return fmt.Errorf("bad version.introduced")
}
if err := module.Check(vuln.Package, version.Introduced); err != nil {
return err
}
}
if version.Fixed != "" {
if !semver.IsValid(version.Fixed) {
return fmt.Errorf("bad version.fixed")
}
if err := module.Check(vuln.Package, version.Fixed); err != nil {
return err
}
}
}
if vuln.Description == "" {
return errors.New("missing description")
}
sevs := map[string]bool{
"low": true,
"medium": true,
"high": true,
"critical": true,
}
// Could also just default to medium if not provided?
// Need to document what the default case is and what factors lower
// or raise the sev
if vuln.Severity != "" && !sevs[vuln.Severity] {
return fmt.Errorf("unknown severity %q", vuln.Severity)
}
if vuln.CVE != "" && vuln.CVEMetadata.ID != "" {
// TODO: may just want to use one of these? :shrug:
return errors.New("only one of cve and cve_metadata.id should be present")
}
if vuln.CVE != "" && !cveRegex.MatchString(vuln.CVE) {
return fmt.Errorf("malformed CVE number: %s", vuln.CVE)
}
return nil
}