Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: new command "scan-source" #555

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pkg/cli/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ func New() *cobra.Command {
cmdLs(),
cmdSBOM(),
cmdScan(),
cmdScanSource(),
cmdUpdate(),
cmdVEX(),
cmdWithdraw(),
Expand Down
66 changes: 66 additions & 0 deletions pkg/cli/scan-source.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package cli

import (
"fmt"

"github.com/spf13/cobra"
"github.com/wolfi-dev/wolfictl/pkg/configs/build"
rwos "github.com/wolfi-dev/wolfictl/pkg/configs/rwfs/os"
"github.com/wolfi-dev/wolfictl/pkg/scan"
)

func cmdScanSource() *cobra.Command {
p := &scanSourceParams{}
cmd := &cobra.Command{
Use: "scan-source",
Short: "Scan a package's source code for vulnerabilities",
Args: cobra.MinimumNArgs(1),
SilenceErrors: true,
RunE: func(cmd *cobra.Command, args []string) error {
logger := newLogger(p.verbosity)

distroDir := p.distroDir
if distroDir == "" {
distroDir = "."
}

fsys := rwos.DirFS(distroDir)
index, err := build.NewIndex(fsys)
if err != nil {
return fmt.Errorf("failed to create index of package configurations: %w", err)
}

first, err := index.Select().WhereName(args[0]).First()
if err != nil {
return fmt.Errorf("failed to find configuration for package %q: %w", args[0], err)
}
cfg := first.Configuration()

logger.Info("scanning source code used in melange configuration", "package", cfg.Name())

scanResults, err := scan.Sources(cmd.Context(), logger, cfg)
if err != nil {
return fmt.Errorf("failed to scan sources for %q: %w", cfg.Name(), err)
}

for _, r := range scanResults {
fmt.Print(r)
}

return nil
},
}

p.addFlagsTo(cmd)
return cmd
}

type scanSourceParams struct {
distroDir string
verbosity int
}

func (p *scanSourceParams) addFlagsTo(cmd *cobra.Command) {
addDistroDirFlag(&p.distroDir, cmd)
addVerboseFlag(&p.verbosity, cmd)
}
59 changes: 59 additions & 0 deletions pkg/git/git.go
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,65 @@ func TempClone(gitURL, hash string, useAuth bool) (repoDir string, err error) {
return dir, nil
}

// TempCloneTag is like TempClone, but clones the repo at the provided tag.
func TempCloneTag(gitURL, tag string, useAuth bool) (repoDir string, err error) {
if tag == "" {
return "", fmt.Errorf("tag must be provided")
}

dir, err := os.MkdirTemp("", "wolfictl-git-clone-*")
if err != nil {
return dir, fmt.Errorf("unable to create temp directory for git clone: %w", err)
}

var auth transport.AuthMethod
if useAuth {
auth = GetGitAuth()
}

repo, err := git.PlainClone(dir, false, &git.CloneOptions{
Auth: auth,
URL: gitURL,
})
if err != nil {
return dir, fmt.Errorf("unable to clone repo %q to temp directory: %w", gitURL, err)
}

tags, err := repo.Tags()
if err != nil {
return "", fmt.Errorf("failed to get tags: %w", err)
}

var tagRef *plumbing.Reference
err = tags.ForEach(func(ref *plumbing.Reference) error {
if ref.Name().Short() == tag { // replace with your tag
tagRef = ref
return storer.ErrStop
}
return nil
})
if err != nil {
return "", fmt.Errorf("unable to find tag %q for repo %q: %w", tag, gitURL, err)
}

if tagRef == nil {
return "", fmt.Errorf("tag %q not found", tag)
}

w, err := repo.Worktree()
if err != nil {
return "", fmt.Errorf("unable to get worktree for repo %q: %w", gitURL, err)
}
err = w.Checkout(&git.CheckoutOptions{
Branch: tagRef.Name(),
})
if err != nil {
return "", fmt.Errorf("unable to checkout tag %q for repo %q: %w", tag, gitURL, err)
}

return dir, nil
}

// FindForkPoint finds the fork point between the local branch and the upstream
// branch.
//
Expand Down
57 changes: 57 additions & 0 deletions pkg/scan/govulncheck.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ import (
"net/http"
"time"

"golang.org/x/tools/go/packages"
"golang.org/x/vuln/pkg/client"
"golang.org/x/vuln/pkg/govulncheck"
"golang.org/x/vuln/pkg/osv"
vulnscan "golang.org/x/vuln/pkg/scan"
"golang.org/x/vuln/pkg/vulncheck"
)
Expand Down Expand Up @@ -41,6 +43,61 @@ func runGovulncheck(ctx context.Context, exe io.ReaderAt) (*vulncheck.Result, er
return result, nil
}

func runGovulncheckSource(ctx context.Context, dir string) (*vulncheck.Result, error) {
c, err := client.NewClient(govulncheckDB, nil)
if err != nil {
return nil, fmt.Errorf("creating DB client: %w", err)
}

cfg := &govulncheck.Config{
ScanLevel: "symbol",
}

patterns := []string{"./..."}

graph := vulncheck.NewPackageGraph("")
pkgConfig := &packages.Config{
Dir: dir,
Tests: false, // Don't scan tests. We can revisit this later if needed.
Env: nil, // TODO: This will use the current environment, but we should probably make sure to use the build environment.
}

var pkgs []*packages.Package
pkgs, err = graph.LoadPackages(pkgConfig, nil, patterns)
if err != nil {
return nil, fmt.Errorf("govulncheck: loading packages: %w", err)
}

result, err := vulncheck.Source(ctx, nopVulncheckHandler{}, pkgs, cfg, c, graph)
if err != nil {
return nil, err
}

result.Vulns = vulnscan.UniqueVulns(result.Vulns)

return result, nil
}

var _ govulncheck.Handler = (*nopVulncheckHandler)(nil)

type nopVulncheckHandler struct{}

func (g nopVulncheckHandler) Config(_ *govulncheck.Config) error {
return nil
}

func (g nopVulncheckHandler) Progress(_ *govulncheck.Progress) error {
return nil
}

func (g nopVulncheckHandler) OSV(_ *osv.Entry) error {
return nil
}

func (g nopVulncheckHandler) Finding(_ *govulncheck.Finding) error {
return nil
}

type GoVulnDBIndex struct {
index map[string]GoVulnDBIndexEntry
}
Expand Down
131 changes: 131 additions & 0 deletions pkg/scan/source.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package scan

import (
"context"
"fmt"
"log/slog"
"os"
"strings"

"chainguard.dev/melange/pkg/config"
"github.com/wolfi-dev/wolfictl/pkg/git"
"golang.org/x/vuln/pkg/vulncheck"
)

func Sources(ctx context.Context, logger *slog.Logger, cfg *config.Configuration) ([]SourceResult, error) {
// TODO: handle "fetch" pipelines

const gitCheckoutPipelineName = "git-checkout"

var targets []gitCloneTarget
for i := range cfg.Pipeline {
step := cfg.Pipeline[i]

if step.Uses != gitCheckoutPipelineName {
continue
}

// TODO: Ideally there's an elegant way to do this by leaning on Melange's code,
// but that code is pretty messy at the moment. I would've expected these
// substitutions that don't depend on runtime information to be done at parse
// time, but they're not.
step.With["tag"] = strings.ReplaceAll(step.With["tag"], config.SubstitutionPackageVersion, cfg.Package.Version)

t := gitCloneTarget{
url: step.With["repository"],
tag: step.With["tag"],
}
targets = append(targets, t)

logger.Debug(
"found git checkout step",
"stepIndex",
i,
"repository",
t.url,
"tag",
t.tag,
)
}

logger.Debug("finished finding git checkout steps", "total", len(targets))

var results []SourceResult
for _, t := range targets {
r, err := t.cloneAndRunGovulncheckSourceScan(ctx, logger)
if err != nil {
return nil, err
}

results = append(results, SourceResult{
Name: cfg.Name(),
GitRepository: t,
VulncheckResult: r,
})
}

return results, nil
}

type gitCloneTarget struct {
url string
tag string
}

func (t gitCloneTarget) cloneAndRunGovulncheckSourceScan(ctx context.Context, logger *slog.Logger) (*vulncheck.Result, error) {
logger.Debug("beginning git clone", "url", t.url, "tag", t.tag)
tempDir, err := git.TempCloneTag(t.url, t.tag, false)
defer os.RemoveAll(tempDir)
if err != nil {
return nil, fmt.Errorf("unable to clone repo %q: %w", t.url, err)
}
logger.Debug("finished git clone", "url", t.url, "tag", t.tag)

logger.Debug("beginning govulncheck source scan", "repo", t.url, "tag", t.tag, "tempDir", tempDir)
result, err := runGovulncheckSource(ctx, tempDir)
if err != nil {
return nil, fmt.Errorf("unable to run govulncheck on %q: %w", t.url, err)
}
logger.Debug("finished govulncheck source scan", "target", t)

return result, nil
}

// SourceResult is the result of scanning a package's source code for
// vulnerabilities using govulncheck.
type SourceResult struct {
// The Name of the package described in the Melange configuration.
Name string

// GitRepository describes the repository that was scanned.
GitRepository gitCloneTarget

// VulncheckResult is the result of running govulncheck on the repository.
VulncheckResult *vulncheck.Result
}

// String returns a human-readable representation of the result.
func (r SourceResult) String() string {
s := fmt.Sprintf(
"%s %s %s\n",
r.Name,
r.GitRepository.url,
r.GitRepository.tag,
)

if len(r.VulncheckResult.Vulns) == 0 {
s += " No vulnerabilities found\n"
return s
}

for _, v := range r.VulncheckResult.Vulns {
s += fmt.Sprintf(
" %s %s %s\n",
v.OSV.ID,
v.ImportSink.String(),
v.Symbol,
)
}

return s
}