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

Add flyctl support for scanning images with scantron #3725

Merged
merged 28 commits into from
Jul 19, 2024
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
0da471b
Add flyctl support for scanning images with scantron
timflyio Jul 10, 2024
88da1dc
go fmt
timflyio Jul 10, 2024
d85dd8d
support vulnerability filtering
timflyio Jul 10, 2024
5ffdd96
sort results before printing
timflyio Jul 11, 2024
f067ae1
support reporting raw json scan results
timflyio Jul 11, 2024
5071d44
explore implementation for a summary scan across apps in an org
timflyio Jul 12, 2024
89d71f7
reorg and cleanup, and support for vulns/sbom scan by image path.
timflyio Jul 12, 2024
154ffa1
rename scan as registry
timflyio Jul 12, 2024
dad8cfd
cleanup command descriptions
timflyio Jul 12, 2024
721dd8d
Merge branch 'master' into tim-scantron
timflyio Jul 12, 2024
1bcb728
cleanup deepsrc recommendations
timflyio Jul 12, 2024
dfc16b4
remove unnecessasry placeholder arg
timflyio Jul 12, 2024
9fdd347
address some comments
timflyio Jul 17, 2024
106d6ff
use a single org token and provide spinners
timflyio Jul 17, 2024
355f899
concurrent image lookup and refactoring of args processing
timflyio Jul 18, 2024
7df90fc
remove need for looking up each AppCompact
timflyio Jul 18, 2024
42b832e
gofmt
timflyio Jul 18, 2024
26f92db
forgot to commit flapsutil changes earlier.
timflyio Jul 18, 2024
893dfa3
fix lint
timflyio Jul 18, 2024
e15fac3
use registry_token and throw out attenuation which is no longer needed
timflyio Jul 18, 2024
38561e1
hide the experimental registry command from help
timflyio Jul 18, 2024
ce07204
hide the registry command a cleaner way
timflyio Jul 18, 2024
568e48d
better error handling when fetching scan results. continue summary ev…
timflyio Jul 18, 2024
d8d2599
fix error message when using a bogus org name
timflyio Jul 18, 2024
6855fc0
mark commands as experimental
timflyio Jul 18, 2024
fc4af8c
show skipped scans in sorted order
timflyio Jul 18, 2024
2225906
remove TODO comments
timflyio Jul 19, 2024
dbf5608
Merge branch 'master' into tim-scantron
timflyio Jul 19, 2024
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
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ require (
github.com/superfly/fly-go v0.1.19-0.20240702095246-59db1fe4ffe8
github.com/superfly/graphql v0.2.4
github.com/superfly/lfsc-go v0.1.1
github.com/superfly/macaroon v0.2.13
github.com/superfly/macaroon v0.2.14-0.20240702184853-b8ac52a1fc77
github.com/superfly/tokenizer v0.0.2
github.com/vektah/gqlparser v1.3.1
github.com/vektah/gqlparser/v2 v2.5.16
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -615,8 +615,8 @@ github.com/superfly/lfsc-go v0.1.1 h1:dGjLgt81D09cG+aR9lJZIdmonjZSR5zYCi7s54+ZU2
github.com/superfly/lfsc-go v0.1.1/go.mod h1:zVb0VENz/Il8Nmvvd4XAsX2bWhQ+sr0nK8vv9PeezcE=
github.com/superfly/ltx v0.3.12 h1:Z7z1sc4g34/jUi3XO84+zBlIsbaoh2RJ3b4zTQpBK/M=
github.com/superfly/ltx v0.3.12/go.mod h1:ly+Dq7UVacQVEI5/b0r6j+PSNy9ibwx1yikcWAaSkhE=
github.com/superfly/macaroon v0.2.13 h1:WEZnifapjW5yuCEsdZxqCq4X8xuzIf+AUVPv6lm7GtI=
github.com/superfly/macaroon v0.2.13/go.mod h1:Kt6/EdSYfFjR4GIe+erMwcJgU8iMu1noYVceQ5dNdKo=
github.com/superfly/macaroon v0.2.14-0.20240702184853-b8ac52a1fc77 h1:W5LHJ6jjIB8YxOzsA5ZfdvNHifUKN/mFLiMaCAaWyB8=
github.com/superfly/macaroon v0.2.14-0.20240702184853-b8ac52a1fc77/go.mod h1:Kt6/EdSYfFjR4GIe+erMwcJgU8iMu1noYVceQ5dNdKo=
github.com/superfly/tokenizer v0.0.2 h1:gBREpm08sPWUHsqKHKHFy/AIKwrqpg+VBD/wtJ6x5Jk=
github.com/superfly/tokenizer v0.0.2/go.mod h1:jO8bIWFsfcBghh7I6cFJHmJb5E3RQr0v4F0Rnev3Yj4=
github.com/tj/assert v0.0.0-20171129193455-018094318fb0/go.mod h1:mZ9/Rh9oLWpLLDRpvE+3b7gP/C2YyLFYxNmcLnPTMe0=
Expand Down
3 changes: 1 addition & 2 deletions internal/buildinfo/buildinfo.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"path/filepath"
"runtime"
"runtime/debug"
"strings"
"time"

"github.com/pkg/errors"
Expand Down Expand Up @@ -135,5 +134,5 @@ func Commit() string {
}

func UserAgent() string {
return strings.TrimSpace(fmt.Sprintf("fly-cli/%s", Version()))
return fmt.Sprintf("flyctl/%s", Version())
}
288 changes: 288 additions & 0 deletions internal/command/registry/args.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,288 @@
package registry

import (
"context"
"errors"
"fmt"
"slices"
"strings"
"sync"

"github.com/samber/lo"
"golang.org/x/sync/errgroup"

fly "github.com/superfly/fly-go"
"github.com/superfly/fly-go/flaps"
"github.com/superfly/flyctl/internal/appconfig"
"github.com/superfly/flyctl/internal/flag"
"github.com/superfly/flyctl/internal/flapsutil"
"github.com/superfly/flyctl/internal/flyutil"
"github.com/superfly/flyctl/internal/prompt"
"github.com/superfly/flyctl/internal/spinner"
"github.com/superfly/flyctl/iostreams"
)

type Unit struct{}

// ImgInfo carries image information for a machine.
type ImgInfo struct {
Org string
OrgID string
App string
Mach string
Path string
}

func (a ImgInfo) Compare(b ImgInfo) int {
if d := strings.Compare(a.Org, b.Org); d != 0 {
return d
}
if d := strings.Compare(a.OrgID, b.OrgID); d != 0 {
return d
}
if d := strings.Compare(a.App, b.App); d != 0 {
return d
}
if d := strings.Compare(a.Mach, b.Mach); d != 0 {
return d
}
if d := strings.Compare(a.Path, b.Path); d != 0 {
return d
}
return 0
}

// AugmentMap includes all of src into targ.
func AugmentMap[K comparable, V any](targ, src map[K]V) {
for k, v := range src {
targ[k] = v
}
}

// SortedKeys returns the keys in a map in sorted order.
// Could be made generic.
func SortedKeys(m map[ImgInfo]Unit) []ImgInfo {
keys := lo.Keys(m)
slices.SortFunc(keys, func(a, b ImgInfo) int { return a.Compare(b) })
return keys
}

// argsGetAppCompact returns the AppCompact for the selected app, using `app`.
func argsGetAppCompact(ctx context.Context) (*fly.AppCompact, error) {
appName := appconfig.NameFromContext(ctx)
apiClient := flyutil.ClientFromContext(ctx)
app, err := apiClient.GetAppCompact(ctx, appName)
if err != nil {
return nil, fmt.Errorf("failed to get app: %w", err)
}
return app, nil
}

func getFlapsClient(ctx context.Context, app *fly.AppCompact) (*flaps.Client, error) {
flapsClient, err := flapsutil.NewClientWithOptions(ctx, flaps.NewClientOpts{
AppCompact: app,
AppName: app.Name,
})
if err != nil {
return nil, fmt.Errorf("failed to create flaps client for app %s: %w", app.Name, err)
}
return flapsClient, nil
}

// argsGetMachine returns the selected machine, using `app`, `select` and `machine`.
func argsGetMachine(ctx context.Context, app *fly.AppCompact) (*fly.Machine, error) {
if flag.IsSpecified(ctx, "machine") {
if flag.IsSpecified(ctx, "select") {
return nil, errors.New("--machine can't be used with -s/--select")
}
return argsGetMachineByID(ctx, app)
}
return argsSelectMachine(ctx, app)
}

// argsSelectMachine lets the user select a machine if there are multiple machines and
// the user specified "-s". Otherwise it returns the first machine for an app.
// Using `select`.
func argsSelectMachine(ctx context.Context, app *fly.AppCompact) (*fly.Machine, error) {
anyMachine := !flag.GetBool(ctx, "select")

flapsClient, err := getFlapsClient(ctx, app)
if err != nil {
return nil, err
}

machines, err := flapsClient.ListActive(ctx)
if err != nil {
return nil, err
}

var options []string
for _, machine := range machines {
imgPath := imageRefPath(&machine.ImageRef)
options = append(options, fmt.Sprintf("%s: %s %s %s", machine.Region, machine.ID, machine.Name, imgPath))
}

if len(machines) == 0 {
return nil, fmt.Errorf("no machines found")
}

if anyMachine || len(machines) == 1 {
return machines[0], nil
}

index := 0
if err := prompt.Select(ctx, &index, "Select a machine:", "", options...); err != nil {
return nil, fmt.Errorf("failed to prompt for a machine: %w", err)
}
return machines[index], nil
}

// argsGetMachineByID returns an app's machine using the `machine` argument.
func argsGetMachineByID(ctx context.Context, app *fly.AppCompact) (*fly.Machine, error) {
flapsClient, err := getFlapsClient(ctx, app)
if err != nil {
return nil, err
}

machineID := flag.GetString(ctx, "machine")
machine, err := flapsClient.Get(ctx, machineID)
if err != nil {
return nil, err
}

return machine, nil
}

// argsGetImgPath returns an image path and its OrgID from the command line or from a
// selected app machine, using `app`, `image`, `select`, and `machine`.
func argsGetImgPath(ctx context.Context) (string, string, error) {
app, err := argsGetAppCompact(ctx)
if err != nil {
return "", "", err
}

if flag.IsSpecified(ctx, "image") {
if flag.IsSpecified(ctx, "machine") || flag.IsSpecified(ctx, "select") {
return "", "", fmt.Errorf("image option cannot be used with machine and select options")
}

path := flag.GetString(ctx, "image")
return path, app.Organization.ID, nil
}

machine, err := argsGetMachine(ctx, app)
if err != nil {
return "", "", err
}

return imageRefPath(&machine.ImageRef), app.Organization.ID, nil
}

// argsGetImages returns a list of images in ImgInfo format from
// command line args or the environment, using `org`, `app`, `running`.
func argsGetImages(ctx context.Context) (map[ImgInfo]Unit, error) {
ios := iostreams.FromContext(ctx)
spin := spinner.Run(ios, "Finding images...")
defer spin.Stop()

if appName := flag.GetApp(ctx); appName != "" {
return argsGetAppImages(ctx, appName)
} else if orgName := flag.GetOrg(ctx); orgName != "" {
return argsGetOrgImages(ctx, orgName)
} else if appName := appconfig.NameFromContext(ctx); appName != "" {
return argsGetAppImages(ctx, appName)
}
return nil, fmt.Errorf("No org or application specified")
}

// argsGetOrgImages returns a list of images for an org in ImgInfo format
// from `running`.
func argsGetOrgImages(ctx context.Context, orgName string) (map[ImgInfo]Unit, error) {
client := flyutil.ClientFromContext(ctx)
org, err := client.GetOrganizationBySlug(ctx, orgName)
if err != nil {
return nil, err
}

apps, err := client.GetAppsForOrganization(ctx, org.ID)
if err != nil {
return nil, err
}

eg, ctx := errgroup.WithContext(ctx)
eg.SetLimit(concurrentScans)
mu := sync.Mutex{}
allImgs := make(map[ImgInfo]Unit)
for n := range apps {
n := n
eg.Go(func() error {
app := &apps[n]
imgs, err := argsGetOrgAppImages(ctx, org.Name, org.ID, app.Name)
if err != nil {
return fmt.Errorf("could not fetch images for %q app: %w", app.Name, err)
}

mu.Lock()
AugmentMap(allImgs, imgs)
mu.Unlock()
return nil
})
}
if err := eg.Wait(); err != nil {
return nil, err
}

return allImgs, nil
}

// argsGetAppImages returns a list of images for an app in ImgInfo format
// from `running`.
func argsGetAppImages(ctx context.Context, appName string) (map[ImgInfo]Unit, error) {
apiClient := flyutil.ClientFromContext(ctx)
app, err := apiClient.GetAppCompact(ctx, appName)
if err != nil {
return nil, fmt.Errorf("failed to get app %q: %w", appName, err)
}

org := app.Organization
return argsGetOrgAppImages(ctx, org.Name, org.ID, app.Name)
}

// argsGetOrgAppImages returns a list of images for an org/app in ImgInfo format
// from `running`.
func argsGetOrgAppImages(ctx context.Context, orgName, orgId, appName string) (map[ImgInfo]Unit, error) {
flapsClient, err := flapsutil.NewClientWithOptions(ctx, flaps.NewClientOpts{
OrgSlug: orgId,
AppName: appName,
})
if err != nil {
return nil, fmt.Errorf("failed to create flaps client for %q: %w", appName, err)
}

machines, err := flapsClient.ListActive(ctx)
if err != nil {
return nil, err
}

if flag.GetBool(ctx, "running") {
machines = lo.Filter(machines, func(machine *fly.Machine, _ int) bool {
return machine.State == fly.MachineStateStarted
})
}

imgs := make(map[ImgInfo]Unit)
for _, machine := range machines {
ir := machine.ImageRef
imgPath := fmt.Sprintf("%s/%s@%s", ir.Registry, ir.Repository, ir.Digest)

img := ImgInfo{
Org: orgName,
OrgID: orgId,
App: appName,
Mach: machine.Name,
Path: imgPath,
}
imgs[img] = Unit{}
}
return imgs, nil
}
26 changes: 26 additions & 0 deletions internal/command/registry/auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package registry

import (
"context"
"fmt"

"github.com/superfly/flyctl/gql"
"github.com/superfly/flyctl/internal/flyutil"
)

func makeToken(ctx context.Context, name, orgID, expiry, profile string, options *gql.LimitedAccessTokenOptions) (*gql.CreateLimitedAccessTokenResponse, error) {
timflyio marked this conversation as resolved.
Show resolved Hide resolved
apiClient := flyutil.ClientFromContext(ctx)
resp, err := gql.CreateLimitedAccessToken(
ctx,
apiClient.GenqClient(),
name,
orgID,
profile,
options,
expiry,
)
if err != nil {
return nil, fmt.Errorf("failed creating token: %w", err)
}
return resp, nil
}
24 changes: 24 additions & 0 deletions internal/command/registry/command.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package registry

import (
"github.com/spf13/cobra"

"github.com/superfly/flyctl/internal/command"
)

func New() *cobra.Command {
const (
usage = "registry"
short = "Operate on registry images"
long = "Scan registry images for an SBOM or vulnerabilities."
)
cmd := command.New(usage, short, long, nil)
timflyio marked this conversation as resolved.
Show resolved Hide resolved

cmd.AddCommand(
newSbom(),
newVulns(),
newVulnSummary(),
)

return cmd
}
Loading
Loading