mirror of
https://github.com/lukaszraczylo/gohoarder.git
synced 2026-06-08 23:09:33 +00:00
195 lines
5.5 KiB
Go
195 lines
5.5 KiB
Go
package govulncheck
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/lukaszraczylo/gohoarder/pkg/config"
|
|
"github.com/lukaszraczylo/gohoarder/pkg/metadata"
|
|
"github.com/lukaszraczylo/gohoarder/pkg/uuid"
|
|
"github.com/rs/zerolog/log"
|
|
)
|
|
|
|
// ScannerName is the name of this scanner
|
|
const ScannerName = "govulncheck"
|
|
|
|
// Scanner implements the govulncheck vulnerability scanner for Go modules
|
|
type Scanner struct {
|
|
config config.GovulncheckConfig
|
|
}
|
|
|
|
// New creates a new govulncheck scanner
|
|
func New(cfg config.GovulncheckConfig) *Scanner {
|
|
return &Scanner{
|
|
config: cfg,
|
|
}
|
|
}
|
|
|
|
// Name returns the scanner name
|
|
func (s *Scanner) Name() string {
|
|
return ScannerName
|
|
}
|
|
|
|
// Scan scans a Go module using govulncheck
|
|
func (s *Scanner) Scan(ctx context.Context, registry, packageName, version string, filePath string) (*metadata.ScanResult, error) {
|
|
// Only scan Go packages
|
|
if registry != "go" {
|
|
return &metadata.ScanResult{
|
|
ID: uuid.New().String(),
|
|
Registry: registry,
|
|
PackageName: packageName,
|
|
PackageVersion: version,
|
|
Scanner: ScannerName,
|
|
ScannedAt: time.Now(),
|
|
Status: metadata.ScanStatusClean,
|
|
VulnerabilityCount: 0,
|
|
Vulnerabilities: []metadata.Vulnerability{},
|
|
Details: map[string]interface{}{
|
|
"skipped": "govulncheck only supports Go modules",
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
log.Info().
|
|
Str("scanner", ScannerName).
|
|
Str("package", packageName).
|
|
Str("version", version).
|
|
Msg("Starting govulncheck scan")
|
|
|
|
// Create a temporary directory for extraction
|
|
tmpDir, err := os.MkdirTemp("", "govulncheck-*")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create temp dir: %w", err)
|
|
}
|
|
defer os.RemoveAll(tmpDir)
|
|
|
|
// Extract the .zip file
|
|
if err := s.extractZip(filePath, tmpDir); err != nil {
|
|
return nil, fmt.Errorf("failed to extract zip: %w", err)
|
|
}
|
|
|
|
// Run govulncheck
|
|
cmd := exec.CommandContext(ctx, "govulncheck", "-json", "-mode=binary", tmpDir) // #nosec G204 -- govulncheck command with temp directory
|
|
output, _ := cmd.CombinedOutput()
|
|
|
|
// govulncheck returns non-zero when vulnerabilities are found
|
|
// Parse output regardless of error
|
|
var vulns []GovulncheckVuln
|
|
if len(output) > 0 {
|
|
// Parse line-delimited JSON
|
|
lines := strings.Split(string(output), "\n")
|
|
for _, line := range lines {
|
|
if strings.TrimSpace(line) == "" {
|
|
continue
|
|
}
|
|
var entry GovulncheckEntry
|
|
if err := json.Unmarshal([]byte(line), &entry); err != nil {
|
|
log.Warn().Err(err).Str("line", line).Msg("Failed to parse govulncheck line")
|
|
continue
|
|
}
|
|
if entry.Finding != nil && entry.Finding.OSV != "" {
|
|
vulns = append(vulns, GovulncheckVuln{
|
|
OSV: entry.Finding.OSV,
|
|
FixedVersion: entry.Finding.FixedVersion,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
// Convert to our format
|
|
result := s.convertResult(vulns, registry, packageName, version)
|
|
|
|
log.Info().
|
|
Str("scanner", ScannerName).
|
|
Str("package", packageName).
|
|
Int("vulnerabilities", result.VulnerabilityCount).
|
|
Msg("govulncheck scan completed")
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// Health checks if govulncheck is available
|
|
func (s *Scanner) Health(ctx context.Context) error {
|
|
cmd := exec.CommandContext(ctx, "govulncheck", "-version")
|
|
if err := cmd.Run(); err != nil {
|
|
return fmt.Errorf("govulncheck not available: %w (install with: go install golang.org/x/vuln/cmd/govulncheck@latest)", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// extractZip extracts a zip file to destination
|
|
func (s *Scanner) extractZip(zipPath, destDir string) error {
|
|
cmd := exec.Command("unzip", "-q", zipPath, "-d", destDir)
|
|
return cmd.Run()
|
|
}
|
|
|
|
// convertResult converts govulncheck findings to our ScanResult format
|
|
func (s *Scanner) convertResult(vulns []GovulncheckVuln, registry, packageName, version string) *metadata.ScanResult {
|
|
vulnerabilities := make([]metadata.Vulnerability, 0)
|
|
severityCounts := make(map[string]int)
|
|
seen := make(map[string]bool)
|
|
|
|
for _, vuln := range vulns {
|
|
// Deduplicate by OSV ID
|
|
if seen[vuln.OSV] {
|
|
continue
|
|
}
|
|
seen[vuln.OSV] = true
|
|
|
|
// govulncheck doesn't provide severity in output
|
|
// Default to HIGH for found vulnerabilities
|
|
severity := metadata.NormalizeSeverity("HIGH")
|
|
severityCounts[severity]++
|
|
|
|
vulnerabilities = append(vulnerabilities, metadata.Vulnerability{
|
|
ID: vuln.OSV,
|
|
Severity: severity,
|
|
Title: vuln.OSV,
|
|
Description: fmt.Sprintf("Vulnerability %s found by govulncheck", vuln.OSV),
|
|
References: []string{fmt.Sprintf("https://pkg.go.dev/vuln/%s", vuln.OSV)},
|
|
FixedIn: vuln.FixedVersion,
|
|
})
|
|
}
|
|
|
|
status := metadata.ScanStatusClean
|
|
if len(vulnerabilities) > 0 {
|
|
status = metadata.ScanStatusVulnerable
|
|
}
|
|
|
|
return &metadata.ScanResult{
|
|
ID: uuid.New().String(),
|
|
Registry: registry,
|
|
PackageName: packageName,
|
|
PackageVersion: version,
|
|
Scanner: ScannerName,
|
|
ScannedAt: time.Now(),
|
|
Status: status,
|
|
VulnerabilityCount: len(vulnerabilities),
|
|
Vulnerabilities: vulnerabilities,
|
|
Details: map[string]interface{}{
|
|
"severity_counts": severityCounts,
|
|
"note": "govulncheck provides reachability analysis for Go modules",
|
|
},
|
|
}
|
|
}
|
|
|
|
// GovulncheckEntry represents a single line of govulncheck JSON output
|
|
type GovulncheckEntry struct {
|
|
Finding *GovulncheckFinding `json:"finding,omitempty"`
|
|
}
|
|
|
|
type GovulncheckFinding struct {
|
|
OSV string `json:"osv"`
|
|
FixedVersion string `json:"fixed_version,omitempty"`
|
|
}
|
|
|
|
type GovulncheckVuln struct {
|
|
OSV string
|
|
FixedVersion string
|
|
}
|