mirror of
https://github.com/safedep/vet.git
synced 2025-12-10 00:22:08 -06:00
328 lines
9.3 KiB
Go
328 lines
9.3 KiB
Go
package reporter
|
|
|
|
import (
|
|
"os"
|
|
"slices"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/safedep/dry/utils"
|
|
jsonreportspec "github.com/safedep/vet/gen/jsonreport"
|
|
schema "github.com/safedep/vet/gen/jsonreport"
|
|
modelspec "github.com/safedep/vet/gen/models"
|
|
"github.com/safedep/vet/gen/violations"
|
|
"github.com/safedep/vet/pkg/analyzer"
|
|
"github.com/safedep/vet/pkg/common/logger"
|
|
"github.com/safedep/vet/pkg/models"
|
|
"github.com/safedep/vet/pkg/policy"
|
|
"github.com/safedep/vet/pkg/readers"
|
|
"github.com/safedep/vet/pkg/remediations"
|
|
"github.com/safedep/vet/pkg/schemamapper"
|
|
)
|
|
|
|
type JsonReportingConfig struct {
|
|
Path string
|
|
Tool ToolMetadata
|
|
}
|
|
|
|
// Json reporter is built on top of summary reporter to
|
|
// provide extended visibility
|
|
type jsonReportGenerator struct {
|
|
config JsonReportingConfig
|
|
remediations remediations.RemediationGenerator
|
|
|
|
manifests map[string]*jsonreportspec.PackageManifestReport
|
|
packages map[string]*jsonreportspec.PackageReport
|
|
}
|
|
|
|
func NewJsonReportGenerator(config JsonReportingConfig) (Reporter, error) {
|
|
return &jsonReportGenerator{
|
|
config: config,
|
|
remediations: remediations.NewStaticRemediationGenerator(),
|
|
manifests: make(map[string]*schema.PackageManifestReport),
|
|
packages: make(map[string]*schema.PackageReport),
|
|
}, nil
|
|
}
|
|
|
|
func (r *jsonReportGenerator) Name() string {
|
|
return "JSON Report Generator"
|
|
}
|
|
|
|
func (r *jsonReportGenerator) AddManifest(manifest *models.PackageManifest) {
|
|
// Eager load the package manifest in the cache
|
|
_ = r.findPackageManifestReport(manifest)
|
|
|
|
err := readers.NewManifestModelReader(manifest).EnumPackages(func(p *models.Package) error {
|
|
// Eager load the package in the cache
|
|
_ = r.findPackageReport(p)
|
|
|
|
if !slices.Contains(r.packages[p.Id()].Manifests, manifest.Id()) {
|
|
r.packages[p.Id()].Manifests = append(r.packages[p.Id()].Manifests, manifest.Id())
|
|
}
|
|
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
logger.Warnf("Failed to enumerate manifest packages: %v", err)
|
|
}
|
|
}
|
|
|
|
func (r *jsonReportGenerator) AddAnalyzerEvent(event *analyzer.AnalyzerEvent) {
|
|
if event.IsFilterMatch() {
|
|
r.handleFilterEvent(event)
|
|
} else if event.IsLockfilePoisoningSignal() {
|
|
r.handleThreatEvent(event)
|
|
}
|
|
}
|
|
|
|
func (r *jsonReportGenerator) handleThreatEvent(event *analyzer.AnalyzerEvent) {
|
|
if event.Threat == nil {
|
|
return
|
|
}
|
|
|
|
if event.Threat.SubjectType == jsonreportspec.ReportThreat_Manifest && event.Manifest == nil {
|
|
return
|
|
}
|
|
|
|
if event.Threat.SubjectType == jsonreportspec.ReportThreat_Package && event.Package == nil {
|
|
return
|
|
}
|
|
|
|
switch event.Threat.SubjectType {
|
|
case jsonreportspec.ReportThreat_Manifest:
|
|
manifest := r.findPackageManifestReport(event.Manifest)
|
|
manifest.Threats = append(manifest.Threats, event.Threat)
|
|
|
|
case jsonreportspec.ReportThreat_Package:
|
|
pkg := r.findPackageReport(event.Package)
|
|
pkg.Threats = append(pkg.Threats, event.Threat)
|
|
}
|
|
}
|
|
|
|
func (r *jsonReportGenerator) handleFilterEvent(event *analyzer.AnalyzerEvent) {
|
|
if event.Package == nil {
|
|
logger.Warnf("Analyzer event with nil package")
|
|
return
|
|
}
|
|
|
|
if event.Package.Manifest == nil {
|
|
logger.Warnf("Analyzer event with nil package manifest")
|
|
return
|
|
}
|
|
|
|
if event.Filter == nil {
|
|
logger.Warnf("Analyzer event that matched filter but without Filter object")
|
|
return
|
|
}
|
|
|
|
// All subsequent operations are on this pkg
|
|
pkg := r.findPackageReport(event.Package)
|
|
|
|
// We avoid duplicate violation for a package. Duplicates can occur because same package
|
|
// is in multiple manifests hence raising same violation
|
|
v := utils.FindAnyWith(pkg.Violations, func(item **violations.Violation) bool {
|
|
return ((*item).GetFilter().GetName() == event.Filter.GetName())
|
|
})
|
|
if v != nil {
|
|
return
|
|
}
|
|
|
|
// Fall through here to associate a Violation and a RemediationAdvice
|
|
violation := &violations.Violation{
|
|
CheckType: event.Filter.GetCheckType(),
|
|
Filter: event.Filter,
|
|
}
|
|
|
|
pkg.Violations = append(pkg.Violations, violation)
|
|
|
|
advice, err := r.remediations.Advice(event.Package, violation)
|
|
if err != nil {
|
|
logger.Warnf("Failed to generate remediation for %s due to %v",
|
|
event.Package.ShortName(), err)
|
|
} else {
|
|
pkg.Advices = append(pkg.Advices, advice)
|
|
}
|
|
}
|
|
|
|
func (r *jsonReportGenerator) findPackageManifestReport(manifest *models.PackageManifest) *jsonreportspec.PackageManifestReport {
|
|
manifestId := manifest.Id()
|
|
if _, ok := r.manifests[manifestId]; !ok {
|
|
r.manifests[manifestId] = &jsonreportspec.PackageManifestReport{
|
|
Id: manifestId,
|
|
SourceType: string(manifest.GetSource().GetType()),
|
|
Namespace: manifest.GetSource().GetNamespace(),
|
|
Path: manifest.GetSource().GetPath(),
|
|
DisplayPath: manifest.GetDisplayPath(),
|
|
Ecosystem: manifest.GetSpecEcosystem(),
|
|
Threats: make([]*schema.ReportThreat, 0),
|
|
}
|
|
}
|
|
|
|
return r.manifests[manifestId]
|
|
}
|
|
|
|
func (r *jsonReportGenerator) findPackageReport(pkg *models.Package) *jsonreportspec.PackageReport {
|
|
pkgId := pkg.Id()
|
|
if _, ok := r.packages[pkgId]; !ok {
|
|
r.packages[pkgId] = r.buildJsonPackageReportFromPackage(pkg)
|
|
}
|
|
|
|
return r.packages[pkgId]
|
|
}
|
|
|
|
func (r *jsonReportGenerator) AddPolicyEvent(event *policy.PolicyEvent) {}
|
|
|
|
func (r *jsonReportGenerator) Finish() error {
|
|
logger.Infof("Generating consolidated Json report: %s", r.config.Path)
|
|
|
|
report, err := r.buildSpecReport()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
b, err := utils.ToPbJson(report, "")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
file, err := os.Create(r.config.Path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
defer file.Close()
|
|
_, err = file.WriteString(b)
|
|
return err
|
|
}
|
|
|
|
func (r *jsonReportGenerator) buildSpecReport() (*schema.Report, error) {
|
|
report := schema.Report{
|
|
Meta: &schema.ReportMeta{
|
|
ToolName: r.config.Tool.Name,
|
|
ToolVersion: r.config.Tool.Version,
|
|
CreatedAt: time.Now().UTC().Format(time.RFC3339),
|
|
},
|
|
Packages: make([]*schema.PackageReport, 0),
|
|
Manifests: make([]*schema.PackageManifestReport, 0),
|
|
}
|
|
|
|
for _, pm := range r.manifests {
|
|
report.Manifests = append(report.Manifests, pm)
|
|
}
|
|
|
|
for _, p := range r.packages {
|
|
report.Packages = append(report.Packages, p)
|
|
}
|
|
|
|
return &report, nil
|
|
}
|
|
|
|
func (j *jsonReportGenerator) buildJsonPackageReportFromPackage(p *models.Package) *jsonreportspec.PackageReport {
|
|
pkg := &jsonreportspec.PackageReport{
|
|
Package: &modelspec.Package{
|
|
Ecosystem: p.GetSpecEcosystem(),
|
|
Name: p.GetName(),
|
|
Version: p.GetVersion(),
|
|
},
|
|
Violations: make([]*violations.Violation, 0),
|
|
Advices: make([]*schema.RemediationAdvice, 0),
|
|
Vulnerabilities: make([]*modelspec.InsightVulnerability, 0),
|
|
Licenses: make([]*modelspec.InsightLicenseInfo, 0),
|
|
Projects: make([]*modelspec.InsightProjectInfo, 0),
|
|
Threats: make([]*schema.ReportThreat, 0),
|
|
}
|
|
|
|
insights := utils.SafelyGetValue(p.Insights)
|
|
vulns := utils.SafelyGetValue(insights.Vulnerabilities)
|
|
licenses := utils.SafelyGetValue(insights.Licenses)
|
|
projects := utils.SafelyGetValue(insights.Projects)
|
|
|
|
for _, vuln := range vulns {
|
|
insightSeverities := utils.SafelyGetValue(vuln.Severities)
|
|
severties := []*modelspec.InsightVulnerabilitySeverity{}
|
|
|
|
for _, sev := range insightSeverities {
|
|
mappedSeverity, err := schemamapper.InsightsVulnerabilitySeverityToModelSeverity(&schemamapper.InsightsVulnerabilitySeverity{
|
|
Type: sev.Type,
|
|
Risk: sev.Risk,
|
|
Score: sev.Score,
|
|
})
|
|
if err != nil {
|
|
logger.Errorf("Failed to convert InsightAPI schema to model spec: %v", err)
|
|
continue
|
|
}
|
|
|
|
severties = append(severties, mappedSeverity)
|
|
}
|
|
|
|
pkg.Vulnerabilities = append(pkg.Vulnerabilities, &modelspec.InsightVulnerability{
|
|
Id: utils.SafelyGetValue(vuln.Id),
|
|
Title: utils.SafelyGetValue(vuln.Summary),
|
|
Aliases: utils.SafelyGetValue(vuln.Aliases),
|
|
Severities: severties,
|
|
})
|
|
|
|
}
|
|
|
|
for _, license := range licenses {
|
|
pkg.Licenses = append(pkg.Licenses, &modelspec.InsightLicenseInfo{
|
|
Id: string(license),
|
|
})
|
|
}
|
|
|
|
// Re-usable function to get project name and url from scorecard
|
|
// when projects are not available in insights
|
|
getProjectFromScorecard := func() (string, string) {
|
|
scorecard := utils.SafelyGetValue(insights.Scorecard)
|
|
content := utils.SafelyGetValue(scorecard.Content)
|
|
repository := utils.SafelyGetValue(content.Repository)
|
|
|
|
projectUrl := utils.SafelyGetValue(repository.Name)
|
|
projectName := ""
|
|
|
|
parts := strings.SplitN(projectUrl, "/", 2)
|
|
if len(parts) == 2 {
|
|
projectName = parts[1]
|
|
}
|
|
|
|
if projectUrl != "" && !strings.HasPrefix(projectUrl, "http") {
|
|
projectUrl = "https://" + projectUrl
|
|
}
|
|
|
|
return projectName, projectUrl
|
|
}
|
|
|
|
for _, project := range projects {
|
|
stars := utils.SafelyGetValue(project.Stars)
|
|
projectUrl := utils.SafelyGetValue(project.Link)
|
|
|
|
pkg.Projects = append(pkg.Projects, &modelspec.InsightProjectInfo{
|
|
Name: utils.SafelyGetValue(project.Name),
|
|
Stars: int32(stars),
|
|
Url: projectUrl,
|
|
})
|
|
}
|
|
|
|
// Project Url can be empty because we use custom data source
|
|
// for RubyGems. We should copy from scorecard
|
|
if len(projects) == 0 {
|
|
projectName, projectUrl := getProjectFromScorecard()
|
|
|
|
if projectUrl != "" {
|
|
pkg.Projects = append(pkg.Projects, &modelspec.InsightProjectInfo{
|
|
Name: projectName,
|
|
Url: projectUrl,
|
|
})
|
|
}
|
|
}
|
|
|
|
if len(pkg.Vulnerabilities) > 0 {
|
|
pkg.Advices = append(pkg.Advices, &schema.RemediationAdvice{
|
|
Type: schema.RemediationAdviceType_UpgradePackage,
|
|
TargetAlternatePackageVersion: utils.SafelyGetValue(insights.PackageCurrentVersion),
|
|
})
|
|
}
|
|
|
|
return pkg
|
|
}
|