mirror of
https://github.com/anchore/syft.git
synced 2025-11-18 17:03:17 +01:00
Add documentation around catalogers, UI elements, and the event bus (#143)
* add basic documentation for catalogers (with refactoring for simplification) Signed-off-by: Alex Goodman <alex.goodman@anchore.com> * add docs for catalog parsers, UI, and event bus Signed-off-by: Alex Goodman <alex.goodman@anchore.com> * update bus phrasing Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
This commit is contained in:
parent
5042d371cf
commit
51b9c73c31
@ -1,3 +1,16 @@
|
|||||||
|
/*
|
||||||
|
Package bus provides access to a singleton instance of an event bus (provided by the calling application). The event bus
|
||||||
|
is intended to allow for the syft library to publish events which library consumers can subscribe to. These events
|
||||||
|
can provide static information, but also have an object as a payload for which the consumer can poll for updates.
|
||||||
|
This is akin to a logger, except instead of only allowing strings to be logged, rich objects that can be interacted with.
|
||||||
|
|
||||||
|
Note that the singleton instance is only allowed to publish events and not subscribe to them --this is intentional.
|
||||||
|
Internal library interactions should continue to use traditional in-execution-path approaches for data sharing
|
||||||
|
(e.g. function returns and channels) and not depend on bus subscriptions for critical interactions (e.g. one part of the
|
||||||
|
lib publishes an event and another part of the lib subscribes and reacts to that event). The bus is provided only as a
|
||||||
|
means for consumers to observe events emitted from the library (such as to provide a rich UI) and not to allow
|
||||||
|
consumers to augment or otherwise change execution.
|
||||||
|
*/
|
||||||
package bus
|
package bus
|
||||||
|
|
||||||
import "github.com/wagoodman/go-partybus"
|
import "github.com/wagoodman/go-partybus"
|
||||||
@ -5,6 +18,8 @@ import "github.com/wagoodman/go-partybus"
|
|||||||
var publisher partybus.Publisher
|
var publisher partybus.Publisher
|
||||||
var active bool
|
var active bool
|
||||||
|
|
||||||
|
// SetPublisher sets the singleton event bus publisher. This is optional; if no bus is provided, the library will
|
||||||
|
// behave no differently than if a bus had been provided.
|
||||||
func SetPublisher(p partybus.Publisher) {
|
func SetPublisher(p partybus.Publisher) {
|
||||||
publisher = p
|
publisher = p
|
||||||
if p != nil {
|
if p != nil {
|
||||||
@ -12,6 +27,7 @@ func SetPublisher(p partybus.Publisher) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Publish an event onto the bus. If there is no bus set by the calling application, this does nothing.
|
||||||
func Publish(event partybus.Event) {
|
func Publish(event partybus.Event) {
|
||||||
if active {
|
if active {
|
||||||
publisher.Publish(event)
|
publisher.Publish(event)
|
||||||
|
|||||||
@ -8,6 +8,8 @@ import (
|
|||||||
"github.com/wagoodman/go-partybus"
|
"github.com/wagoodman/go-partybus"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// CatalogerFinishedHandler is a UI function for processing the CatalogerFinished bus event, displaying the catalog
|
||||||
|
// via the given presenter to stdout.
|
||||||
func CatalogerFinishedHandler(event partybus.Event) error {
|
func CatalogerFinishedHandler(event partybus.Event) error {
|
||||||
// show the report to stdout
|
// show the report to stdout
|
||||||
pres, err := syftEventParsers.ParseCatalogerFinished(event)
|
pres, err := syftEventParsers.ParseCatalogerFinished(event)
|
||||||
|
|||||||
@ -1,3 +1,21 @@
|
|||||||
|
/*
|
||||||
|
Package etui provides an "ephemeral" terminal user interface to display the application state dynamically.
|
||||||
|
The terminal is placed into raw mode and the cursor is manipulated to allow for a dynamic, multi-line
|
||||||
|
UI (provided by the jotframe lib), for this reason all other application mechanisms that write to the screen
|
||||||
|
must be suppressed before starting (such as logs); since bytes in the device and in application memory combine to make
|
||||||
|
a shared state, bytes coming from elsewhere to the screen will disrupt this state.
|
||||||
|
|
||||||
|
This UI is primarily driven off of events from the event bus, creating single-line terminal widgets to represent a
|
||||||
|
published element on the event bus, typically polling the element for the latest state. This allows for the UI to
|
||||||
|
control update frequency to the screen, provide "liveness" indications that are interpolated between bus events,
|
||||||
|
and overall loosely couple the bus events from screen interactions.
|
||||||
|
|
||||||
|
By convention, all elements published on the bus should be treated as read-only, and publishers on the bus should
|
||||||
|
attempt to enforce this when possible by wrapping complex objects with interfaces to prescribe interactions. Also by
|
||||||
|
convention, each new event that the UI should respond to should be added either in this package as a handler function,
|
||||||
|
or in the shared ui package as a function on the main handler object. All handler functions should be completed
|
||||||
|
processing an event before the ETUI exits (coordinated with a sync.WaitGroup)
|
||||||
|
*/
|
||||||
package etui
|
package etui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -18,8 +36,10 @@ import (
|
|||||||
"github.com/wagoodman/jotframe/pkg/frame"
|
"github.com/wagoodman/jotframe/pkg/frame"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO: specify per-platform implementations with build tags
|
// TODO: specify per-platform implementations with build tags (needed when windows is supported by syft)
|
||||||
|
|
||||||
|
// setupScreen creates a new jotframe object to manage specific screen lines dynamically, preparing the screen device
|
||||||
|
// as needed (i.e. setting the terminal to raw mode).
|
||||||
func setupScreen(output *os.File) *frame.Frame {
|
func setupScreen(output *os.File) *frame.Frame {
|
||||||
config := frame.Config{
|
config := frame.Config{
|
||||||
PositionPolicy: frame.PolicyFloatForward,
|
PositionPolicy: frame.PolicyFloatForward,
|
||||||
@ -36,6 +56,8 @@ func setupScreen(output *os.File) *frame.Frame {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// nolint:funlen,gocognit
|
// nolint:funlen,gocognit
|
||||||
|
// OutputToEphemeralTUI is a UI function that provides a terminal UI experience without a separate, in-application
|
||||||
|
// screen buffer. All logging is suppressed, buffered to a string, and output after the ETUI has been torn down.
|
||||||
func OutputToEphemeralTUI(workerErrs <-chan error, subscription *partybus.Subscription) error {
|
func OutputToEphemeralTUI(workerErrs <-chan error, subscription *partybus.Subscription) error {
|
||||||
output := os.Stderr
|
output := os.Stderr
|
||||||
|
|
||||||
@ -84,6 +106,7 @@ eventLoop:
|
|||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case err := <-workerErrs:
|
case err := <-workerErrs:
|
||||||
|
// TODO: we should show errors more explicitly in the ETUI
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import (
|
|||||||
"github.com/wagoodman/jotframe/pkg/frame"
|
"github.com/wagoodman/jotframe/pkg/frame"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// appUpdateAvailableHandler is a UI handler function to display a new application version to the top of the screen.
|
||||||
func appUpdateAvailableHandler(_ context.Context, fr *frame.Frame, event partybus.Event, _ *sync.WaitGroup) error {
|
func appUpdateAvailableHandler(_ context.Context, fr *frame.Frame, event partybus.Event, _ *sync.WaitGroup) error {
|
||||||
newVersion, err := syftEventParsers.ParseAppUpdateAvailable(event)
|
newVersion, err := syftEventParsers.ParseAppUpdateAvailable(event)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import (
|
|||||||
"github.com/wagoodman/go-partybus"
|
"github.com/wagoodman/go-partybus"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// LoggerUI is a UI function that leverages the displays all application logs to the screen.
|
||||||
func LoggerUI(workerErrs <-chan error, subscription *partybus.Subscription) error {
|
func LoggerUI(workerErrs <-chan error, subscription *partybus.Subscription) error {
|
||||||
events := subscription.Events()
|
events := subscription.Events()
|
||||||
eventLoop:
|
eventLoop:
|
||||||
|
|||||||
@ -10,6 +10,8 @@ import (
|
|||||||
|
|
||||||
// TODO: build tags to exclude options from windows
|
// TODO: build tags to exclude options from windows
|
||||||
|
|
||||||
|
// Select is responsible for determining the specific UI function given select user option, the current platform
|
||||||
|
// config values, and environment status (such as a TTY being present).
|
||||||
func Select(verbose, quiet bool) UI {
|
func Select(verbose, quiet bool) UI {
|
||||||
var ui UI
|
var ui UI
|
||||||
|
|
||||||
|
|||||||
@ -4,4 +4,6 @@ import (
|
|||||||
"github.com/wagoodman/go-partybus"
|
"github.com/wagoodman/go-partybus"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// UI is a function that takes a channel of errors from the main() worker and a event bus subscription and is
|
||||||
|
// responsible for displaying pertinent events to the user, on the screen or otherwise.
|
||||||
type UI func(<-chan error, *partybus.Subscription) error
|
type UI func(<-chan error, *partybus.Subscription) error
|
||||||
|
|||||||
@ -7,22 +7,25 @@ import (
|
|||||||
|
|
||||||
const valueNotProvided = "[not provided]"
|
const valueNotProvided = "[not provided]"
|
||||||
|
|
||||||
|
// all variables here are provided as build-time arguments, with clear default values
|
||||||
var version = valueNotProvided
|
var version = valueNotProvided
|
||||||
var gitCommit = valueNotProvided
|
var gitCommit = valueNotProvided
|
||||||
var gitTreeState = valueNotProvided
|
var gitTreeState = valueNotProvided
|
||||||
var buildDate = valueNotProvided
|
var buildDate = valueNotProvided
|
||||||
var platform = fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH)
|
var platform = fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH)
|
||||||
|
|
||||||
|
// Version defines the application version details (generally from build information)
|
||||||
type Version struct {
|
type Version struct {
|
||||||
Version string
|
Version string // application semantic version
|
||||||
GitCommit string
|
GitCommit string // git SHA at build-time
|
||||||
GitTreeState string
|
GitTreeState string // indication of git tree (either "clean" or "dirty") at build-time
|
||||||
BuildDate string
|
BuildDate string // date of the build
|
||||||
GoVersion string
|
GoVersion string // go runtime version at build-time
|
||||||
Compiler string
|
Compiler string // compiler used at build-time
|
||||||
Platform string
|
Platform string // GOOS and GOARCH at build-time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FromBuild provides all version details
|
||||||
func FromBuild() Version {
|
func FromBuild() Version {
|
||||||
return Version{
|
return Version{
|
||||||
Version: version,
|
Version: version,
|
||||||
|
|||||||
@ -7,11 +7,13 @@ import (
|
|||||||
"github.com/anchore/syft/syft/scope"
|
"github.com/anchore/syft/syft/scope"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Cataloger catalogs pkg.ApkPkg Package Types defined in Alpine DB files.
|
||||||
type Cataloger struct {
|
type Cataloger struct {
|
||||||
cataloger common.GenericCataloger
|
cataloger common.GenericCataloger
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCataloger() *Cataloger {
|
// New returns a new Alpine DB cataloger object.
|
||||||
|
func New() *Cataloger {
|
||||||
globParsers := map[string]common.ParserFn{
|
globParsers := map[string]common.ParserFn{
|
||||||
"**/lib/apk/db/installed": parseApkDB,
|
"**/lib/apk/db/installed": parseApkDB,
|
||||||
}
|
}
|
||||||
@ -21,14 +23,17 @@ func NewCataloger() *Cataloger {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Name returns a string that uniquely describes this cataloger.
|
||||||
func (a *Cataloger) Name() string {
|
func (a *Cataloger) Name() string {
|
||||||
return "apkdb-cataloger"
|
return "apkdb-cataloger"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SelectFiles returns a set of discovered Alpine DB files from the user content source.
|
||||||
func (a *Cataloger) SelectFiles(resolver scope.FileResolver) []file.Reference {
|
func (a *Cataloger) SelectFiles(resolver scope.FileResolver) []file.Reference {
|
||||||
return a.cataloger.SelectFiles(resolver)
|
return a.cataloger.SelectFiles(resolver)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Catalog returns the Packages indexed from all Alpine DB files discovered.
|
||||||
func (a *Cataloger) Catalog(contents map[file.Reference]string) ([]pkg.Package, error) {
|
func (a *Cataloger) Catalog(contents map[file.Reference]string) ([]pkg.Package, error) {
|
||||||
return a.cataloger.Catalog(contents, a.Name())
|
return a.cataloger.Catalog(contents, a.Name())
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,10 +9,16 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/anchore/syft/internal/log"
|
"github.com/anchore/syft/internal/log"
|
||||||
|
"github.com/anchore/syft/syft/cataloger/common"
|
||||||
"github.com/anchore/syft/syft/pkg"
|
"github.com/anchore/syft/syft/pkg"
|
||||||
"github.com/mitchellh/mapstructure"
|
"github.com/mitchellh/mapstructure"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// integrity check
|
||||||
|
var _ common.ParserFn = parseApkDB
|
||||||
|
|
||||||
|
// parseApkDb parses individual packages from a given Alpine DB file. For more information on specific fields
|
||||||
|
// see https://wiki.alpinelinux.org/wiki/Apk_spec .
|
||||||
func parseApkDB(_ string, reader io.Reader) ([]pkg.Package, error) {
|
func parseApkDB(_ string, reader io.Reader) ([]pkg.Package, error) {
|
||||||
packages := make([]pkg.Package, 0)
|
packages := make([]pkg.Package, 0)
|
||||||
|
|
||||||
@ -55,6 +61,7 @@ func parseApkDB(_ string, reader io.Reader) ([]pkg.Package, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// nolint:funlen
|
// nolint:funlen
|
||||||
|
// parseApkDBEntry reads and parses a single pkg.ApkMetadata element from the stream, returning nil if their are no more entries.
|
||||||
func parseApkDBEntry(reader io.Reader) (*pkg.ApkMetadata, error) {
|
func parseApkDBEntry(reader io.Reader) (*pkg.ApkMetadata, error) {
|
||||||
var entry pkg.ApkMetadata
|
var entry pkg.ApkMetadata
|
||||||
pkgFields := make(map[string]interface{})
|
pkgFields := make(map[string]interface{})
|
||||||
|
|||||||
@ -7,11 +7,13 @@ import (
|
|||||||
"github.com/anchore/syft/syft/scope"
|
"github.com/anchore/syft/syft/scope"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Cataloger catalogs pkg.GemPkg Package Types defined in Bundler Gemfile.lock files.
|
||||||
type Cataloger struct {
|
type Cataloger struct {
|
||||||
cataloger common.GenericCataloger
|
cataloger common.GenericCataloger
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCataloger() *Cataloger {
|
// New returns a new Bundler cataloger object.
|
||||||
|
func New() *Cataloger {
|
||||||
globParsers := map[string]common.ParserFn{
|
globParsers := map[string]common.ParserFn{
|
||||||
"**/Gemfile.lock": parseGemfileLockEntries,
|
"**/Gemfile.lock": parseGemfileLockEntries,
|
||||||
}
|
}
|
||||||
@ -21,14 +23,17 @@ func NewCataloger() *Cataloger {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Name returns a string that uniquely describes this cataloger.
|
||||||
func (a *Cataloger) Name() string {
|
func (a *Cataloger) Name() string {
|
||||||
return "bundler-cataloger"
|
return "bundler-cataloger"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SelectFiles returns a set of discovered Gemfile.lock files from the user content source.
|
||||||
func (a *Cataloger) SelectFiles(resolver scope.FileResolver) []file.Reference {
|
func (a *Cataloger) SelectFiles(resolver scope.FileResolver) []file.Reference {
|
||||||
return a.cataloger.SelectFiles(resolver)
|
return a.cataloger.SelectFiles(resolver)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Catalog returns the Packages indexed from all Gemfile.lock files discovered.
|
||||||
func (a *Cataloger) Catalog(contents map[file.Reference]string) ([]pkg.Package, error) {
|
func (a *Cataloger) Catalog(contents map[file.Reference]string) ([]pkg.Package, error) {
|
||||||
return a.cataloger.Catalog(contents, a.Name())
|
return a.cataloger.Catalog(contents, a.Name())
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,11 +6,16 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/anchore/syft/internal"
|
"github.com/anchore/syft/internal"
|
||||||
|
"github.com/anchore/syft/syft/cataloger/common"
|
||||||
"github.com/anchore/syft/syft/pkg"
|
"github.com/anchore/syft/syft/pkg"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// integrity check
|
||||||
|
var _ common.ParserFn = parseGemfileLockEntries
|
||||||
|
|
||||||
var sectionsOfInterest = internal.NewStringSetFromSlice([]string{"GEM"})
|
var sectionsOfInterest = internal.NewStringSetFromSlice([]string{"GEM"})
|
||||||
|
|
||||||
|
// parseGemfileLockEntries is a parser function for Gemfile.lock contents, returning all Gems discovered.
|
||||||
func parseGemfileLockEntries(_ string, reader io.Reader) ([]pkg.Package, error) {
|
func parseGemfileLockEntries(_ string, reader io.Reader) ([]pkg.Package, error) {
|
||||||
pkgs := make([]pkg.Package, 0)
|
pkgs := make([]pkg.Package, 0)
|
||||||
scanner := bufio.NewScanner(reader)
|
scanner := bufio.NewScanner(reader)
|
||||||
|
|||||||
@ -4,14 +4,6 @@ import (
|
|||||||
"github.com/anchore/stereoscope/pkg/file"
|
"github.com/anchore/stereoscope/pkg/file"
|
||||||
"github.com/anchore/syft/internal/bus"
|
"github.com/anchore/syft/internal/bus"
|
||||||
"github.com/anchore/syft/internal/log"
|
"github.com/anchore/syft/internal/log"
|
||||||
"github.com/anchore/syft/syft/cataloger/apkdb"
|
|
||||||
"github.com/anchore/syft/syft/cataloger/bundler"
|
|
||||||
"github.com/anchore/syft/syft/cataloger/dpkg"
|
|
||||||
golang "github.com/anchore/syft/syft/cataloger/golang"
|
|
||||||
"github.com/anchore/syft/syft/cataloger/java"
|
|
||||||
"github.com/anchore/syft/syft/cataloger/javascript"
|
|
||||||
"github.com/anchore/syft/syft/cataloger/python"
|
|
||||||
"github.com/anchore/syft/syft/cataloger/rpmdb"
|
|
||||||
"github.com/anchore/syft/syft/event"
|
"github.com/anchore/syft/syft/event"
|
||||||
"github.com/anchore/syft/syft/pkg"
|
"github.com/anchore/syft/syft/pkg"
|
||||||
"github.com/anchore/syft/syft/scope"
|
"github.com/anchore/syft/syft/scope"
|
||||||
@ -20,54 +12,14 @@ import (
|
|||||||
"github.com/wagoodman/go-progress"
|
"github.com/wagoodman/go-progress"
|
||||||
)
|
)
|
||||||
|
|
||||||
var controllerInstance controller
|
// Monitor provides progress-related data for observing the progress of a Catalog() call (published on the event bus).
|
||||||
|
|
||||||
func init() {
|
|
||||||
controllerInstance = newController()
|
|
||||||
}
|
|
||||||
|
|
||||||
func Catalogers() []string {
|
|
||||||
c := make([]string, len(controllerInstance.catalogers))
|
|
||||||
for idx, catalog := range controllerInstance.catalogers {
|
|
||||||
c[idx] = catalog.Name()
|
|
||||||
}
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
func Catalog(s scope.Resolver) (*pkg.Catalog, error) {
|
|
||||||
return controllerInstance.catalog(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
type controller struct {
|
|
||||||
catalogers []Cataloger
|
|
||||||
}
|
|
||||||
|
|
||||||
func newController() controller {
|
|
||||||
ctrlr := controller{
|
|
||||||
catalogers: make([]Cataloger, 0),
|
|
||||||
}
|
|
||||||
ctrlr.add(dpkg.NewCataloger())
|
|
||||||
ctrlr.add(bundler.NewCataloger())
|
|
||||||
ctrlr.add(python.NewCataloger())
|
|
||||||
ctrlr.add(rpmdb.NewCataloger())
|
|
||||||
ctrlr.add(java.NewCataloger())
|
|
||||||
ctrlr.add(apkdb.NewCataloger())
|
|
||||||
ctrlr.add(golang.NewCataloger())
|
|
||||||
ctrlr.add(javascript.NewCataloger())
|
|
||||||
return ctrlr
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *controller) add(a Cataloger) {
|
|
||||||
log.Debugf("adding cataloger: %s", a.Name())
|
|
||||||
c.catalogers = append(c.catalogers, a)
|
|
||||||
}
|
|
||||||
|
|
||||||
type Monitor struct {
|
type Monitor struct {
|
||||||
FilesProcessed progress.Monitorable
|
FilesProcessed progress.Monitorable // the number of files selected and contents analyzed from all registered catalogers
|
||||||
PackagesDiscovered progress.Monitorable
|
PackagesDiscovered progress.Monitorable // the number of packages discovered from all registered catalogers
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *controller) trackCataloger() (*progress.Manual, *progress.Manual) {
|
// newMonitor creates a new Monitor object and publishes the object on the bus as a CatalogerStarted event.
|
||||||
|
func newMonitor() (*progress.Manual, *progress.Manual) {
|
||||||
filesProcessed := progress.Manual{}
|
filesProcessed := progress.Manual{}
|
||||||
packagesDiscovered := progress.Manual{}
|
packagesDiscovered := progress.Manual{}
|
||||||
|
|
||||||
@ -81,20 +33,25 @@ func (c *controller) trackCataloger() (*progress.Manual, *progress.Manual) {
|
|||||||
return &filesProcessed, &packagesDiscovered
|
return &filesProcessed, &packagesDiscovered
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *controller) catalog(s scope.Resolver) (*pkg.Catalog, error) {
|
// Catalog a given scope (container image or filesystem) with the given catalogers, returning all discovered packages.
|
||||||
|
// In order to efficiently retrieve contents from a underlying container image the content fetch requests are
|
||||||
|
// done in bulk. Specifically, all files of interest are collected from each catalogers and accumulated into a single
|
||||||
|
// request.
|
||||||
|
func Catalog(s scope.Resolver, catalogers ...Cataloger) (*pkg.Catalog, error) {
|
||||||
catalog := pkg.NewCatalog()
|
catalog := pkg.NewCatalog()
|
||||||
fileSelection := make([]file.Reference, 0)
|
fileSelection := make([]file.Reference, 0)
|
||||||
|
|
||||||
filesProcessed, packagesDiscovered := c.trackCataloger()
|
filesProcessed, packagesDiscovered := newMonitor()
|
||||||
|
|
||||||
// ask catalogers for files to extract from the image tar
|
// ask catalogers for files to extract from the image tar
|
||||||
for _, a := range c.catalogers {
|
for _, a := range catalogers {
|
||||||
fileSelection = append(fileSelection, a.SelectFiles(s)...)
|
fileSelection = append(fileSelection, a.SelectFiles(s)...)
|
||||||
log.Debugf("cataloger '%s' selected '%d' files", a.Name(), len(fileSelection))
|
log.Debugf("cataloger '%s' selected '%d' files", a.Name(), len(fileSelection))
|
||||||
filesProcessed.N += int64(len(fileSelection))
|
filesProcessed.N += int64(len(fileSelection))
|
||||||
}
|
}
|
||||||
|
|
||||||
// fetch contents for requested selection by catalogers
|
// fetch contents for requested selection by catalogers
|
||||||
|
// TODO: we should consider refactoring to return a set of io.Readers instead of the full contents themselves (allow for optional buffering).
|
||||||
contents, err := s.MultipleFileContentsByRef(fileSelection...)
|
contents, err := s.MultipleFileContentsByRef(fileSelection...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -102,7 +59,7 @@ func (c *controller) catalog(s scope.Resolver) (*pkg.Catalog, error) {
|
|||||||
|
|
||||||
// perform analysis, accumulating errors for each failed analysis
|
// perform analysis, accumulating errors for each failed analysis
|
||||||
var errs error
|
var errs error
|
||||||
for _, a := range c.catalogers {
|
for _, a := range catalogers {
|
||||||
// TODO: check for multiple rounds of analyses by Iterate error
|
// TODO: check for multiple rounds of analyses by Iterate error
|
||||||
packages, err := a.Catalog(contents)
|
packages, err := a.Catalog(contents)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -1,16 +1,48 @@
|
|||||||
|
/*
|
||||||
|
Package cataloger provides the ability to process files from a container image or file system and discover packages
|
||||||
|
(e.g. gems, wheels, jars, rpms, debs, etc.). Specifically, this package contains both a catalog function to utilize all
|
||||||
|
catalogers defined in child packages as well as the interface definition to implement a cataloger.
|
||||||
|
*/
|
||||||
package cataloger
|
package cataloger
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/anchore/stereoscope/pkg/file"
|
"github.com/anchore/stereoscope/pkg/file"
|
||||||
|
"github.com/anchore/syft/syft/cataloger/apkdb"
|
||||||
|
"github.com/anchore/syft/syft/cataloger/bundler"
|
||||||
|
"github.com/anchore/syft/syft/cataloger/dpkg"
|
||||||
|
"github.com/anchore/syft/syft/cataloger/golang"
|
||||||
|
"github.com/anchore/syft/syft/cataloger/java"
|
||||||
|
"github.com/anchore/syft/syft/cataloger/javascript"
|
||||||
|
"github.com/anchore/syft/syft/cataloger/python"
|
||||||
|
"github.com/anchore/syft/syft/cataloger/rpmdb"
|
||||||
"github.com/anchore/syft/syft/pkg"
|
"github.com/anchore/syft/syft/pkg"
|
||||||
"github.com/anchore/syft/syft/scope"
|
"github.com/anchore/syft/syft/scope"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Cataloger describes behavior for an object to participate in parsing container image or file system
|
||||||
|
// contents for the purpose of discovering Packages. Each concrete implementation should focus on discovering Packages
|
||||||
|
// for a specific Package Type or ecosystem.
|
||||||
type Cataloger interface {
|
type Cataloger interface {
|
||||||
|
// Name returns a string that uniquely describes a cataloger
|
||||||
Name() string
|
Name() string
|
||||||
// TODO: add ID / Name for catalog for uniquely identifying this cataloger type
|
// SelectFiles discovers and returns specific files that the cataloger would like to inspect the contents of.
|
||||||
SelectFiles(scope.FileResolver) []file.Reference
|
SelectFiles(scope.FileResolver) []file.Reference
|
||||||
// NOTE: one of the errors which is returned is "IterationNeeded", which indicates to the driver to
|
// Catalog is given the file contents and should return any discovered Packages after analyzing the contents.
|
||||||
// continue with another Select/Catalog pass
|
|
||||||
Catalog(map[file.Reference]string) ([]pkg.Package, error)
|
Catalog(map[file.Reference]string) ([]pkg.Package, error)
|
||||||
|
// TODO: add "IterationNeeded" error to indicate to the driver to continue with another Select/Catalog pass
|
||||||
|
// TODO: we should consider refactoring to return a set of io.Readers instead of the full contents themselves (allow for optional buffering).
|
||||||
|
}
|
||||||
|
|
||||||
|
// All returns a slice of all locally defined catalogers (defined in child packages).
|
||||||
|
func All() []Cataloger {
|
||||||
|
return []Cataloger{
|
||||||
|
dpkg.New(),
|
||||||
|
bundler.New(),
|
||||||
|
python.New(),
|
||||||
|
rpmdb.New(),
|
||||||
|
java.New(),
|
||||||
|
apkdb.New(),
|
||||||
|
golang.New(),
|
||||||
|
javascript.New(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,11 +7,13 @@ import (
|
|||||||
"github.com/anchore/syft/syft/scope"
|
"github.com/anchore/syft/syft/scope"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Cataloger catalogs pkg.DebPkg Package Types defined in DPKG status files.
|
||||||
type Cataloger struct {
|
type Cataloger struct {
|
||||||
cataloger common.GenericCataloger
|
cataloger common.GenericCataloger
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCataloger() *Cataloger {
|
// New returns a new Deb package cataloger object.
|
||||||
|
func New() *Cataloger {
|
||||||
globParsers := map[string]common.ParserFn{
|
globParsers := map[string]common.ParserFn{
|
||||||
"**/var/lib/dpkg/status": parseDpkgStatus,
|
"**/var/lib/dpkg/status": parseDpkgStatus,
|
||||||
}
|
}
|
||||||
@ -21,14 +23,17 @@ func NewCataloger() *Cataloger {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Name returns a string that uniquely describes this cataloger.
|
||||||
func (a *Cataloger) Name() string {
|
func (a *Cataloger) Name() string {
|
||||||
return "dpkg-cataloger"
|
return "dpkg-cataloger"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SelectFiles returns a set of discovered DPKG status files from the user content source.
|
||||||
func (a *Cataloger) SelectFiles(resolver scope.FileResolver) []file.Reference {
|
func (a *Cataloger) SelectFiles(resolver scope.FileResolver) []file.Reference {
|
||||||
return a.cataloger.SelectFiles(resolver)
|
return a.cataloger.SelectFiles(resolver)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Catalog returns the Packages indexed from all DPKG status files discovered.
|
||||||
func (a *Cataloger) Catalog(contents map[file.Reference]string) ([]pkg.Package, error) {
|
func (a *Cataloger) Catalog(contents map[file.Reference]string) ([]pkg.Package, error) {
|
||||||
return a.cataloger.Catalog(contents, a.Name())
|
return a.cataloger.Catalog(contents, a.Name())
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,12 +6,17 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/syft/cataloger/common"
|
||||||
"github.com/anchore/syft/syft/pkg"
|
"github.com/anchore/syft/syft/pkg"
|
||||||
"github.com/mitchellh/mapstructure"
|
"github.com/mitchellh/mapstructure"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// integrity check
|
||||||
|
var _ common.ParserFn = parseDpkgStatus
|
||||||
|
|
||||||
var errEndOfPackages = fmt.Errorf("no more packages to read")
|
var errEndOfPackages = fmt.Errorf("no more packages to read")
|
||||||
|
|
||||||
|
// parseDpkgStatus is a parser function for Debian DB status contents, returning all Debian packages listed.
|
||||||
func parseDpkgStatus(_ string, reader io.Reader) ([]pkg.Package, error) {
|
func parseDpkgStatus(_ string, reader io.Reader) ([]pkg.Package, error) {
|
||||||
buffedReader := bufio.NewReader(reader)
|
buffedReader := bufio.NewReader(reader)
|
||||||
var packages = make([]pkg.Package, 0)
|
var packages = make([]pkg.Package, 0)
|
||||||
@ -35,6 +40,7 @@ func parseDpkgStatus(_ string, reader io.Reader) ([]pkg.Package, error) {
|
|||||||
return packages, nil
|
return packages, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parseDpkgStatusEntry returns an individual Dpkg entry, or returns errEndOfPackages if there are no more packages to parse from the reader.
|
||||||
func parseDpkgStatusEntry(reader *bufio.Reader) (entry pkg.DpkgMetadata, err error) {
|
func parseDpkgStatusEntry(reader *bufio.Reader) (entry pkg.DpkgMetadata, err error) {
|
||||||
dpkgFields := make(map[string]string)
|
dpkgFields := make(map[string]string)
|
||||||
var key string
|
var key string
|
||||||
|
|||||||
@ -7,11 +7,13 @@ import (
|
|||||||
"github.com/anchore/syft/syft/scope"
|
"github.com/anchore/syft/syft/scope"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Cataloger catalogs pkg.GoModulePkg Package Types defined in go.mod files.
|
||||||
type Cataloger struct {
|
type Cataloger struct {
|
||||||
cataloger common.GenericCataloger
|
cataloger common.GenericCataloger
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCataloger() *Cataloger {
|
// New returns a new Go module cataloger object.
|
||||||
|
func New() *Cataloger {
|
||||||
globParsers := map[string]common.ParserFn{
|
globParsers := map[string]common.ParserFn{
|
||||||
"**/go.mod": parseGoMod,
|
"**/go.mod": parseGoMod,
|
||||||
}
|
}
|
||||||
@ -21,14 +23,17 @@ func NewCataloger() *Cataloger {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Name returns a string that uniquely describes this cataloger.
|
||||||
func (a *Cataloger) Name() string {
|
func (a *Cataloger) Name() string {
|
||||||
return "go-cataloger"
|
return "go-cataloger"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SelectFiles returns a set of discovered go.mod files from the user content source.
|
||||||
func (a *Cataloger) SelectFiles(resolver scope.FileResolver) []file.Reference {
|
func (a *Cataloger) SelectFiles(resolver scope.FileResolver) []file.Reference {
|
||||||
return a.cataloger.SelectFiles(resolver)
|
return a.cataloger.SelectFiles(resolver)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Catalog returns the Packages indexed from all go.mod files discovered.
|
||||||
func (a *Cataloger) Catalog(contents map[file.Reference]string) ([]pkg.Package, error) {
|
func (a *Cataloger) Catalog(contents map[file.Reference]string) ([]pkg.Package, error) {
|
||||||
return a.cataloger.Catalog(contents, a.Name())
|
return a.cataloger.Catalog(contents, a.Name())
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import (
|
|||||||
"github.com/anchore/syft/syft/pkg"
|
"github.com/anchore/syft/syft/pkg"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// parseGoMod takes a go.mod and lists all packages discovered.
|
||||||
func parseGoMod(path string, reader io.Reader) ([]pkg.Package, error) {
|
func parseGoMod(path string, reader io.Reader) ([]pkg.Package, error) {
|
||||||
packages := make(map[string]pkg.Package)
|
packages := make(map[string]pkg.Package)
|
||||||
|
|
||||||
|
|||||||
@ -7,9 +7,13 @@ import (
|
|||||||
|
|
||||||
"github.com/anchore/syft/internal"
|
"github.com/anchore/syft/internal"
|
||||||
"github.com/anchore/syft/internal/file"
|
"github.com/anchore/syft/internal/file"
|
||||||
|
"github.com/anchore/syft/syft/cataloger/common"
|
||||||
"github.com/anchore/syft/syft/pkg"
|
"github.com/anchore/syft/syft/pkg"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// integrity check
|
||||||
|
var _ common.ParserFn = parseJavaArchive
|
||||||
|
|
||||||
var archiveFormatGlobs = []string{
|
var archiveFormatGlobs = []string{
|
||||||
"**/*.jar",
|
"**/*.jar",
|
||||||
"**/*.war",
|
"**/*.war",
|
||||||
@ -28,6 +32,7 @@ type archiveParser struct {
|
|||||||
detectNested bool
|
detectNested bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parseJavaArchive is a parser function for java archive contents, returning all Java libraries and nested archives.
|
||||||
func parseJavaArchive(virtualPath string, reader io.Reader) ([]pkg.Package, error) {
|
func parseJavaArchive(virtualPath string, reader io.Reader) ([]pkg.Package, error) {
|
||||||
parser, cleanupFn, err := newJavaArchiveParser(virtualPath, reader, true)
|
parser, cleanupFn, err := newJavaArchiveParser(virtualPath, reader, true)
|
||||||
// note: even on error, we should always run cleanup functions
|
// note: even on error, we should always run cleanup functions
|
||||||
@ -38,6 +43,7 @@ func parseJavaArchive(virtualPath string, reader io.Reader) ([]pkg.Package, erro
|
|||||||
return parser.parse()
|
return parser.parse()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// uniquePkgKey creates a unique string to identify the given package.
|
||||||
func uniquePkgKey(p *pkg.Package) string {
|
func uniquePkgKey(p *pkg.Package) string {
|
||||||
if p == nil {
|
if p == nil {
|
||||||
return ""
|
return ""
|
||||||
@ -45,6 +51,8 @@ func uniquePkgKey(p *pkg.Package) string {
|
|||||||
return fmt.Sprintf("%s|%s", p.Name, p.Version)
|
return fmt.Sprintf("%s|%s", p.Name, p.Version)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// newJavaArchiveParser returns a new java archive parser object for the given archive. Can be configured to discover
|
||||||
|
// and parse nested archives or ignore them.
|
||||||
func newJavaArchiveParser(virtualPath string, reader io.Reader, detectNested bool) (*archiveParser, func(), error) {
|
func newJavaArchiveParser(virtualPath string, reader io.Reader, detectNested bool) (*archiveParser, func(), error) {
|
||||||
contentPath, archivePath, cleanupFn, err := saveArchiveToTmp(reader)
|
contentPath, archivePath, cleanupFn, err := saveArchiveToTmp(reader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -67,6 +75,7 @@ func newJavaArchiveParser(virtualPath string, reader io.Reader, detectNested boo
|
|||||||
}, cleanupFn, nil
|
}, cleanupFn, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parse the loaded archive and return all packages found.
|
||||||
func (j *archiveParser) parse() ([]pkg.Package, error) {
|
func (j *archiveParser) parse() ([]pkg.Package, error) {
|
||||||
var pkgs = make([]pkg.Package, 0)
|
var pkgs = make([]pkg.Package, 0)
|
||||||
|
|
||||||
@ -106,6 +115,7 @@ func (j *archiveParser) parse() ([]pkg.Package, error) {
|
|||||||
return pkgs, nil
|
return pkgs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// discoverMainPackage parses the root Java manifest used as the parent package to all discovered nested packages.
|
||||||
func (j *archiveParser) discoverMainPackage() (*pkg.Package, error) {
|
func (j *archiveParser) discoverMainPackage() (*pkg.Package, error) {
|
||||||
// search and parse java manifest files
|
// search and parse java manifest files
|
||||||
manifestMatches := j.fileManifest.GlobMatch(manifestPath)
|
manifestMatches := j.fileManifest.GlobMatch(manifestPath)
|
||||||
@ -140,6 +150,8 @@ func (j *archiveParser) discoverMainPackage() (*pkg.Package, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// discoverPkgsFromPomProperties parses Maven POM properties for a given parent package, returning all listed Java packages found and
|
||||||
|
// associating each discovered package to the given parent package.
|
||||||
func (j *archiveParser) discoverPkgsFromPomProperties(parentPkg *pkg.Package) ([]pkg.Package, error) {
|
func (j *archiveParser) discoverPkgsFromPomProperties(parentPkg *pkg.Package) ([]pkg.Package, error) {
|
||||||
var pkgs = make([]pkg.Package, 0)
|
var pkgs = make([]pkg.Package, 0)
|
||||||
parentKey := uniquePkgKey(parentPkg)
|
parentKey := uniquePkgKey(parentPkg)
|
||||||
@ -192,6 +204,8 @@ func (j *archiveParser) discoverPkgsFromPomProperties(parentPkg *pkg.Package) ([
|
|||||||
return pkgs, nil
|
return pkgs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// discoverPkgsFromNestedArchives finds Java archives within Java archives, returning all listed Java packages found and
|
||||||
|
// associating each discovered package to the given parent package.
|
||||||
func (j *archiveParser) discoverPkgsFromNestedArchives(parentPkg *pkg.Package) ([]pkg.Package, error) {
|
func (j *archiveParser) discoverPkgsFromNestedArchives(parentPkg *pkg.Package) ([]pkg.Package, error) {
|
||||||
var pkgs = make([]pkg.Package, 0)
|
var pkgs = make([]pkg.Package, 0)
|
||||||
|
|
||||||
|
|||||||
@ -7,11 +7,13 @@ import (
|
|||||||
"github.com/anchore/syft/syft/scope"
|
"github.com/anchore/syft/syft/scope"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Cataloger catalogs pkg.JavaPkg and pkg.JenkinsPluginPkg Package Types defined in java archive files.
|
||||||
type Cataloger struct {
|
type Cataloger struct {
|
||||||
cataloger common.GenericCataloger
|
cataloger common.GenericCataloger
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCataloger() *Cataloger {
|
// New returns a new Java archive cataloger object.
|
||||||
|
func New() *Cataloger {
|
||||||
globParsers := make(map[string]common.ParserFn)
|
globParsers := make(map[string]common.ParserFn)
|
||||||
for _, pattern := range archiveFormatGlobs {
|
for _, pattern := range archiveFormatGlobs {
|
||||||
globParsers[pattern] = parseJavaArchive
|
globParsers[pattern] = parseJavaArchive
|
||||||
@ -22,14 +24,17 @@ func NewCataloger() *Cataloger {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Name returns a string that uniquely describes this cataloger.
|
||||||
func (a *Cataloger) Name() string {
|
func (a *Cataloger) Name() string {
|
||||||
return "java-cataloger"
|
return "java-cataloger"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SelectFiles returns a set of discovered Java archive files from the user content source.
|
||||||
func (a *Cataloger) SelectFiles(resolver scope.FileResolver) []file.Reference {
|
func (a *Cataloger) SelectFiles(resolver scope.FileResolver) []file.Reference {
|
||||||
return a.cataloger.SelectFiles(resolver)
|
return a.cataloger.SelectFiles(resolver)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Catalog returns the Packages indexed from all Java archive files discovered.
|
||||||
func (a *Cataloger) Catalog(contents map[file.Reference]string) ([]pkg.Package, error) {
|
func (a *Cataloger) Catalog(contents map[file.Reference]string) ([]pkg.Package, error) {
|
||||||
return a.cataloger.Catalog(contents, a.Name())
|
return a.cataloger.Catalog(contents, a.Name())
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,11 +7,13 @@ import (
|
|||||||
"github.com/anchore/syft/syft/scope"
|
"github.com/anchore/syft/syft/scope"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Cataloger catalogs pkg.YarnPkg and pkg.NpmPkg Package Types defined in package-lock.json and yarn.lock files.
|
||||||
type Cataloger struct {
|
type Cataloger struct {
|
||||||
cataloger common.GenericCataloger
|
cataloger common.GenericCataloger
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCataloger() *Cataloger {
|
// New returns a new JavaScript cataloger object.
|
||||||
|
func New() *Cataloger {
|
||||||
globParsers := map[string]common.ParserFn{
|
globParsers := map[string]common.ParserFn{
|
||||||
"**/package-lock.json": parsePackageLock,
|
"**/package-lock.json": parsePackageLock,
|
||||||
"**/yarn.lock": parseYarnLock,
|
"**/yarn.lock": parseYarnLock,
|
||||||
@ -22,14 +24,17 @@ func NewCataloger() *Cataloger {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Name returns a string that uniquely describes this cataloger.
|
||||||
func (a *Cataloger) Name() string {
|
func (a *Cataloger) Name() string {
|
||||||
return "npm-cataloger"
|
return "javascript-cataloger"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SelectFiles returns a set of discovered Javascript ecosystem files from the user content source.
|
||||||
func (a *Cataloger) SelectFiles(resolver scope.FileResolver) []file.Reference {
|
func (a *Cataloger) SelectFiles(resolver scope.FileResolver) []file.Reference {
|
||||||
return a.cataloger.SelectFiles(resolver)
|
return a.cataloger.SelectFiles(resolver)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Catalog returns the Packages indexed from all Javascript ecosystem files discovered.
|
||||||
func (a *Cataloger) Catalog(contents map[file.Reference]string) ([]pkg.Package, error) {
|
func (a *Cataloger) Catalog(contents map[file.Reference]string) ([]pkg.Package, error) {
|
||||||
return a.cataloger.Catalog(contents, a.Name())
|
return a.cataloger.Catalog(contents, a.Name())
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,15 +5,21 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/syft/cataloger/common"
|
||||||
"github.com/anchore/syft/syft/pkg"
|
"github.com/anchore/syft/syft/pkg"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// integrity check
|
||||||
|
var _ common.ParserFn = parsePackageLock
|
||||||
|
|
||||||
|
// PackageLock represents a JavaScript package.lock json file
|
||||||
type PackageLock struct {
|
type PackageLock struct {
|
||||||
Requires bool `json:"requires"`
|
Requires bool `json:"requires"`
|
||||||
LockfileVersion int `json:"lockfileVersion"`
|
LockfileVersion int `json:"lockfileVersion"`
|
||||||
Dependencies Dependencies
|
Dependencies map[string]Dependency
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Dependency represents a single package dependency listed in the package.lock json file
|
||||||
type Dependency struct {
|
type Dependency struct {
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
Resolved string `json:"resolved"`
|
Resolved string `json:"resolved"`
|
||||||
@ -21,8 +27,7 @@ type Dependency struct {
|
|||||||
Requires map[string]string
|
Requires map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
type Dependencies map[string]Dependency
|
// parsePackageLock parses a package.lock and returns the discovered JavaScript packages.
|
||||||
|
|
||||||
func parsePackageLock(_ string, reader io.Reader) ([]pkg.Package, error) {
|
func parsePackageLock(_ string, reader io.Reader) ([]pkg.Package, error) {
|
||||||
packages := make([]pkg.Package, 0)
|
packages := make([]pkg.Package, 0)
|
||||||
dec := json.NewDecoder(reader)
|
dec := json.NewDecoder(reader)
|
||||||
|
|||||||
@ -8,9 +8,13 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/anchore/syft/internal/log"
|
"github.com/anchore/syft/internal/log"
|
||||||
|
"github.com/anchore/syft/syft/cataloger/common"
|
||||||
"github.com/anchore/syft/syft/pkg"
|
"github.com/anchore/syft/syft/pkg"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// integrity check
|
||||||
|
var _ common.ParserFn = parseYarnLock
|
||||||
|
|
||||||
var composedNameExp = regexp.MustCompile("^\"(@{1}[^@]+)")
|
var composedNameExp = regexp.MustCompile("^\"(@{1}[^@]+)")
|
||||||
var simpleNameExp = regexp.MustCompile(`^[a-zA-Z\-]+@`)
|
var simpleNameExp = regexp.MustCompile(`^[a-zA-Z\-]+@`)
|
||||||
var versionExp = regexp.MustCompile(`^\W+(version)\W+`)
|
var versionExp = regexp.MustCompile(`^\W+(version)\W+`)
|
||||||
|
|||||||
@ -7,14 +7,13 @@ import (
|
|||||||
"github.com/anchore/syft/syft/scope"
|
"github.com/anchore/syft/syft/scope"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Cataloger catalogs pkg.WheelPkg, pkg.EggPkg, and pkg.PythonRequirementsPkg Package Types defined in Python ecosystem files.
|
||||||
type Cataloger struct {
|
type Cataloger struct {
|
||||||
cataloger common.GenericCataloger
|
cataloger common.GenericCataloger
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCataloger() *Cataloger {
|
// New returns a new Python cataloger object.
|
||||||
// we want to match on partial dir names
|
func New() *Cataloger {
|
||||||
// /home/user/requests-2.10.0.dist-info/METADATA
|
|
||||||
// /home/user/requests-2.10.0/dist-info/METADATA
|
|
||||||
globParsers := map[string]common.ParserFn{
|
globParsers := map[string]common.ParserFn{
|
||||||
"**/*egg-info/PKG-INFO": parseEggMetadata,
|
"**/*egg-info/PKG-INFO": parseEggMetadata,
|
||||||
"**/*dist-info/METADATA": parseWheelMetadata,
|
"**/*dist-info/METADATA": parseWheelMetadata,
|
||||||
@ -27,14 +26,17 @@ func NewCataloger() *Cataloger {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Name returns a string that uniquely describes this cataloger.
|
||||||
func (a *Cataloger) Name() string {
|
func (a *Cataloger) Name() string {
|
||||||
return "python-cataloger"
|
return "python-cataloger"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SelectFiles returns a set of discovered Python ecosystem files from the user content source.
|
||||||
func (a *Cataloger) SelectFiles(resolver scope.FileResolver) []file.Reference {
|
func (a *Cataloger) SelectFiles(resolver scope.FileResolver) []file.Reference {
|
||||||
return a.cataloger.SelectFiles(resolver)
|
return a.cataloger.SelectFiles(resolver)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Catalog returns the Packages indexed from all Python ecosystem files discovered.
|
||||||
func (a *Cataloger) Catalog(contents map[file.Reference]string) ([]pkg.Package, error) {
|
func (a *Cataloger) Catalog(contents map[file.Reference]string) ([]pkg.Package, error) {
|
||||||
return a.cataloger.Catalog(contents, a.Name())
|
return a.cataloger.Catalog(contents, a.Name())
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,10 +4,15 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/syft/cataloger/common"
|
||||||
"github.com/anchore/syft/syft/pkg"
|
"github.com/anchore/syft/syft/pkg"
|
||||||
"github.com/pelletier/go-toml"
|
"github.com/pelletier/go-toml"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// integrity check
|
||||||
|
var _ common.ParserFn = parsePoetryLock
|
||||||
|
|
||||||
|
// parsePoetryLock is a parser function for poetry.lock contents, returning all python packages discovered.
|
||||||
func parsePoetryLock(_ string, reader io.Reader) ([]pkg.Package, error) {
|
func parsePoetryLock(_ string, reader io.Reader) ([]pkg.Package, error) {
|
||||||
tree, err := toml.LoadReader(reader)
|
tree, err := toml.LoadReader(reader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@ -6,9 +6,15 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/syft/cataloger/common"
|
||||||
"github.com/anchore/syft/syft/pkg"
|
"github.com/anchore/syft/syft/pkg"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// integrity check
|
||||||
|
var _ common.ParserFn = parseRequirementsTxt
|
||||||
|
|
||||||
|
// parseRequirementsTxt takes a Python requirements.txt file, returning all Python packages that are locked to a
|
||||||
|
// specific version.
|
||||||
func parseRequirementsTxt(_ string, reader io.Reader) ([]pkg.Package, error) {
|
func parseRequirementsTxt(_ string, reader io.Reader) ([]pkg.Package, error) {
|
||||||
packages := make([]pkg.Package, 0)
|
packages := make([]pkg.Package, 0)
|
||||||
|
|
||||||
@ -55,6 +61,7 @@ func parseRequirementsTxt(_ string, reader io.Reader) ([]pkg.Package, error) {
|
|||||||
return packages, nil
|
return packages, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// removeTrailingComment takes a requirements.txt line and strips off comment strings.
|
||||||
func removeTrailingComment(line string) string {
|
func removeTrailingComment(line string) string {
|
||||||
parts := strings.Split(line, "#")
|
parts := strings.Split(line, "#")
|
||||||
switch len(parts) {
|
switch len(parts) {
|
||||||
|
|||||||
@ -6,9 +6,16 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/syft/cataloger/common"
|
||||||
"github.com/anchore/syft/syft/pkg"
|
"github.com/anchore/syft/syft/pkg"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// integrity check
|
||||||
|
var _ common.ParserFn = parseWheelMetadata
|
||||||
|
var _ common.ParserFn = parseEggMetadata
|
||||||
|
|
||||||
|
// parseWheelMetadata is a parser function for individual Python Wheel metadata file contents, returning all Python
|
||||||
|
// packages listed.
|
||||||
func parseWheelMetadata(_ string, reader io.Reader) ([]pkg.Package, error) {
|
func parseWheelMetadata(_ string, reader io.Reader) ([]pkg.Package, error) {
|
||||||
packages, err := parseWheelOrEggMetadata(reader)
|
packages, err := parseWheelOrEggMetadata(reader)
|
||||||
for idx := range packages {
|
for idx := range packages {
|
||||||
@ -17,6 +24,8 @@ func parseWheelMetadata(_ string, reader io.Reader) ([]pkg.Package, error) {
|
|||||||
return packages, err
|
return packages, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parseEggMetadata is a parser function for individual Python Egg metadata file contents, returning all Python
|
||||||
|
// packages listed.
|
||||||
func parseEggMetadata(_ string, reader io.Reader) ([]pkg.Package, error) {
|
func parseEggMetadata(_ string, reader io.Reader) ([]pkg.Package, error) {
|
||||||
packages, err := parseWheelOrEggMetadata(reader)
|
packages, err := parseWheelOrEggMetadata(reader)
|
||||||
for idx := range packages {
|
for idx := range packages {
|
||||||
@ -25,6 +34,8 @@ func parseEggMetadata(_ string, reader io.Reader) ([]pkg.Package, error) {
|
|||||||
return packages, err
|
return packages, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parseWheelOrEggMetadata takes a Python Egg or Wheel (which share the same format and values for our purposes),
|
||||||
|
// returning all Python packages listed.
|
||||||
func parseWheelOrEggMetadata(reader io.Reader) ([]pkg.Package, error) {
|
func parseWheelOrEggMetadata(reader io.Reader) ([]pkg.Package, error) {
|
||||||
fields := make(map[string]string)
|
fields := make(map[string]string)
|
||||||
var key string
|
var key string
|
||||||
|
|||||||
@ -7,11 +7,13 @@ import (
|
|||||||
"github.com/anchore/syft/syft/scope"
|
"github.com/anchore/syft/syft/scope"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Cataloger catalogs pkg.RpmPkg Package Types defined in RPM DB files.
|
||||||
type Cataloger struct {
|
type Cataloger struct {
|
||||||
cataloger common.GenericCataloger
|
cataloger common.GenericCataloger
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCataloger() *Cataloger {
|
// New returns a new RPM DB cataloger object.
|
||||||
|
func New() *Cataloger {
|
||||||
globParsers := map[string]common.ParserFn{
|
globParsers := map[string]common.ParserFn{
|
||||||
"**/var/lib/rpm/Packages": parseRpmDB,
|
"**/var/lib/rpm/Packages": parseRpmDB,
|
||||||
}
|
}
|
||||||
@ -21,14 +23,17 @@ func NewCataloger() *Cataloger {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Name returns a string that uniquely describes this cataloger.
|
||||||
func (a *Cataloger) Name() string {
|
func (a *Cataloger) Name() string {
|
||||||
return "rpmdb-cataloger"
|
return "rpmdb-cataloger"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SelectFiles returns a set of discovered RPM DB files from the user content source.
|
||||||
func (a *Cataloger) SelectFiles(resolver scope.FileResolver) []file.Reference {
|
func (a *Cataloger) SelectFiles(resolver scope.FileResolver) []file.Reference {
|
||||||
return a.cataloger.SelectFiles(resolver)
|
return a.cataloger.SelectFiles(resolver)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Catalog returns the Packages indexed from all RPM DB files discovered.
|
||||||
func (a *Cataloger) Catalog(contents map[file.Reference]string) ([]pkg.Package, error) {
|
func (a *Cataloger) Catalog(contents map[file.Reference]string) ([]pkg.Package, error) {
|
||||||
return a.cataloger.Catalog(contents, a.Name())
|
return a.cataloger.Catalog(contents, a.Name())
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,9 +9,14 @@ import (
|
|||||||
rpmdb "github.com/anchore/go-rpmdb/pkg"
|
rpmdb "github.com/anchore/go-rpmdb/pkg"
|
||||||
"github.com/anchore/syft/internal"
|
"github.com/anchore/syft/internal"
|
||||||
"github.com/anchore/syft/internal/log"
|
"github.com/anchore/syft/internal/log"
|
||||||
|
"github.com/anchore/syft/syft/cataloger/common"
|
||||||
"github.com/anchore/syft/syft/pkg"
|
"github.com/anchore/syft/syft/pkg"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// integrity check
|
||||||
|
var _ common.ParserFn = parseRpmDB
|
||||||
|
|
||||||
|
// parseApkDb parses an "Packages" RPM DB and returns the Packages listed within it.
|
||||||
func parseRpmDB(_ string, reader io.Reader) ([]pkg.Package, error) {
|
func parseRpmDB(_ string, reader io.Reader) ([]pkg.Package, error) {
|
||||||
f, err := ioutil.TempFile("", internal.ApplicationName+"-rpmdb")
|
f, err := ioutil.TempFile("", internal.ApplicationName+"-rpmdb")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@ -41,7 +41,7 @@ func IdentifyDistro(s scope.Scope) distro.Distro {
|
|||||||
|
|
||||||
func CatalogFromScope(s scope.Scope) (*pkg.Catalog, error) {
|
func CatalogFromScope(s scope.Scope) (*pkg.Catalog, error) {
|
||||||
log.Info("building the catalog")
|
log.Info("building the catalog")
|
||||||
return cataloger.Catalog(s)
|
return cataloger.Catalog(s, cataloger.All()...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func SetLogger(logger logger.Logger) {
|
func SetLogger(logger logger.Logger) {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
package pkg
|
package pkg
|
||||||
|
|
||||||
// Available fields are described at http://manpages.ubuntu.com/manpages/xenial/man1/dpkg-query.1.html
|
// DpkgMetadata represents all captured data for a Debian package DB entry; available fields are described
|
||||||
// in the --showformat section
|
// at http://manpages.ubuntu.com/manpages/xenial/man1/dpkg-query.1.html in the --showformat section.
|
||||||
type DpkgMetadata struct {
|
type DpkgMetadata struct {
|
||||||
Package string `mapstructure:"Package" json:"package"`
|
Package string `mapstructure:"Package" json:"package"`
|
||||||
Source string `mapstructure:"Source" json:"source"`
|
Source string `mapstructure:"Source" json:"source"`
|
||||||
@ -9,6 +9,7 @@ type DpkgMetadata struct {
|
|||||||
// TODO: consider keeping the remaining values as an embedded map
|
// TODO: consider keeping the remaining values as an embedded map
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RpmMetadata represents all captured data for a RHEL package DB entry.
|
||||||
type RpmMetadata struct {
|
type RpmMetadata struct {
|
||||||
Version string `mapstructure:"Version" json:"version"`
|
Version string `mapstructure:"Version" json:"version"`
|
||||||
Epoch int `mapstructure:"Epoch" json:"epoch"`
|
Epoch int `mapstructure:"Epoch" json:"epoch"`
|
||||||
|
|||||||
@ -36,6 +36,8 @@ var (
|
|||||||
dockerPullStageChars = strings.Split("▁▃▄▅▆▇█", "")
|
dockerPullStageChars = strings.Split("▁▃▄▅▆▇█", "")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// startProcess is a helper function for providing common elements for long-running UI elements (such as a
|
||||||
|
// progress bar formatter and status spinner)
|
||||||
func startProcess() (format.Simple, *common.Spinner) {
|
func startProcess() (format.Simple, *common.Spinner) {
|
||||||
width, _ := frame.GetTerminalSize()
|
width, _ := frame.GetTerminalSize()
|
||||||
barWidth := int(0.25 * float64(width))
|
barWidth := int(0.25 * float64(width))
|
||||||
@ -48,6 +50,7 @@ func startProcess() (format.Simple, *common.Spinner) {
|
|||||||
return formatter, &spinner
|
return formatter, &spinner
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// formatDockerPullPhase returns a single character that represents the status of a layer pull.
|
||||||
func formatDockerPullPhase(phase docker.PullPhase, inputStr string) string {
|
func formatDockerPullPhase(phase docker.PullPhase, inputStr string) string {
|
||||||
switch phase {
|
switch phase {
|
||||||
case docker.WaitingPhase:
|
case docker.WaitingPhase:
|
||||||
@ -69,6 +72,7 @@ func formatDockerPullPhase(phase docker.PullPhase, inputStr string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// nolint:funlen
|
// nolint:funlen
|
||||||
|
// formatDockerImagePullStatus writes the docker image pull status summarized into a single line for the given state.
|
||||||
func formatDockerImagePullStatus(pullStatus *docker.PullStatus, spinner *common.Spinner, line *frame.Line) {
|
func formatDockerImagePullStatus(pullStatus *docker.PullStatus, spinner *common.Spinner, line *frame.Line) {
|
||||||
var size, current uint64
|
var size, current uint64
|
||||||
|
|
||||||
@ -136,6 +140,7 @@ func formatDockerImagePullStatus(pullStatus *docker.PullStatus, spinner *common.
|
|||||||
_, _ = io.WriteString(line, fmt.Sprintf(statusTitleTemplate+"%s%s", spin, title, progStr, auxInfo))
|
_, _ = io.WriteString(line, fmt.Sprintf(statusTitleTemplate+"%s%s", spin, title, progStr, auxInfo))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PullDockerImageHandler periodically writes a formatted line widget representing a docker image pull event.
|
||||||
func PullDockerImageHandler(ctx context.Context, fr *frame.Frame, event partybus.Event, wg *sync.WaitGroup) error {
|
func PullDockerImageHandler(ctx context.Context, fr *frame.Frame, event partybus.Event, wg *sync.WaitGroup) error {
|
||||||
_, pullStatus, err := stereoEventParsers.ParsePullDockerImage(event)
|
_, pullStatus, err := stereoEventParsers.ParsePullDockerImage(event)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -175,6 +180,7 @@ func PullDockerImageHandler(ctx context.Context, fr *frame.Frame, event partybus
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FetchImageHandler periodically writes a the image save and write-to-disk process in the form of a progress bar.
|
||||||
func FetchImageHandler(ctx context.Context, fr *frame.Frame, event partybus.Event, wg *sync.WaitGroup) error {
|
func FetchImageHandler(ctx context.Context, fr *frame.Frame, event partybus.Event, wg *sync.WaitGroup) error {
|
||||||
_, prog, err := stereoEventParsers.ParseFetchImage(event)
|
_, prog, err := stereoEventParsers.ParseFetchImage(event)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -217,6 +223,7 @@ func FetchImageHandler(ctx context.Context, fr *frame.Frame, event partybus.Even
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ReadImageHandler periodically writes a the image read/parse/build-tree status in the form of a progress bar.
|
||||||
func ReadImageHandler(ctx context.Context, fr *frame.Frame, event partybus.Event, wg *sync.WaitGroup) error {
|
func ReadImageHandler(ctx context.Context, fr *frame.Frame, event partybus.Event, wg *sync.WaitGroup) error {
|
||||||
_, prog, err := stereoEventParsers.ParseReadImage(event)
|
_, prog, err := stereoEventParsers.ParseReadImage(event)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -260,6 +267,7 @@ func ReadImageHandler(ctx context.Context, fr *frame.Frame, event partybus.Event
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CatalogerStartedHandler periodically writes catalog statistics to a single line.
|
||||||
func CatalogerStartedHandler(ctx context.Context, fr *frame.Frame, event partybus.Event, wg *sync.WaitGroup) error {
|
func CatalogerStartedHandler(ctx context.Context, fr *frame.Frame, event partybus.Event, wg *sync.WaitGroup) error {
|
||||||
monitor, err := syftEventParsers.ParseCatalogerStarted(event)
|
monitor, err := syftEventParsers.ParseCatalogerStarted(event)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@ -1,3 +1,8 @@
|
|||||||
|
/*
|
||||||
|
Package ui provides all public UI elements intended to be repurposed in other applications. Specifically, a single
|
||||||
|
Handler object is provided to allow consuming applications (such as grype) to check if there are UI elements the handler
|
||||||
|
can respond to (given a specific event type) and handle the event in context of the given screen frame object.
|
||||||
|
*/
|
||||||
package ui
|
package ui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -10,13 +15,16 @@ import (
|
|||||||
"github.com/wagoodman/jotframe/pkg/frame"
|
"github.com/wagoodman/jotframe/pkg/frame"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Handler is an aggregated event handler for the set of supported events (PullDockerImage, ReadImage, FetchImage, CatalogerStarted)
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewHandler returns an empty Handler
|
||||||
func NewHandler() *Handler {
|
func NewHandler() *Handler {
|
||||||
return &Handler{}
|
return &Handler{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RespondsTo indicates if the handler is capable of handling the given event.
|
||||||
func (r *Handler) RespondsTo(event partybus.Event) bool {
|
func (r *Handler) RespondsTo(event partybus.Event) bool {
|
||||||
switch event.Type {
|
switch event.Type {
|
||||||
case stereoscopeEvent.PullDockerImage, stereoscopeEvent.ReadImage, stereoscopeEvent.FetchImage, syftEvent.CatalogerStarted:
|
case stereoscopeEvent.PullDockerImage, stereoscopeEvent.ReadImage, stereoscopeEvent.FetchImage, syftEvent.CatalogerStarted:
|
||||||
@ -26,6 +34,7 @@ func (r *Handler) RespondsTo(event partybus.Event) bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle calls the specific event handler for the given event within the context of the screen frame.
|
||||||
func (r *Handler) Handle(ctx context.Context, fr *frame.Frame, event partybus.Event, wg *sync.WaitGroup) error {
|
func (r *Handler) Handle(ctx context.Context, fr *frame.Frame, event partybus.Event, wg *sync.WaitGroup) error {
|
||||||
switch event.Type {
|
switch event.Type {
|
||||||
case stereoscopeEvent.PullDockerImage:
|
case stereoscopeEvent.PullDockerImage:
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user