mirror of
https://github.com/anchore/syft.git
synced 2026-11-01 23:16:30 +01:00
Add experimental cataloger capabilities command (#4317)
* add info command from generated capabilities Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * correct gentoo and arch ecosystems Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * rename os pkg types Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * better binary cataloger description Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * expose metadata and pacakge types in json Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * expose json schema types Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * add completeness tests for metadata types Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * latest generation Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * fix linting Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * improve testing a docs Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * fix tests and linting Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * restore goreleaser config Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * tweak diagram Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * fix pdm Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * chore: java binary data Signed-off-by: Keith Zantow <kzantow@gmail.com> * new capability descriptions for gguf and python Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * correct poetry lock integrity hash claim Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * fix compile error Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * fix: remove purl version from overrides Signed-off-by: Keith Zantow <kzantow@gmail.com> * fix lua deps ref Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * keep gguf as ai ecosystem Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * split packages.yaml to multiple files by go package Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * ensure tests do not use go test cache Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * sort json output for info command Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * docs: fix ocaml, php, and portage capabilities yaml Signed-off-by: Will Murphy <willmurphyscode@users.noreply.github.com> * chore: update erlang capabilities Signed-off-by: Keith Zantow <kzantow@gmail.com> * chore: update java capabilities Signed-off-by: Keith Zantow <kzantow@gmail.com> * chore: update javascript capabilities Signed-off-by: Keith Zantow <kzantow@gmail.com> * chore: update linux kernel capabilities Signed-off-by: Keith Zantow <kzantow@gmail.com> * remove missing tests Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * fix package.yaml references Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * revert license list change Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * check for drift in capability descriptions Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * regenerate capabilities Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * test cleanup Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * use fixture cache in static analysis Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * claim fixtures pre-req for cap generation Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * update documentation with correct regeneration procedure Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * chore: ruby-gemspec-cataloger finds no dependencies Signed-off-by: Will Murphy <willmurphyscode@users.noreply.github.com> * chore: fix python docs and config comment Signed-off-by: Will Murphy <willmurphyscode@users.noreply.github.com> * chore: commit re-generated java yaml Signed-off-by: Will Murphy <willmurphyscode@users.noreply.github.com> * add cataloger selection to caps command Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * re-generate cap yamls Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * fix tests for cataloger selection Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * fix cli test Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * add missing tests Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * fix linting Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * rename cmd to `cataloger info` Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * [wip] change capability description locations Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * [wip] continued Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * [wip] adjust for import cycles Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * correct docs Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * fix linting Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> --------- Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> Signed-off-by: Keith Zantow <kzantow@gmail.com> Signed-off-by: Will Murphy <willmurphyscode@users.noreply.github.com> Co-authored-by: Keith Zantow <kzantow@gmail.com> Co-authored-by: Will Murphy <willmurphyscode@users.noreply.github.com>
This commit is contained in:
co-authored by
Keith Zantow
Will Murphy
parent
ae1a247f3d
commit
b3c70da3ea
@@ -35,6 +35,7 @@ const (
|
||||
attestSchemeHelp = "\n " + schemeHelpHeader + "\n" + imageSchemeHelp
|
||||
attestHelp = attestExample + attestSchemeHelp
|
||||
cosignBinName = "cosign"
|
||||
jsonFormat = "json"
|
||||
)
|
||||
|
||||
type attestOptions struct {
|
||||
@@ -93,9 +94,7 @@ func defaultAttestOutputOptions() options.Output {
|
||||
string(spdxtagvalue.ID),
|
||||
},
|
||||
Outputs: []string{syftjson.ID.String()},
|
||||
OutputFile: options.OutputFile{ //nolint:staticcheck
|
||||
Enabled: false, // explicitly not allowed
|
||||
},
|
||||
// OutputFile is omitted (defaults to Enabled: false, explicitly not allowed for attestation)
|
||||
Format: options.DefaultFormat(),
|
||||
}
|
||||
}
|
||||
@@ -240,7 +239,7 @@ func predicateType(outputName string) string {
|
||||
return "cyclonedx"
|
||||
case "spdx-tag-value", "spdx-tv":
|
||||
return "spdx"
|
||||
case "spdx-json", "json":
|
||||
case "spdx-json", jsonFormat:
|
||||
return "spdxjson"
|
||||
default:
|
||||
return "custom"
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/anchore/clio"
|
||||
@@ -16,5 +18,14 @@ func Cataloger(app clio.Application) *cobra.Command {
|
||||
CatalogerList(app),
|
||||
)
|
||||
|
||||
// only add cataloger info command if experimental capabilities feature is enabled
|
||||
if isCapabilitiesExperimentEnabled() {
|
||||
cmd.AddCommand(CatalogerCaps(app))
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func isCapabilitiesExperimentEnabled() bool {
|
||||
return os.Getenv("SYFT_EXP_CAPABILITIES") == "true"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,589 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/olekukonko/tablewriter/renderer"
|
||||
"github.com/olekukonko/tablewriter/tw"
|
||||
"github.com/scylladb/go-set/strset"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/anchore/clio"
|
||||
"github.com/anchore/syft/cmd/syft/internal/options"
|
||||
"github.com/anchore/syft/internal/bus"
|
||||
"github.com/anchore/syft/internal/capabilities"
|
||||
"github.com/anchore/syft/internal/task"
|
||||
"github.com/anchore/syft/syft/cataloging"
|
||||
_ "github.com/anchore/syft/syft/pkg/cataloger" // register all cataloger capabilities, otherwise the info command will not function
|
||||
)
|
||||
|
||||
var (
|
||||
yesStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("10")) // hi green
|
||||
noStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("8")) // dark grey
|
||||
criteriaWidth = 60
|
||||
criteriaMargin = 10
|
||||
)
|
||||
|
||||
// types for JSON cataloger info output
|
||||
type (
|
||||
configFieldInfo struct {
|
||||
Key string `json:"key"`
|
||||
Description string `json:"description"`
|
||||
AppKey string `json:"app_key,omitempty"`
|
||||
}
|
||||
|
||||
configInfo struct {
|
||||
Type string `json:"type"`
|
||||
Fields []configFieldInfo `json:"fields,omitempty"`
|
||||
}
|
||||
|
||||
detectorPackageInfo struct {
|
||||
Class string `json:"class"`
|
||||
Name string `json:"name"`
|
||||
PURL string `json:"purl"`
|
||||
CPEs []string `json:"cpes"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
patternInfo struct {
|
||||
Method string `json:"method"`
|
||||
Criteria []string `json:"criteria"`
|
||||
Conditions []capabilities.DetectorCondition `json:"conditions,omitempty"`
|
||||
Packages []detectorPackageInfo `json:"packages,omitempty"`
|
||||
Comment string `json:"comment,omitempty"`
|
||||
PackageTypes []string `json:"package_types,omitempty"`
|
||||
JSONSchemaTypes []string `json:"json_schema_types,omitempty"`
|
||||
Capabilities capabilities.CapabilitySet `json:"capabilities,omitempty"`
|
||||
}
|
||||
|
||||
catalogerInfo struct {
|
||||
Ecosystem string `json:"ecosystem,omitempty"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Selectors []string `json:"selectors,omitempty"`
|
||||
Deprecated bool `json:"deprecated,omitempty"`
|
||||
Patterns []patternInfo `json:"patterns,omitempty"`
|
||||
Capabilities capabilities.CapabilitySet `json:"capabilities,omitempty"`
|
||||
Config *configInfo `json:"config,omitempty"`
|
||||
}
|
||||
)
|
||||
|
||||
type catalogerInfoOptions struct {
|
||||
Output string `yaml:"output" json:"output" mapstructure:"output"`
|
||||
options.CatalogerSelection `yaml:",inline" json:",inline" mapstructure:",squash"`
|
||||
Names []string // cataloger names from args
|
||||
}
|
||||
|
||||
func (o *catalogerInfoOptions) setNames(args []string) error {
|
||||
o.Names = args
|
||||
|
||||
usingLegacyCatalogers := len(o.Catalogers) > 0
|
||||
usingNewCatalogers := len(o.DefaultCatalogers) > 0 || len(o.SelectCatalogers) > 0
|
||||
usingSelection := usingNewCatalogers || usingLegacyCatalogers
|
||||
|
||||
if usingSelection && len(o.Names) > 0 {
|
||||
return fmt.Errorf("cannot use both cataloger name arguments and '--catalogers'/'--select-catalogers'/'--default-catalogers' flags")
|
||||
}
|
||||
|
||||
if usingSelection {
|
||||
// get all available package cataloger tasks
|
||||
pkgTaskFactories := task.DefaultPackageTaskFactories()
|
||||
allPkgTasks, err := pkgTaskFactories.Tasks(task.DefaultCatalogingFactoryConfig())
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create pkg cataloger tasks: %w", err)
|
||||
}
|
||||
|
||||
// make the selection based on user input
|
||||
defaultCatalogers := options.FlattenAndSort(o.DefaultCatalogers)
|
||||
selectCatalogers := options.FlattenAndSort(o.SelectCatalogers)
|
||||
selectedTaskGroups, _, err := task.SelectInGroups(
|
||||
[][]task.Task{allPkgTasks},
|
||||
cataloging.NewSelectionRequest().
|
||||
WithDefaults(defaultCatalogers...).
|
||||
WithExpression(selectCatalogers...),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to select catalogers: %w", err)
|
||||
}
|
||||
|
||||
// build the list of cataloger names based on the selection
|
||||
for _, g := range selectedTaskGroups {
|
||||
for _, t := range g {
|
||||
o.Names = append(o.Names, t.Name())
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *catalogerInfoOptions) AddFlags(flags clio.FlagSet) {
|
||||
flags.StringVarP(&o.Output, "output", "o", "format to output the cataloger info (available: table, json)")
|
||||
}
|
||||
|
||||
func defaultCatalogerCapsOptions() *catalogerInfoOptions {
|
||||
return &catalogerInfoOptions{
|
||||
CatalogerSelection: options.CatalogerSelection{
|
||||
// this is different than the default behavior where a scan will automatically detect the default set
|
||||
DefaultCatalogers: []string{"all"},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func CatalogerCaps(app clio.Application) *cobra.Command {
|
||||
opts := defaultCatalogerCapsOptions()
|
||||
|
||||
return app.SetupCommand(&cobra.Command{
|
||||
Use: "info [OPTIONS] [CATALOGER_NAMES...]",
|
||||
Short: "Show detailed capabilities of catalogers",
|
||||
Args: cobra.ArbitraryArgs,
|
||||
PreRunE: disableUI(app, os.Stdout),
|
||||
RunE: func(_ *cobra.Command, args []string) error {
|
||||
if err := opts.setNames(args); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return runCatalogerInfo(opts)
|
||||
},
|
||||
}, opts)
|
||||
}
|
||||
|
||||
func runCatalogerInfo(opts *catalogerInfoOptions) error {
|
||||
doc, err := capabilities.LoadDocument()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to load cataloger capabilities: %w", err)
|
||||
}
|
||||
|
||||
// filter by cataloger names if provided
|
||||
catalogers := doc.Catalogers
|
||||
if len(opts.Names) > 0 {
|
||||
catalogers = filterCatalogersByName(catalogers, opts.Names)
|
||||
}
|
||||
|
||||
report, err := catalogerInfoReport(opts, doc, catalogers)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to generate cataloger info report: %w", err)
|
||||
}
|
||||
|
||||
bus.Report(report)
|
||||
bus.Notify("Note: the `cataloger info` command is experimental and may change or be removed without notice. Do not depend on its output in production systems.")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func filterCatalogersByName(catalogers []capabilities.CatalogerEntry, names []string) []capabilities.CatalogerEntry {
|
||||
nameSet := make(map[string]bool)
|
||||
for _, name := range names {
|
||||
nameSet[name] = true
|
||||
}
|
||||
|
||||
var filtered []capabilities.CatalogerEntry
|
||||
for _, cat := range catalogers {
|
||||
if nameSet[cat.Name] {
|
||||
filtered = append(filtered, cat)
|
||||
}
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
|
||||
func catalogerInfoReport(opts *catalogerInfoOptions, doc *capabilities.Document, catalogers []capabilities.CatalogerEntry) (string, error) {
|
||||
switch opts.Output {
|
||||
case jsonFormat:
|
||||
return renderCatalogerInfoJSON(doc, catalogers)
|
||||
case "table", "":
|
||||
return renderCatalogerInfoTable(doc, catalogers), nil
|
||||
default:
|
||||
return "", fmt.Errorf("invalid output format %q, must be one of: table, json", opts.Output)
|
||||
}
|
||||
}
|
||||
|
||||
func renderCatalogerInfoJSON(doc *capabilities.Document, catalogers []capabilities.CatalogerEntry) (string, error) {
|
||||
type document struct {
|
||||
Catalogers []catalogerInfo `json:"catalogers"`
|
||||
}
|
||||
|
||||
docOut := document{}
|
||||
|
||||
// sort catalogers by ecosystem then name
|
||||
sort.Slice(catalogers, func(i, j int) bool {
|
||||
if catalogers[i].Ecosystem != catalogers[j].Ecosystem {
|
||||
return catalogers[i].Ecosystem < catalogers[j].Ecosystem
|
||||
}
|
||||
return catalogers[i].Name < catalogers[j].Name
|
||||
})
|
||||
|
||||
for _, cat := range catalogers {
|
||||
info := catalogerInfo{
|
||||
Ecosystem: cat.Ecosystem,
|
||||
Name: cat.Name,
|
||||
Type: cat.Type,
|
||||
Selectors: cat.Selectors,
|
||||
Deprecated: isDeprecatedCataloger(cat.Selectors),
|
||||
}
|
||||
|
||||
// convert parsers to patterns if available
|
||||
info.Patterns = convertParsersToPatterns(cat.Parsers)
|
||||
|
||||
// if no parsers, use detectors instead
|
||||
if len(info.Patterns) == 0 {
|
||||
info.Capabilities = cat.Capabilities
|
||||
info.Patterns = convertDetectorsToPatterns(cat.Detectors, cat.PackageTypes, cat.JSONSchemaTypes)
|
||||
}
|
||||
|
||||
info.Config = getConfigInfoFromDocument(doc, cat.Config)
|
||||
|
||||
docOut.Catalogers = append(docOut.Catalogers, info)
|
||||
}
|
||||
|
||||
by, err := json.Marshal(docOut)
|
||||
return string(by), err
|
||||
}
|
||||
|
||||
// isDeprecatedCataloger checks if a cataloger is deprecated based on its selectors
|
||||
func isDeprecatedCataloger(selectors []string) bool {
|
||||
for _, selector := range selectors {
|
||||
if selector == "deprecated" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// convertDetectorPackages converts detector package info to the JSON output format
|
||||
func convertDetectorPackages(pkgs []capabilities.DetectorPackageInfo) []detectorPackageInfo {
|
||||
var result []detectorPackageInfo
|
||||
for _, pkg := range pkgs {
|
||||
result = append(result, detectorPackageInfo{
|
||||
Class: pkg.Class,
|
||||
Name: pkg.Name,
|
||||
PURL: pkg.PURL,
|
||||
CPEs: pkg.CPEs,
|
||||
Type: pkg.Type,
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// convertParsersToPatterns converts parser entries to pattern info for JSON output
|
||||
func convertParsersToPatterns(parsers []capabilities.Parser) []patternInfo {
|
||||
var patterns []patternInfo
|
||||
for _, parser := range parsers {
|
||||
patterns = append(patterns, patternInfo{
|
||||
Method: string(parser.Detector.Method),
|
||||
Criteria: parser.Detector.Criteria,
|
||||
Conditions: parser.Detector.Conditions,
|
||||
Packages: convertDetectorPackages(parser.Detector.Packages),
|
||||
Comment: parser.Detector.Comment,
|
||||
PackageTypes: parser.PackageTypes,
|
||||
JSONSchemaTypes: parser.JSONSchemaTypes,
|
||||
Capabilities: parser.Capabilities,
|
||||
})
|
||||
}
|
||||
return patterns
|
||||
}
|
||||
|
||||
// convertDetectorsToPatterns converts detector entries to pattern info for JSON output (for non-parser catalogers)
|
||||
func convertDetectorsToPatterns(detectors []capabilities.Detector, packageTypes, jsonSchemaTypes []string) []patternInfo {
|
||||
var patterns []patternInfo
|
||||
for _, det := range detectors {
|
||||
patterns = append(patterns, patternInfo{
|
||||
Method: string(det.Method),
|
||||
Criteria: det.Criteria,
|
||||
Conditions: det.Conditions,
|
||||
Packages: convertDetectorPackages(det.Packages),
|
||||
Comment: det.Comment,
|
||||
PackageTypes: packageTypes,
|
||||
JSONSchemaTypes: jsonSchemaTypes,
|
||||
})
|
||||
}
|
||||
return patterns
|
||||
}
|
||||
|
||||
// getConfigInfoFromDocument retrieves config info from the capabilities document
|
||||
func getConfigInfoFromDocument(doc *capabilities.Document, configType string) *configInfo {
|
||||
if configType == "" {
|
||||
return nil
|
||||
}
|
||||
configEntry, ok := doc.Configs[configType]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
cfg := &configInfo{
|
||||
Type: configType,
|
||||
}
|
||||
for _, field := range configEntry.Fields {
|
||||
cfg.Fields = append(cfg.Fields, configFieldInfo{
|
||||
Key: field.Key,
|
||||
Description: field.Description,
|
||||
AppKey: field.AppKey,
|
||||
})
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
func renderCatalogerInfoTable(_ *capabilities.Document, catalogers []capabilities.CatalogerEntry) string {
|
||||
if len(catalogers) == 0 {
|
||||
return noStyle.Render("No catalogers found")
|
||||
}
|
||||
|
||||
// sort catalogers by ecosystem then name
|
||||
sort.Slice(catalogers, func(i, j int) bool {
|
||||
if catalogers[i].Ecosystem != catalogers[j].Ecosystem {
|
||||
return catalogers[i].Ecosystem < catalogers[j].Ecosystem
|
||||
}
|
||||
return catalogers[i].Name < catalogers[j].Name
|
||||
})
|
||||
|
||||
var buf bytes.Buffer
|
||||
|
||||
// create table with hierarchical merging
|
||||
table := tablewriter.NewTable(&buf,
|
||||
tablewriter.WithRenderer(renderer.NewBlueprint(tw.Rendition{
|
||||
Settings: tw.Settings{Separators: tw.Separators{BetweenRows: tw.On}},
|
||||
})),
|
||||
tablewriter.WithConfig(tablewriter.Config{
|
||||
Row: tw.CellConfig{
|
||||
Formatting: tw.CellFormatting{MergeMode: tw.MergeHierarchical},
|
||||
Alignment: tw.CellAlignment{Global: tw.AlignLeft},
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
// set headers
|
||||
table.Header("ECOSYSTEM", "CATALOGER", "CRITERIA", "LICENSE", "NODES", "EDGES", "KINDS", "LISTING", "DIGESTS", "HASH")
|
||||
|
||||
// build rows for each cataloger
|
||||
var data [][]string
|
||||
for _, cat := range catalogers {
|
||||
ecosystem := cat.Ecosystem
|
||||
if ecosystem == "" {
|
||||
ecosystem = "other"
|
||||
}
|
||||
|
||||
if cat.Type == "generic" {
|
||||
// generic catalogers: one row per parser
|
||||
for _, parser := range cat.Parsers {
|
||||
criteria := formatCriteria([]capabilities.Detector{parser.Detector})
|
||||
row := buildTableRowFromCapabilities(ecosystem, cat.Name, criteria, parser.Capabilities)
|
||||
data = append(data, row)
|
||||
}
|
||||
} else {
|
||||
// custom catalogers: one row with all detectors
|
||||
criteria := formatCriteria(cat.Detectors)
|
||||
row := buildTableRowFromCapabilities(ecosystem, cat.Name, criteria, cat.Capabilities)
|
||||
data = append(data, row)
|
||||
}
|
||||
}
|
||||
|
||||
// add all rows at once for better merging
|
||||
_ = table.Bulk(data)
|
||||
_ = table.Render()
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// buildTableRowFromCapabilities builds a table row from capability values
|
||||
func buildTableRowFromCapabilities(ecosystem, name, criteria string, caps capabilities.CapabilitySet) []string {
|
||||
// extract capability default values
|
||||
license := extractBoolCapability(caps, "license")
|
||||
nodes := extractNodesCapability(caps)
|
||||
edges := extractStringCapability(caps, "dependency.edges")
|
||||
kinds := extractArrayCapability(caps, "dependency.kinds")
|
||||
listing := extractBoolCapability(caps, "package_manager.files.listing")
|
||||
digests := extractBoolCapability(caps, "package_manager.files.digests")
|
||||
hash := extractBoolCapability(caps, "package_manager.package_integrity_hash")
|
||||
|
||||
return []string{
|
||||
ecosystem,
|
||||
name,
|
||||
criteria,
|
||||
license,
|
||||
nodes,
|
||||
edges,
|
||||
kinds,
|
||||
listing,
|
||||
digests,
|
||||
hash,
|
||||
}
|
||||
}
|
||||
|
||||
// extractBoolCapability extracts a boolean capability value and formats it
|
||||
func extractBoolCapability(caps capabilities.CapabilitySet, name string) string {
|
||||
for _, cap := range caps {
|
||||
if cap.Name == name {
|
||||
if b, ok := cap.Default.(bool); ok {
|
||||
if b {
|
||||
return yesStyle.Render("✔")
|
||||
}
|
||||
return noStyle.Render("·")
|
||||
}
|
||||
return noStyle.Render("·")
|
||||
}
|
||||
}
|
||||
return noStyle.Render("·")
|
||||
}
|
||||
|
||||
// extractStringCapability extracts a string capability value
|
||||
func extractStringCapability(caps capabilities.CapabilitySet, name string) string {
|
||||
for _, cap := range caps {
|
||||
if cap.Name == name {
|
||||
if s, ok := cap.Default.(string); ok && s != "" {
|
||||
return s
|
||||
}
|
||||
return noStyle.Render("·")
|
||||
}
|
||||
}
|
||||
return noStyle.Render("·")
|
||||
}
|
||||
|
||||
// extractArrayCapability extracts an array capability value and formats it as comma-separated
|
||||
func extractArrayCapability(caps capabilities.CapabilitySet, name string) string {
|
||||
for _, cap := range caps {
|
||||
if cap.Name == name {
|
||||
// handle various array types
|
||||
switch v := cap.Default.(type) {
|
||||
case []string:
|
||||
if len(v) > 0 {
|
||||
return strings.Join(v, ", ")
|
||||
}
|
||||
case []interface{}:
|
||||
if len(v) > 0 {
|
||||
strs := make([]string, 0, len(v))
|
||||
for _, item := range v {
|
||||
strs = append(strs, fmt.Sprintf("%v", item))
|
||||
}
|
||||
return strings.Join(strs, ", ")
|
||||
}
|
||||
}
|
||||
return noStyle.Render("·")
|
||||
}
|
||||
}
|
||||
return noStyle.Render("·")
|
||||
}
|
||||
|
||||
// extractNodesCapability extracts dependency.depth and formats it
|
||||
// maps ["direct", "indirect"] to "transitive"
|
||||
func extractNodesCapability(caps capabilities.CapabilitySet) string {
|
||||
for _, cap := range caps {
|
||||
if cap.Name == "dependency.depth" {
|
||||
switch v := cap.Default.(type) {
|
||||
case []string:
|
||||
return formatDepthStringArray(v)
|
||||
case []interface{}:
|
||||
return formatDepthInterfaceArray(v)
|
||||
}
|
||||
return noStyle.Render("·")
|
||||
}
|
||||
}
|
||||
return noStyle.Render("·")
|
||||
}
|
||||
|
||||
// formatDepthStringArray formats a []string dependency depth value
|
||||
func formatDepthStringArray(v []string) string {
|
||||
if len(v) == 0 {
|
||||
return noStyle.Render("·")
|
||||
}
|
||||
if hasBothDirectAndIndirect(v) {
|
||||
return "transitive"
|
||||
}
|
||||
return strings.Join(v, ", ")
|
||||
}
|
||||
|
||||
// formatDepthInterfaceArray formats a []interface{} dependency depth value
|
||||
func formatDepthInterfaceArray(v []interface{}) string {
|
||||
if len(v) == 0 {
|
||||
return noStyle.Render("·")
|
||||
}
|
||||
strs := make([]string, 0, len(v))
|
||||
for _, item := range v {
|
||||
strs = append(strs, fmt.Sprintf("%v", item))
|
||||
}
|
||||
if hasBothDirectAndIndirect(strs) {
|
||||
return "transitive"
|
||||
}
|
||||
return strings.Join(strs, ", ")
|
||||
}
|
||||
|
||||
// hasBothDirectAndIndirect checks if a slice contains both "direct" and "indirect" strings
|
||||
func hasBothDirectAndIndirect(items []string) bool {
|
||||
hasDirect := false
|
||||
hasIndirect := false
|
||||
for _, item := range items {
|
||||
if item == "direct" {
|
||||
hasDirect = true
|
||||
}
|
||||
if item == "indirect" {
|
||||
hasIndirect = true
|
||||
}
|
||||
}
|
||||
return hasDirect && hasIndirect
|
||||
}
|
||||
|
||||
func formatCriteria(detectors []capabilities.Detector) string {
|
||||
var allCriteria []string
|
||||
methods := strset.New()
|
||||
|
||||
for _, det := range detectors {
|
||||
allCriteria = append(allCriteria, det.Criteria...)
|
||||
methods.Add(string(det.Method))
|
||||
}
|
||||
|
||||
// smart word wrapping: wrap by word (criterion) up to criteriaWidth,
|
||||
// allow up to criteriaWidth+criteriaMargin before forcing a new line
|
||||
var lines []string
|
||||
var currentLine []string
|
||||
currentLength := 0
|
||||
|
||||
for _, criterion := range allCriteria {
|
||||
// calculate length including the criterion plus ", " separator (except for first item)
|
||||
itemLength := len(criterion)
|
||||
if len(currentLine) > 0 {
|
||||
itemLength += 2 // for ", "
|
||||
}
|
||||
|
||||
newLength := currentLength + itemLength
|
||||
|
||||
if len(currentLine) > 0 && newLength > criteriaWidth {
|
||||
// check if it's within margin
|
||||
if newLength <= criteriaWidth+criteriaMargin {
|
||||
// close enough, add it anyway
|
||||
currentLine = append(currentLine, criterion)
|
||||
currentLength = newLength
|
||||
} else {
|
||||
// too long, finalize current line and start new one
|
||||
lines = append(lines, strings.Join(currentLine, ", "))
|
||||
currentLine = []string{criterion}
|
||||
currentLength = len(criterion)
|
||||
}
|
||||
} else {
|
||||
// fits within width or first item
|
||||
currentLine = append(currentLine, criterion)
|
||||
currentLength = newLength
|
||||
}
|
||||
}
|
||||
|
||||
// add final line
|
||||
if len(currentLine) > 0 {
|
||||
lines = append(lines, strings.Join(currentLine, ", "))
|
||||
}
|
||||
|
||||
methodsList := methods.List()
|
||||
sort.Strings(methodsList)
|
||||
method := strings.Join(methodsList, ", ")
|
||||
|
||||
if len(lines) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
joined := strings.Join(lines, "\n")
|
||||
if method != string(capabilities.GlobDetection) {
|
||||
return fmt.Sprintf("%s (%s)", joined, method)
|
||||
}
|
||||
return joined
|
||||
}
|
||||
@@ -0,0 +1,373 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/anchore/syft/internal/capabilities"
|
||||
)
|
||||
|
||||
func Test_isDeprecatedCataloger(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
selectors []string
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "empty selectors",
|
||||
selectors: nil,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "no deprecated selector",
|
||||
selectors: []string{"python", "pip", "package"},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "has deprecated selector",
|
||||
selectors: []string{"python", "deprecated", "package"},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "only deprecated selector",
|
||||
selectors: []string{"deprecated"},
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := isDeprecatedCataloger(tt.selectors)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_hasBothDirectAndIndirect(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
items []string
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "empty slice",
|
||||
items: nil,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "only direct",
|
||||
items: []string{"direct"},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "only indirect",
|
||||
items: []string{"indirect"},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "both direct and indirect",
|
||||
items: []string{"direct", "indirect"},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "both with other items",
|
||||
items: []string{"other", "direct", "more", "indirect", "stuff"},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "unrelated items",
|
||||
items: []string{"foo", "bar", "baz"},
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := hasBothDirectAndIndirect(tt.items)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_filterCatalogersByName(t *testing.T) {
|
||||
catalogers := []capabilities.CatalogerEntry{
|
||||
{Name: "python-cataloger", Ecosystem: "python"},
|
||||
{Name: "java-cataloger", Ecosystem: "java"},
|
||||
{Name: "go-cataloger", Ecosystem: "golang"},
|
||||
{Name: "npm-cataloger", Ecosystem: "javascript"},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
catalogers []capabilities.CatalogerEntry
|
||||
names []string
|
||||
want []capabilities.CatalogerEntry
|
||||
}{
|
||||
{
|
||||
name: "empty names returns empty",
|
||||
catalogers: catalogers,
|
||||
names: nil,
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "single match",
|
||||
catalogers: catalogers,
|
||||
names: []string{"python-cataloger"},
|
||||
want: []capabilities.CatalogerEntry{
|
||||
{Name: "python-cataloger", Ecosystem: "python"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multiple matches",
|
||||
catalogers: catalogers,
|
||||
names: []string{"python-cataloger", "java-cataloger"},
|
||||
want: []capabilities.CatalogerEntry{
|
||||
{Name: "python-cataloger", Ecosystem: "python"},
|
||||
{Name: "java-cataloger", Ecosystem: "java"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no matches",
|
||||
catalogers: catalogers,
|
||||
names: []string{"nonexistent"},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "partial matches",
|
||||
catalogers: catalogers,
|
||||
names: []string{"python-cataloger", "nonexistent"},
|
||||
want: []capabilities.CatalogerEntry{
|
||||
{Name: "python-cataloger", Ecosystem: "python"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := filterCatalogersByName(tt.catalogers, tt.names)
|
||||
if diff := cmp.Diff(tt.want, got); diff != "" {
|
||||
t.Errorf("filterCatalogersByName() mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_formatCriteria(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
detectors []capabilities.Detector
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "empty detectors",
|
||||
detectors: nil,
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "single glob detector",
|
||||
detectors: []capabilities.Detector{
|
||||
{
|
||||
Method: capabilities.GlobDetection,
|
||||
Criteria: []string{"**/*.py"},
|
||||
},
|
||||
},
|
||||
want: "**/*.py",
|
||||
},
|
||||
{
|
||||
name: "multiple criteria single detector",
|
||||
detectors: []capabilities.Detector{
|
||||
{
|
||||
Method: capabilities.GlobDetection,
|
||||
Criteria: []string{"**/*.py", "**/*.pyc", "**/requirements.txt"},
|
||||
},
|
||||
},
|
||||
want: "**/*.py, **/*.pyc, **/requirements.txt",
|
||||
},
|
||||
{
|
||||
name: "path detection shows method",
|
||||
detectors: []capabilities.Detector{
|
||||
{
|
||||
Method: capabilities.PathDetection,
|
||||
Criteria: []string{"/usr/bin/python"},
|
||||
},
|
||||
},
|
||||
want: "/usr/bin/python (path)",
|
||||
},
|
||||
{
|
||||
name: "mimetype detection shows method",
|
||||
detectors: []capabilities.Detector{
|
||||
{
|
||||
Method: capabilities.MIMETypeDetection,
|
||||
Criteria: []string{"application/x-python"},
|
||||
},
|
||||
},
|
||||
want: "application/x-python (mimetype)",
|
||||
},
|
||||
{
|
||||
name: "multiple detectors combine criteria",
|
||||
detectors: []capabilities.Detector{
|
||||
{
|
||||
Method: capabilities.GlobDetection,
|
||||
Criteria: []string{"**/*.py"},
|
||||
},
|
||||
{
|
||||
Method: capabilities.GlobDetection,
|
||||
Criteria: []string{"**/*.pyc"},
|
||||
},
|
||||
},
|
||||
want: "**/*.py, **/*.pyc",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := formatCriteria(tt.detectors)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func testDocument() *capabilities.Document {
|
||||
return &capabilities.Document{
|
||||
Configs: map[string]capabilities.CatalogerConfigEntry{
|
||||
"test-config": {
|
||||
Fields: []capabilities.CatalogerConfigFieldEntry{
|
||||
{Key: "field1", Description: "test field"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func Test_catalogerInfoReport(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
opts *catalogerInfoOptions
|
||||
doc *capabilities.Document
|
||||
catalogers []capabilities.CatalogerEntry
|
||||
wantErr require.ErrorAssertionFunc
|
||||
assertions func(t *testing.T, got string)
|
||||
}{
|
||||
{
|
||||
name: "empty catalogers table",
|
||||
opts: &catalogerInfoOptions{Output: "table"},
|
||||
doc: testDocument(),
|
||||
assertions: func(t *testing.T, got string) {
|
||||
assert.Contains(t, got, "No catalogers found")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "empty catalogers json",
|
||||
opts: &catalogerInfoOptions{Output: "json"},
|
||||
doc: testDocument(),
|
||||
assertions: func(t *testing.T, got string) {
|
||||
assert.Contains(t, got, `"catalogers":null`)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "single cataloger table output",
|
||||
opts: &catalogerInfoOptions{Output: "table"},
|
||||
doc: testDocument(),
|
||||
catalogers: []capabilities.CatalogerEntry{
|
||||
{
|
||||
Name: "test-cataloger",
|
||||
Ecosystem: "test",
|
||||
Type: "custom",
|
||||
Detectors: []capabilities.Detector{
|
||||
{Method: capabilities.GlobDetection, Criteria: []string{"**/*.test"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
assertions: func(t *testing.T, got string) {
|
||||
assert.Contains(t, got, "test-cataloger")
|
||||
assert.Contains(t, got, "ECOSYSTEM")
|
||||
assert.Contains(t, got, "CATALOGER")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "single cataloger json output",
|
||||
opts: &catalogerInfoOptions{Output: "json"},
|
||||
doc: testDocument(),
|
||||
catalogers: []capabilities.CatalogerEntry{
|
||||
{
|
||||
Name: "test-cataloger",
|
||||
Ecosystem: "test",
|
||||
Type: "custom",
|
||||
Selectors: []string{"test", "custom"},
|
||||
},
|
||||
},
|
||||
assertions: func(t *testing.T, got string) {
|
||||
assert.Contains(t, got, `"name":"test-cataloger"`)
|
||||
assert.Contains(t, got, `"ecosystem":"test"`)
|
||||
assert.Contains(t, got, `"type":"custom"`)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "deprecated cataloger json output",
|
||||
opts: &catalogerInfoOptions{Output: "json"},
|
||||
doc: testDocument(),
|
||||
catalogers: []capabilities.CatalogerEntry{
|
||||
{
|
||||
Name: "old-cataloger",
|
||||
Ecosystem: "legacy",
|
||||
Type: "custom",
|
||||
Selectors: []string{"legacy", "deprecated"},
|
||||
},
|
||||
},
|
||||
assertions: func(t *testing.T, got string) {
|
||||
assert.Contains(t, got, `"deprecated":true`)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid output format",
|
||||
opts: &catalogerInfoOptions{Output: "invalid"},
|
||||
doc: testDocument(),
|
||||
wantErr: require.Error,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.wantErr == nil {
|
||||
tt.wantErr = require.NoError
|
||||
}
|
||||
|
||||
got, err := catalogerInfoReport(tt.opts, tt.doc, tt.catalogers)
|
||||
tt.wantErr(t, err)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if tt.assertions != nil {
|
||||
tt.assertions(t, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_formatCriteria_wordWrapping(t *testing.T) {
|
||||
// test word wrapping behavior with long criteria lists
|
||||
longCriteria := make([]string, 20)
|
||||
for i := range longCriteria {
|
||||
longCriteria[i] = "**/file" + strings.Repeat("x", 10)
|
||||
}
|
||||
|
||||
detector := capabilities.Detector{
|
||||
Method: capabilities.GlobDetection,
|
||||
Criteria: longCriteria,
|
||||
}
|
||||
|
||||
got := formatCriteria([]capabilities.Detector{detector})
|
||||
|
||||
// should contain newlines for wrapping
|
||||
assert.Contains(t, got, "\n")
|
||||
// should contain all criteria (joined with ", " and possibly split across lines)
|
||||
for _, c := range longCriteria {
|
||||
assert.Contains(t, got, c)
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"github.com/anchore/clio"
|
||||
"github.com/anchore/syft/cmd/syft/internal/options"
|
||||
"github.com/anchore/syft/internal/bus"
|
||||
"github.com/anchore/syft/internal/capabilities"
|
||||
"github.com/anchore/syft/internal/task"
|
||||
"github.com/anchore/syft/syft/cataloging"
|
||||
)
|
||||
@@ -28,25 +29,23 @@ var (
|
||||
)
|
||||
|
||||
type catalogerListOptions struct {
|
||||
Output string `yaml:"output" json:"output" mapstructure:"output"`
|
||||
DefaultCatalogers []string `yaml:"default-catalogers" json:"default-catalogers" mapstructure:"default-catalogers"`
|
||||
SelectCatalogers []string `yaml:"select-catalogers" json:"select-catalogers" mapstructure:"select-catalogers"`
|
||||
ShowHidden bool `yaml:"show-hidden" json:"show-hidden" mapstructure:"show-hidden"`
|
||||
Output string `yaml:"output" json:"output" mapstructure:"output"`
|
||||
options.CatalogerSelection `yaml:",inline" json:",inline" mapstructure:",squash"`
|
||||
ShowHidden bool `yaml:"show-hidden" json:"show-hidden" mapstructure:"show-hidden"`
|
||||
}
|
||||
|
||||
func (o *catalogerListOptions) AddFlags(flags clio.FlagSet) {
|
||||
flags.StringVarP(&o.Output, "output", "o", "format to output the cataloger list (available: table, json)")
|
||||
|
||||
flags.StringArrayVarP(&o.DefaultCatalogers, "override-default-catalogers", "", "override the default catalogers with an expression")
|
||||
|
||||
flags.StringArrayVarP(&o.SelectCatalogers, "select-catalogers", "", "select catalogers with an expression")
|
||||
|
||||
flags.BoolVarP(&o.ShowHidden, "show-hidden", "s", "show catalogers that have been de-selected")
|
||||
}
|
||||
|
||||
func defaultCatalogerListOptions() *catalogerListOptions {
|
||||
return &catalogerListOptions{
|
||||
DefaultCatalogers: []string{"all"},
|
||||
CatalogerSelection: options.CatalogerSelection{
|
||||
// this is different than the default behavior where a scan will automatically detect the default set
|
||||
DefaultCatalogers: []string{"all"},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,7 +100,7 @@ func catalogerListReport(opts *catalogerListOptions, allTaskGroups [][]task.Task
|
||||
var report string
|
||||
|
||||
switch opts.Output {
|
||||
case "json":
|
||||
case jsonFormat:
|
||||
report, err = renderCatalogerListJSON(flattenTaskGroups(selectedTaskGroups), selectionEvidence, defaultCatalogers, selectCatalogers)
|
||||
case "table", "":
|
||||
if opts.ShowHidden {
|
||||
@@ -306,22 +305,14 @@ func formatToken(token string, selection *task.TokenSelection, included bool, de
|
||||
}
|
||||
|
||||
func extractTaskInfo(tasks []task.Task) ([]string, map[string][]string) {
|
||||
infos := capabilities.ExtractCatalogerInfo(tasks)
|
||||
|
||||
tagsByName := make(map[string][]string)
|
||||
var names []string
|
||||
|
||||
for _, tsk := range tasks {
|
||||
var tags []string
|
||||
name := tsk.Name()
|
||||
|
||||
if s, ok := tsk.(task.Selector); ok {
|
||||
set := strset.New(s.Selectors()...)
|
||||
set.Remove(name)
|
||||
tags = set.List()
|
||||
sort.Strings(tags)
|
||||
}
|
||||
|
||||
tagsByName[name] = tags
|
||||
names = append(names, name)
|
||||
for _, info := range infos {
|
||||
tagsByName[info.Name] = info.Selectors
|
||||
names = append(names, info.Name)
|
||||
}
|
||||
|
||||
sort.Strings(names)
|
||||
|
||||
@@ -31,17 +31,16 @@ import (
|
||||
|
||||
type Catalog struct {
|
||||
// high-level cataloger configuration
|
||||
Catalogers []string `yaml:"-" json:"catalogers" mapstructure:"catalogers"` // deprecated and not shown in yaml output
|
||||
DefaultCatalogers []string `yaml:"default-catalogers" json:"default-catalogers" mapstructure:"default-catalogers"`
|
||||
SelectCatalogers []string `yaml:"select-catalogers" json:"select-catalogers" mapstructure:"select-catalogers"`
|
||||
Package packageConfig `yaml:"package" json:"package" mapstructure:"package"`
|
||||
License licenseConfig `yaml:"license" json:"license" mapstructure:"license"`
|
||||
File fileConfig `yaml:"file" json:"file" mapstructure:"file"`
|
||||
Scope string `yaml:"scope" json:"scope" mapstructure:"scope"`
|
||||
Parallelism int `yaml:"parallelism" json:"parallelism" mapstructure:"parallelism"` // the number of catalog workers to run in parallel
|
||||
Relationships relationshipsConfig `yaml:"relationships" json:"relationships" mapstructure:"relationships"`
|
||||
Compliance complianceConfig `yaml:"compliance" json:"compliance" mapstructure:"compliance"`
|
||||
Enrich []string `yaml:"enrich" json:"enrich" mapstructure:"enrich"`
|
||||
CatalogerSelection `yaml:",inline" json:",inline" mapstructure:",squash"`
|
||||
|
||||
Package packageConfig `yaml:"package" json:"package" mapstructure:"package"`
|
||||
License licenseConfig `yaml:"license" json:"license" mapstructure:"license"`
|
||||
File fileConfig `yaml:"file" json:"file" mapstructure:"file"`
|
||||
Scope string `yaml:"scope" json:"scope" mapstructure:"scope"`
|
||||
Parallelism int `yaml:"parallelism" json:"parallelism" mapstructure:"parallelism"` // the number of catalog workers to run in parallel
|
||||
Relationships relationshipsConfig `yaml:"relationships" json:"relationships" mapstructure:"relationships"`
|
||||
Compliance complianceConfig `yaml:"compliance" json:"compliance" mapstructure:"compliance"`
|
||||
Enrich []string `yaml:"enrich" json:"enrich" mapstructure:"enrich"`
|
||||
|
||||
// ecosystem-specific cataloger configuration
|
||||
Dotnet dotnetConfig `yaml:"dotnet" json:"dotnet" mapstructure:"dotnet"`
|
||||
@@ -77,6 +76,8 @@ func DefaultCatalog() Catalog {
|
||||
Package: defaultPackageConfig(),
|
||||
License: defaultLicenseConfig(),
|
||||
LinuxKernel: defaultLinuxKernelConfig(),
|
||||
JavaScript: defaultJavaScriptConfig(),
|
||||
Python: defaultPythonConfig(),
|
||||
Nix: defaultNixConfig(),
|
||||
Dotnet: defaultDotnetConfig(),
|
||||
Golang: defaultGolangConfig(),
|
||||
@@ -230,26 +231,9 @@ func (cfg *Catalog) AddFlags(flags clio.FlagSet) {
|
||||
flags.StringArrayVarP(&cfg.Exclusions, "exclude", "",
|
||||
"exclude paths from being scanned using a glob expression")
|
||||
|
||||
flags.StringArrayVarP(&cfg.Catalogers, "catalogers", "",
|
||||
"enable one or more package catalogers")
|
||||
|
||||
flags.IntVarP(&cfg.Parallelism, "parallelism", "",
|
||||
"number of cataloger workers to run in parallel")
|
||||
|
||||
if pfp, ok := flags.(fangs.PFlagSetProvider); ok {
|
||||
if err := pfp.PFlagSet().MarkDeprecated("catalogers", "use: override-default-catalogers and select-catalogers"); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
} else {
|
||||
panic("unable to mark flags as deprecated")
|
||||
}
|
||||
|
||||
flags.StringArrayVarP(&cfg.DefaultCatalogers, "override-default-catalogers", "",
|
||||
"set the base set of catalogers to use (defaults to 'image' or 'directory' depending on the scan source)")
|
||||
|
||||
flags.StringArrayVarP(&cfg.SelectCatalogers, "select-catalogers", "",
|
||||
"add, remove, and filter the catalogers to be used")
|
||||
|
||||
flags.StringArrayVarP(&cfg.Enrich, "enrich", "",
|
||||
fmt.Sprintf("enable package data enrichment from local and online sources (options: %s)", strings.Join(publicisedEnrichmentOptions, ", ")))
|
||||
|
||||
@@ -276,23 +260,9 @@ Available options are: %s`, strings.Join(publicisedEnrichmentOptions, ", ")))
|
||||
}
|
||||
|
||||
func (cfg *Catalog) PostLoad() error {
|
||||
usingLegacyCatalogers := len(cfg.Catalogers) > 0
|
||||
usingNewCatalogers := len(cfg.DefaultCatalogers) > 0 || len(cfg.SelectCatalogers) > 0
|
||||
|
||||
if usingLegacyCatalogers && usingNewCatalogers {
|
||||
return fmt.Errorf("cannot use both 'catalogers' and 'select-catalogers'/'default-catalogers' flags")
|
||||
}
|
||||
|
||||
cfg.From = Flatten(cfg.From)
|
||||
|
||||
cfg.Catalogers = FlattenAndSort(cfg.Catalogers)
|
||||
cfg.DefaultCatalogers = FlattenAndSort(cfg.DefaultCatalogers)
|
||||
cfg.SelectCatalogers = FlattenAndSort(cfg.SelectCatalogers)
|
||||
cfg.Enrich = FlattenAndSort(cfg.Enrich)
|
||||
|
||||
// for backwards compatibility
|
||||
cfg.DefaultCatalogers = append(cfg.DefaultCatalogers, cfg.Catalogers...)
|
||||
|
||||
s := source.ParseScope(cfg.Scope)
|
||||
if s == source.UnknownScope {
|
||||
return fmt.Errorf("bad scope value %q", cfg.Scope)
|
||||
|
||||
@@ -8,55 +8,12 @@ import (
|
||||
)
|
||||
|
||||
func TestCatalog_PostLoad(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
options Catalog
|
||||
assert func(t *testing.T, options Catalog)
|
||||
wantErr assert.ErrorAssertionFunc
|
||||
}{
|
||||
{
|
||||
name: "mutually exclusive cataloger flags (cat / def-cat)",
|
||||
options: Catalog{
|
||||
Catalogers: []string{"foo,bar", "42"},
|
||||
DefaultCatalogers: []string{"some,thing"},
|
||||
Scope: "squashed",
|
||||
},
|
||||
wantErr: assert.Error,
|
||||
},
|
||||
{
|
||||
name: "mutually exclusive cataloger flags (cat / sel-cat)",
|
||||
options: Catalog{
|
||||
Catalogers: []string{"foo,bar", "42"},
|
||||
SelectCatalogers: []string{"some,thing"},
|
||||
Scope: "squashed",
|
||||
},
|
||||
wantErr: assert.Error,
|
||||
},
|
||||
{
|
||||
name: "allow old cataloger flags",
|
||||
options: Catalog{
|
||||
Catalogers: []string{"foo,bar"},
|
||||
Scope: "squashed",
|
||||
},
|
||||
assert: func(t *testing.T, options Catalog) {
|
||||
assert.Equal(t, []string{"bar", "foo"}, options.DefaultCatalogers) // note: sorted order
|
||||
assert.Equal(t, []string{"bar", "foo"}, options.Catalogers) // note: sorted order
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "allow new cataloger flags",
|
||||
options: Catalog{
|
||||
SelectCatalogers: []string{"foo,bar", "42"},
|
||||
DefaultCatalogers: []string{"some,thing"},
|
||||
Scope: "squashed",
|
||||
},
|
||||
assert: func(t *testing.T, options Catalog) {
|
||||
assert.Equal(t, []string{"42", "bar", "foo"}, options.SelectCatalogers) // note: sorted order
|
||||
assert.Equal(t, []string{"some", "thing"}, options.DefaultCatalogers) // note: sorted order
|
||||
assert.Empty(t, options.Catalogers)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "must have package overlap flag when pruning binaries by overlap",
|
||||
options: Catalog{
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
package options
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/anchore/clio"
|
||||
"github.com/anchore/fangs"
|
||||
)
|
||||
|
||||
type CatalogerSelection struct {
|
||||
// high-level cataloger configuration
|
||||
Catalogers []string `yaml:"-" json:"catalogers" mapstructure:"catalogers"` // deprecated and not shown in yaml output
|
||||
DefaultCatalogers []string `yaml:"default-catalogers" json:"default-catalogers" mapstructure:"default-catalogers"`
|
||||
SelectCatalogers []string `yaml:"select-catalogers" json:"select-catalogers" mapstructure:"select-catalogers"`
|
||||
}
|
||||
|
||||
var _ interface {
|
||||
clio.FlagAdder
|
||||
clio.PostLoader
|
||||
} = (*CatalogerSelection)(nil)
|
||||
|
||||
func (cfg *CatalogerSelection) AddFlags(flags clio.FlagSet) {
|
||||
flags.StringArrayVarP(&cfg.Catalogers, "catalogers", "",
|
||||
"enable one or more package catalogers")
|
||||
|
||||
if pfp, ok := flags.(fangs.PFlagSetProvider); ok {
|
||||
if err := pfp.PFlagSet().MarkDeprecated("catalogers", "use: override-default-catalogers and select-catalogers"); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
} else {
|
||||
panic("unable to mark flags as deprecated")
|
||||
}
|
||||
|
||||
flags.StringArrayVarP(&cfg.DefaultCatalogers, "override-default-catalogers", "",
|
||||
"set the base set of catalogers to use (defaults to 'image' or 'directory' depending on the scan source)")
|
||||
|
||||
flags.StringArrayVarP(&cfg.SelectCatalogers, "select-catalogers", "",
|
||||
"add, remove, and filter the catalogers to be used")
|
||||
}
|
||||
|
||||
func (cfg *CatalogerSelection) PostLoad() error {
|
||||
usingLegacyCatalogers := len(cfg.Catalogers) > 0
|
||||
usingNewCatalogers := len(cfg.DefaultCatalogers) > 0 || len(cfg.SelectCatalogers) > 0
|
||||
|
||||
if usingLegacyCatalogers && usingNewCatalogers {
|
||||
return fmt.Errorf("cannot use both 'catalogers' and 'select-catalogers'/'default-catalogers' flags %q | %q %q", cfg.Catalogers, cfg.SelectCatalogers, cfg.DefaultCatalogers)
|
||||
}
|
||||
|
||||
cfg.Catalogers = FlattenAndSort(cfg.Catalogers)
|
||||
cfg.DefaultCatalogers = FlattenAndSort(cfg.DefaultCatalogers)
|
||||
cfg.SelectCatalogers = FlattenAndSort(cfg.SelectCatalogers)
|
||||
|
||||
// for backwards compatibility
|
||||
cfg.DefaultCatalogers = append(cfg.DefaultCatalogers, cfg.Catalogers...)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package options
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCatalogerSelection_PostLoad(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
options CatalogerSelection
|
||||
assert func(t *testing.T, options CatalogerSelection)
|
||||
wantErr assert.ErrorAssertionFunc
|
||||
}{
|
||||
{
|
||||
name: "mutually exclusive cataloger flags (cat / def-cat)",
|
||||
options: CatalogerSelection{
|
||||
Catalogers: []string{"foo,bar", "42"},
|
||||
DefaultCatalogers: []string{"some,thing"},
|
||||
},
|
||||
wantErr: assert.Error,
|
||||
},
|
||||
{
|
||||
name: "mutually exclusive cataloger flags (cat / sel-cat)",
|
||||
options: CatalogerSelection{
|
||||
Catalogers: []string{"foo,bar", "42"},
|
||||
SelectCatalogers: []string{"some,thing"},
|
||||
},
|
||||
wantErr: assert.Error,
|
||||
},
|
||||
{
|
||||
name: "allow old cataloger flags",
|
||||
options: CatalogerSelection{
|
||||
Catalogers: []string{"foo,bar"},
|
||||
},
|
||||
assert: func(t *testing.T, options CatalogerSelection) {
|
||||
assert.Equal(t, []string{"bar", "foo"}, options.DefaultCatalogers) // note: sorted order
|
||||
assert.Equal(t, []string{"bar", "foo"}, options.Catalogers) // note: sorted order
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "allow new cataloger flags",
|
||||
options: CatalogerSelection{
|
||||
SelectCatalogers: []string{"foo,bar", "42"},
|
||||
DefaultCatalogers: []string{"some,thing"},
|
||||
},
|
||||
assert: func(t *testing.T, options CatalogerSelection) {
|
||||
assert.Equal(t, []string{"42", "bar", "foo"}, options.SelectCatalogers) // note: sorted order
|
||||
assert.Equal(t, []string{"some", "thing"}, options.DefaultCatalogers) // note: sorted order
|
||||
assert.Empty(t, options.Catalogers)
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.wantErr == nil {
|
||||
tt.wantErr = assert.NoError
|
||||
}
|
||||
tt.wantErr(t, tt.options.PostLoad(), fmt.Sprintf("PostLoad()"))
|
||||
if tt.assert != nil {
|
||||
tt.assert(t, tt.options)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
package options
|
||||
|
||||
import "github.com/anchore/clio"
|
||||
import (
|
||||
"github.com/anchore/clio"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/javascript"
|
||||
)
|
||||
|
||||
type javaScriptConfig struct {
|
||||
SearchRemoteLicenses *bool `json:"search-remote-licenses" yaml:"search-remote-licenses" mapstructure:"search-remote-licenses"`
|
||||
@@ -12,6 +15,24 @@ var _ interface {
|
||||
clio.FieldDescriber
|
||||
} = (*javaScriptConfig)(nil)
|
||||
|
||||
func defaultJavaScriptConfig() javaScriptConfig {
|
||||
def := javascript.DefaultCatalogerConfig()
|
||||
var includeDevDependencies *bool
|
||||
if def.IncludeDevDependencies {
|
||||
includeDevDependencies = &def.IncludeDevDependencies
|
||||
}
|
||||
|
||||
var searchRemoteLicenses *bool
|
||||
if def.SearchRemoteLicenses {
|
||||
searchRemoteLicenses = &def.SearchRemoteLicenses
|
||||
}
|
||||
return javaScriptConfig{
|
||||
NpmBaseURL: def.NPMBaseURL,
|
||||
SearchRemoteLicenses: searchRemoteLicenses,
|
||||
IncludeDevDependencies: includeDevDependencies,
|
||||
}
|
||||
}
|
||||
|
||||
func (o *javaScriptConfig) DescribeFields(descriptions clio.FieldDescriptionSet) {
|
||||
descriptions.Add(&o.SearchRemoteLicenses, `enables Syft to use the network to fill in more detailed license information`)
|
||||
descriptions.Add(&o.NpmBaseURL, `base NPM url to use`)
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
package options
|
||||
|
||||
import "github.com/anchore/clio"
|
||||
import (
|
||||
"github.com/anchore/clio"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/kernel"
|
||||
)
|
||||
|
||||
type linuxKernelConfig struct {
|
||||
CatalogModules bool `json:"catalog-modules" yaml:"catalog-modules" mapstructure:"catalog-modules"`
|
||||
}
|
||||
|
||||
func defaultLinuxKernelConfig() linuxKernelConfig {
|
||||
def := kernel.DefaultLinuxKernelCatalogerConfig()
|
||||
return linuxKernelConfig{
|
||||
CatalogModules: true,
|
||||
CatalogModules: def.CatalogModules,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package options
|
||||
|
||||
import "github.com/anchore/clio"
|
||||
import (
|
||||
"github.com/anchore/clio"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/python"
|
||||
)
|
||||
|
||||
type pythonConfig struct {
|
||||
SearchRemoteLicenses *bool `json:"search-remote-licenses" yaml:"search-remote-licenses" mapstructure:"search-remote-licenses"`
|
||||
@@ -12,6 +15,13 @@ var _ interface {
|
||||
clio.FieldDescriber
|
||||
} = (*pythonConfig)(nil)
|
||||
|
||||
func defaultPythonConfig() pythonConfig {
|
||||
def := python.DefaultCatalogerConfig()
|
||||
return pythonConfig{
|
||||
GuessUnpinnedRequirements: &def.GuessUnpinnedRequirements,
|
||||
}
|
||||
}
|
||||
|
||||
func (o *pythonConfig) DescribeFields(descriptions clio.FieldDescriptionSet) {
|
||||
descriptions.Add(&o.SearchRemoteLicenses, `enables Syft to use the network to fill in more detailed license information`)
|
||||
descriptions.Add(&o.PypiBaseURL, `base Pypi url to use`)
|
||||
|
||||
Reference in New Issue
Block a user