mirror of
https://github.com/anchore/syft.git
synced 2025-11-17 16:33:21 +01:00
fix linting
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
This commit is contained in:
parent
abfe73b3da
commit
0dd906b071
@ -1,6 +1,8 @@
|
|||||||
package commands
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
"github.com/anchore/clio"
|
"github.com/anchore/clio"
|
||||||
@ -14,8 +16,16 @@ func Cataloger(app clio.Application) *cobra.Command {
|
|||||||
|
|
||||||
cmd.AddCommand(
|
cmd.AddCommand(
|
||||||
CatalogerList(app),
|
CatalogerList(app),
|
||||||
CatalogerInfo(app),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// only add cataloger info command if experimental capabilities feature is enabled
|
||||||
|
if isCapabilitiesExperimentEnabled() {
|
||||||
|
cmd.AddCommand(CatalogerInfo(app))
|
||||||
|
}
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isCapabilitiesExperimentEnabled() bool {
|
||||||
|
return os.Getenv("SYFT_EXP_CAPABILITIES") == "true"
|
||||||
|
}
|
||||||
|
|||||||
@ -27,6 +27,50 @@ var (
|
|||||||
criteriaMargin = 10
|
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 {
|
type catalogerInfoOptions struct {
|
||||||
Output string `yaml:"output" json:"output" mapstructure:"output"`
|
Output string `yaml:"output" json:"output" mapstructure:"output"`
|
||||||
Names []string // cataloger names from args
|
Names []string // cataloger names from args
|
||||||
@ -73,6 +117,7 @@ func runCatalogerInfo(opts *catalogerInfoOptions) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bus.Report(report)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
@ -104,47 +149,6 @@ func catalogerInfoReport(opts *catalogerInfoOptions, doc *capabilities.Document,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func renderCatalogerInfoJSON(doc *capabilities.Document, catalogers []capabilities.CatalogerEntry) (string, error) {
|
func renderCatalogerInfoJSON(doc *capabilities.Document, catalogers []capabilities.CatalogerEntry) (string, error) {
|
||||||
type configFieldInfo struct {
|
|
||||||
Key string `json:"key"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
AppKey string `json:"app_key,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type configInfo struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
Fields []configFieldInfo `json:"fields,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type detectorPackageInfo struct {
|
|
||||||
Class string `json:"class"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
PURL string `json:"purl"`
|
|
||||||
CPEs []string `json:"cpes"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
}
|
|
||||||
|
|
||||||
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"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type 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 document struct {
|
type document struct {
|
||||||
Catalogers []catalogerInfo `json:"catalogers"`
|
Catalogers []catalogerInfo `json:"catalogers"`
|
||||||
}
|
}
|
||||||
@ -153,92 +157,23 @@ func renderCatalogerInfoJSON(doc *capabilities.Document, catalogers []capabiliti
|
|||||||
|
|
||||||
for _, cat := range catalogers {
|
for _, cat := range catalogers {
|
||||||
info := catalogerInfo{
|
info := catalogerInfo{
|
||||||
Ecosystem: cat.Ecosystem,
|
Ecosystem: cat.Ecosystem,
|
||||||
Name: cat.Name,
|
Name: cat.Name,
|
||||||
Type: cat.Type,
|
Type: cat.Type,
|
||||||
Selectors: cat.Selectors,
|
Selectors: cat.Selectors,
|
||||||
|
Deprecated: isDeprecatedCataloger(cat.Selectors),
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if cataloger is deprecated based on selectors
|
// convert parsers to patterns if available
|
||||||
for _, selector := range cat.Selectors {
|
info.Patterns = convertParsersToPatterns(cat.Parsers)
|
||||||
if selector == "deprecated" {
|
|
||||||
info.Deprecated = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, parser := range cat.Parsers {
|
|
||||||
// convert detector packages
|
|
||||||
var pkgs []detectorPackageInfo
|
|
||||||
for _, pkg := range parser.Detector.Packages {
|
|
||||||
pkgs = append(pkgs, detectorPackageInfo{
|
|
||||||
Class: pkg.Class,
|
|
||||||
Name: pkg.Name,
|
|
||||||
PURL: pkg.PURL,
|
|
||||||
CPEs: pkg.CPEs,
|
|
||||||
Type: pkg.Type,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pi := patternInfo{
|
|
||||||
Method: string(parser.Detector.Method),
|
|
||||||
Criteria: parser.Detector.Criteria,
|
|
||||||
Conditions: parser.Detector.Conditions,
|
|
||||||
Packages: pkgs,
|
|
||||||
Comment: parser.Detector.Comment,
|
|
||||||
PackageTypes: parser.PackageTypes,
|
|
||||||
JSONSchemaTypes: parser.JSONSchemaTypes,
|
|
||||||
Capabilities: parser.Capabilities,
|
|
||||||
}
|
|
||||||
|
|
||||||
info.Patterns = append(info.Patterns, pi)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// if no parsers, use detectors instead
|
||||||
if len(info.Patterns) == 0 {
|
if len(info.Patterns) == 0 {
|
||||||
info.Capabilities = cat.Capabilities
|
info.Capabilities = cat.Capabilities
|
||||||
|
info.Patterns = convertDetectorsToPatterns(cat.Detectors, cat.PackageTypes, cat.JSONSchemaTypes)
|
||||||
for _, det := range cat.Detectors {
|
|
||||||
// convert detector packages
|
|
||||||
var pkgs []detectorPackageInfo
|
|
||||||
for _, pkg := range det.Packages {
|
|
||||||
pkgs = append(pkgs, detectorPackageInfo{
|
|
||||||
Class: pkg.Class,
|
|
||||||
Name: pkg.Name,
|
|
||||||
PURL: pkg.PURL,
|
|
||||||
CPEs: pkg.CPEs,
|
|
||||||
Type: pkg.Type,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pi := patternInfo{
|
|
||||||
Method: string(det.Method),
|
|
||||||
Criteria: det.Criteria,
|
|
||||||
Conditions: det.Conditions,
|
|
||||||
Packages: pkgs,
|
|
||||||
Comment: det.Comment,
|
|
||||||
PackageTypes: cat.PackageTypes,
|
|
||||||
JSONSchemaTypes: cat.JSONSchemaTypes,
|
|
||||||
}
|
|
||||||
info.Patterns = append(info.Patterns, pi)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// add config information
|
info.Config = getConfigInfoFromDocument(doc, cat.Config)
|
||||||
if cat.Config != "" {
|
|
||||||
if configEntry, ok := doc.Configs[cat.Config]; ok {
|
|
||||||
cfg := &configInfo{
|
|
||||||
Type: cat.Config,
|
|
||||||
}
|
|
||||||
for _, field := range configEntry.Fields {
|
|
||||||
cfg.Fields = append(cfg.Fields, configFieldInfo{
|
|
||||||
Key: field.Key,
|
|
||||||
Description: field.Description,
|
|
||||||
AppKey: field.AppKey,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
info.Config = cfg
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
docOut.Catalogers = append(docOut.Catalogers, info)
|
docOut.Catalogers = append(docOut.Catalogers, info)
|
||||||
}
|
}
|
||||||
@ -247,6 +182,88 @@ func renderCatalogerInfoJSON(doc *capabilities.Document, catalogers []capabiliti
|
|||||||
return string(by), err
|
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 {
|
func renderCatalogerInfoTable(_ *capabilities.Document, catalogers []capabilities.CatalogerEntry) string {
|
||||||
if len(catalogers) == 0 {
|
if len(catalogers) == 0 {
|
||||||
return noStyle.Render("No catalogers found")
|
return noStyle.Render("No catalogers found")
|
||||||
@ -391,48 +408,11 @@ func extractArrayCapability(caps capabilities.CapabilitySet, name string) string
|
|||||||
func extractNodesCapability(caps capabilities.CapabilitySet) string {
|
func extractNodesCapability(caps capabilities.CapabilitySet) string {
|
||||||
for _, cap := range caps {
|
for _, cap := range caps {
|
||||||
if cap.Name == "dependency.depth" {
|
if cap.Name == "dependency.depth" {
|
||||||
// handle various array types
|
|
||||||
switch v := cap.Default.(type) {
|
switch v := cap.Default.(type) {
|
||||||
case []string:
|
case []string:
|
||||||
if len(v) == 0 {
|
return formatDepthStringArray(v)
|
||||||
return noStyle.Render("·")
|
|
||||||
}
|
|
||||||
// check if both direct and indirect are present
|
|
||||||
hasDirect := false
|
|
||||||
hasIndirect := false
|
|
||||||
for _, item := range v {
|
|
||||||
if item == "direct" {
|
|
||||||
hasDirect = true
|
|
||||||
}
|
|
||||||
if item == "indirect" {
|
|
||||||
hasIndirect = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if hasDirect && hasIndirect {
|
|
||||||
return "transitive"
|
|
||||||
}
|
|
||||||
return strings.Join(v, ", ")
|
|
||||||
case []interface{}:
|
case []interface{}:
|
||||||
if len(v) == 0 {
|
return formatDepthInterfaceArray(v)
|
||||||
return noStyle.Render("·")
|
|
||||||
}
|
|
||||||
hasDirect := false
|
|
||||||
hasIndirect := false
|
|
||||||
strs := make([]string, 0, len(v))
|
|
||||||
for _, item := range v {
|
|
||||||
str := fmt.Sprintf("%v", item)
|
|
||||||
strs = append(strs, str)
|
|
||||||
if str == "direct" {
|
|
||||||
hasDirect = true
|
|
||||||
}
|
|
||||||
if str == "indirect" {
|
|
||||||
hasIndirect = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if hasDirect && hasIndirect {
|
|
||||||
return "transitive"
|
|
||||||
}
|
|
||||||
return strings.Join(strs, ", ")
|
|
||||||
}
|
}
|
||||||
return noStyle.Render("·")
|
return noStyle.Render("·")
|
||||||
}
|
}
|
||||||
@ -440,6 +420,47 @@ func extractNodesCapability(caps capabilities.CapabilitySet) string {
|
|||||||
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 {
|
func formatCriteria(detectors []capabilities.Detector) string {
|
||||||
var allCriteria []string
|
var allCriteria []string
|
||||||
methods := strset.New()
|
methods := strset.New()
|
||||||
|
|||||||
@ -1,720 +1 @@
|
|||||||
package commands
|
package commands
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
|
|
||||||
"github.com/anchore/syft/internal/capabilities"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestParseEnrichmentMode(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
input string
|
|
||||||
want capabilities.EnrichmentMode
|
|
||||||
wantErr require.ErrorAssertionFunc
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "offline mode",
|
|
||||||
input: "offline",
|
|
||||||
want: capabilities.OfflineMode,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "online mode",
|
|
||||||
input: "online",
|
|
||||||
want: capabilities.OnlineMode,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "tool-execution mode",
|
|
||||||
input: "tool-execution",
|
|
||||||
want: capabilities.ToolExecutionMode,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid mode",
|
|
||||||
input: "invalid",
|
|
||||||
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 := parseEnrichmentMode(tt.input)
|
|
||||||
tt.wantErr(t, err)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
require.Equal(t, tt.want, got)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFilterCatalogersByName(t *testing.T) {
|
|
||||||
catalogers := []capabilities.CatalogerEntry{
|
|
||||||
{Name: "cataloger-a"},
|
|
||||||
{Name: "cataloger-b"},
|
|
||||||
{Name: "cataloger-c"},
|
|
||||||
}
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
names []string
|
|
||||||
wantNames []string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "filter single cataloger",
|
|
||||||
names: []string{"cataloger-a"},
|
|
||||||
wantNames: []string{"cataloger-a"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "filter multiple catalogers",
|
|
||||||
names: []string{"cataloger-a", "cataloger-c"},
|
|
||||||
wantNames: []string{"cataloger-a", "cataloger-c"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "filter non-existent cataloger",
|
|
||||||
names: []string{"cataloger-x"},
|
|
||||||
wantNames: []string{},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "empty filter returns empty",
|
|
||||||
names: []string{},
|
|
||||||
wantNames: []string{},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
got := filterCatalogersByName(catalogers, tt.names)
|
|
||||||
|
|
||||||
var gotNames []string
|
|
||||||
for _, cat := range got {
|
|
||||||
gotNames = append(gotNames, cat.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
if tt.wantNames == nil {
|
|
||||||
tt.wantNames = []string{}
|
|
||||||
}
|
|
||||||
if gotNames == nil {
|
|
||||||
gotNames = []string{}
|
|
||||||
}
|
|
||||||
|
|
||||||
require.Equal(t, tt.wantNames, gotNames)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFormatCriteria(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
detectors []capabilities.Detector
|
|
||||||
want string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "glob method - no parenthetical",
|
|
||||||
detectors: []capabilities.Detector{
|
|
||||||
{
|
|
||||||
Method: capabilities.GlobDetection,
|
|
||||||
Criteria: []string{"**/*.jar", "**/*.war"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
want: "**/*.jar, **/*.war",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "path method - with parenthetical",
|
|
||||||
detectors: []capabilities.Detector{
|
|
||||||
{
|
|
||||||
Method: capabilities.PathDetection,
|
|
||||||
Criteria: []string{"/usr/bin/python"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
want: "/usr/bin/python (path)",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "mimetype method - with parenthetical",
|
|
||||||
detectors: []capabilities.Detector{
|
|
||||||
{
|
|
||||||
Method: capabilities.MIMETypeDetection,
|
|
||||||
Criteria: []string{"application/x-executable"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
want: "application/x-executable (mimetype)",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "multiple criteria with non-glob method",
|
|
||||||
detectors: []capabilities.Detector{
|
|
||||||
{
|
|
||||||
Method: capabilities.PathDetection,
|
|
||||||
Criteria: []string{"/bin/sh", "/bin/bash"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
want: "/bin/sh, /bin/bash (path)",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
got := formatCriteria(tt.detectors)
|
|
||||||
require.Equal(t, tt.want, got)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFormatBool(t *testing.T) {
|
|
||||||
trueVal := true
|
|
||||||
falseVal := false
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
input *bool
|
|
||||||
want string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "true renders check mark",
|
|
||||||
input: &trueVal,
|
|
||||||
want: "✔",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "false renders dot",
|
|
||||||
input: &falseVal,
|
|
||||||
want: "·",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "nil renders dash",
|
|
||||||
input: nil,
|
|
||||||
want: "-",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
got := formatBool(tt.input)
|
|
||||||
// strip any styling to check the core character
|
|
||||||
require.Contains(t, got, tt.want)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFormatStringSlice(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
input []string
|
|
||||||
want string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "empty slice",
|
|
||||||
input: []string{},
|
|
||||||
want: "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "single item",
|
|
||||||
input: []string{"direct"},
|
|
||||||
want: "direct",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "multiple items",
|
|
||||||
input: []string{"direct", "indirect"},
|
|
||||||
want: "direct, indirect",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
got := formatStringSlice(tt.input)
|
|
||||||
require.Equal(t, tt.want, got)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBuildTableRow(t *testing.T) {
|
|
||||||
trueVal := true
|
|
||||||
falseVal := false
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
catalogerName string
|
|
||||||
selectors string
|
|
||||||
capability *capabilities.Capability
|
|
||||||
wantContains []string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "nil capability shows defaults",
|
|
||||||
catalogerName: "test-cataloger",
|
|
||||||
selectors: "**/*.txt",
|
|
||||||
capability: nil,
|
|
||||||
wantContains: []string{"test-cataloger", "**/*.txt", "-"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "capability with all fields",
|
|
||||||
catalogerName: "python-cataloger",
|
|
||||||
selectors: "**/setup.py",
|
|
||||||
capability: &capabilities.Capability{
|
|
||||||
License: &trueVal,
|
|
||||||
Dependencies: &capabilities.DependencyCapabilities{
|
|
||||||
Depth: []string{"direct", "indirect"},
|
|
||||||
Edges: "complete",
|
|
||||||
Kinds: []string{"runtime", "dev"},
|
|
||||||
},
|
|
||||||
PackageManager: &capabilities.PackageManagerCapabilities{
|
|
||||||
Files: &capabilities.FileCapabilities{
|
|
||||||
Listing: &trueVal,
|
|
||||||
Digests: &trueVal,
|
|
||||||
},
|
|
||||||
PackageIntegrityHash: &trueVal,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
wantContains: []string{
|
|
||||||
"python-cataloger",
|
|
||||||
"**/setup.py",
|
|
||||||
"✔",
|
|
||||||
"complete",
|
|
||||||
"runtime, dev",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "capability with partial fields",
|
|
||||||
catalogerName: "minimal-cataloger",
|
|
||||||
selectors: "N/A",
|
|
||||||
capability: &capabilities.Capability{
|
|
||||||
License: &falseVal,
|
|
||||||
Dependencies: &capabilities.DependencyCapabilities{
|
|
||||||
Depth: []string{"direct"},
|
|
||||||
Edges: "flat",
|
|
||||||
Kinds: []string{},
|
|
||||||
},
|
|
||||||
PackageManager: &capabilities.PackageManagerCapabilities{
|
|
||||||
Files: &capabilities.FileCapabilities{
|
|
||||||
Listing: &falseVal,
|
|
||||||
Digests: &falseVal,
|
|
||||||
},
|
|
||||||
PackageIntegrityHash: &falseVal,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
wantContains: []string{
|
|
||||||
"minimal-cataloger",
|
|
||||||
"N/A",
|
|
||||||
"direct",
|
|
||||||
"flat",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
row := buildTableRow("test-ecosystem", tt.catalogerName, tt.selectors, tt.capability)
|
|
||||||
|
|
||||||
// convert row to string representation for checking
|
|
||||||
var rowStr strings.Builder
|
|
||||||
for _, cell := range row {
|
|
||||||
rowStr.WriteString(cell)
|
|
||||||
rowStr.WriteString(" ")
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, want := range tt.wantContains {
|
|
||||||
require.Contains(t, rowStr.String(), want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRenderCatalogerInfoJSON(t *testing.T) {
|
|
||||||
trueVal := true
|
|
||||||
falseVal := false
|
|
||||||
|
|
||||||
catalogers := []capabilities.CatalogerEntry{
|
|
||||||
{
|
|
||||||
Ecosystem: "test",
|
|
||||||
Name: "test-generic-cataloger",
|
|
||||||
Type: "generic",
|
|
||||||
Parsers: []capabilities.Parser{
|
|
||||||
{
|
|
||||||
ParserFunction: "parseTest",
|
|
||||||
Detector: capabilities.Detector{
|
|
||||||
Method: capabilities.GlobDetection,
|
|
||||||
Criteria: []string{"**/*.test"},
|
|
||||||
},
|
|
||||||
Capabilities: map[capabilities.EnrichmentMode]*capabilities.Capability{
|
|
||||||
capabilities.OfflineMode: {
|
|
||||||
License: &trueVal,
|
|
||||||
Dependencies: &capabilities.DependencyCapabilities{
|
|
||||||
Depth: []string{"direct"},
|
|
||||||
Edges: "flat",
|
|
||||||
Kinds: []string{"runtime"},
|
|
||||||
},
|
|
||||||
PackageManager: &capabilities.PackageManagerCapabilities{
|
|
||||||
Files: &capabilities.FileCapabilities{
|
|
||||||
Listing: &trueVal,
|
|
||||||
Digests: &trueVal,
|
|
||||||
},
|
|
||||||
PackageIntegrityHash: &falseVal,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Ecosystem: "test",
|
|
||||||
Name: "test-custom-cataloger",
|
|
||||||
Type: "custom",
|
|
||||||
Capabilities: map[capabilities.EnrichmentMode]*capabilities.Capability{
|
|
||||||
capabilities.OfflineMode: {
|
|
||||||
License: &falseVal,
|
|
||||||
Dependencies: &capabilities.DependencyCapabilities{
|
|
||||||
Depth: []string{},
|
|
||||||
Edges: "",
|
|
||||||
Kinds: []string{},
|
|
||||||
},
|
|
||||||
PackageManager: &capabilities.PackageManagerCapabilities{
|
|
||||||
Files: &capabilities.FileCapabilities{
|
|
||||||
Listing: &falseVal,
|
|
||||||
Digests: &falseVal,
|
|
||||||
},
|
|
||||||
PackageIntegrityHash: &falseVal,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
mode capabilities.EnrichmentMode
|
|
||||||
wantErr require.ErrorAssertionFunc
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "offline mode",
|
|
||||||
mode: capabilities.OfflineMode,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "online mode with no capabilities",
|
|
||||||
mode: capabilities.OnlineMode,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
if tt.wantErr == nil {
|
|
||||||
tt.wantErr = require.NoError
|
|
||||||
}
|
|
||||||
|
|
||||||
doc := &capabilities.Document{
|
|
||||||
Catalogers: catalogers,
|
|
||||||
}
|
|
||||||
|
|
||||||
got, err := renderCatalogerInfoJSON(doc, catalogers, tt.mode)
|
|
||||||
tt.wantErr(t, err)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// verify it's valid JSON
|
|
||||||
var result map[string]interface{}
|
|
||||||
err = json.Unmarshal([]byte(got), &result)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// verify structure
|
|
||||||
require.Contains(t, result, "mode")
|
|
||||||
require.Contains(t, result, "catalogers")
|
|
||||||
require.Equal(t, string(tt.mode), result["mode"])
|
|
||||||
|
|
||||||
// verify capability_format field is present
|
|
||||||
catalogersList := result["catalogers"].([]interface{})
|
|
||||||
require.Greater(t, len(catalogersList), 0)
|
|
||||||
|
|
||||||
for _, cat := range catalogersList {
|
|
||||||
catMap := cat.(map[string]interface{})
|
|
||||||
require.Contains(t, catMap, "capability_format")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRenderCatalogerInfoJSONWithV2Capabilities(t *testing.T) {
|
|
||||||
// test cataloger with capabilities-v2
|
|
||||||
catalogers := []capabilities.CatalogerEntry{
|
|
||||||
{
|
|
||||||
Ecosystem: "test",
|
|
||||||
Name: "test-v2-cataloger",
|
|
||||||
Type: "custom",
|
|
||||||
CapabilitiesV2: capabilities.CapabilitySet{
|
|
||||||
{
|
|
||||||
Name: "license",
|
|
||||||
Default: false,
|
|
||||||
Conditions: []capabilities.CapabilityCondition{
|
|
||||||
{
|
|
||||||
When: map[string]interface{}{"SearchRemoteLicenses": true},
|
|
||||||
Value: true,
|
|
||||||
Comment: "License info fetched from registry",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "dependency.depth",
|
|
||||||
Default: []string{"direct", "indirect"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
doc := &capabilities.Document{
|
|
||||||
Catalogers: catalogers,
|
|
||||||
}
|
|
||||||
|
|
||||||
got, err := renderCatalogerInfoJSON(doc, catalogers, capabilities.OfflineMode)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// verify it's valid JSON
|
|
||||||
var result map[string]interface{}
|
|
||||||
err = json.Unmarshal([]byte(got), &result)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// verify structure includes capabilities_v2
|
|
||||||
catalogersList := result["catalogers"].([]interface{})
|
|
||||||
require.Len(t, catalogersList, 1)
|
|
||||||
|
|
||||||
cat := catalogersList[0].(map[string]interface{})
|
|
||||||
require.Equal(t, "v2", cat["capability_format"])
|
|
||||||
require.Contains(t, cat, "capabilities_v2")
|
|
||||||
|
|
||||||
// verify capabilities_v2 structure
|
|
||||||
capsV2 := cat["capabilities_v2"].([]interface{})
|
|
||||||
require.Len(t, capsV2, 2)
|
|
||||||
|
|
||||||
// check first capability field
|
|
||||||
field1 := capsV2[0].(map[string]interface{})
|
|
||||||
require.Equal(t, "license", field1["field"])
|
|
||||||
require.Equal(t, false, field1["default"])
|
|
||||||
require.Contains(t, field1, "conditions")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRenderCatalogerInfoTable(t *testing.T) {
|
|
||||||
trueVal := true
|
|
||||||
falseVal := false
|
|
||||||
|
|
||||||
catalogers := []capabilities.CatalogerEntry{
|
|
||||||
{
|
|
||||||
Ecosystem: "test",
|
|
||||||
Name: "test-cataloger",
|
|
||||||
Type: "generic",
|
|
||||||
Parsers: []capabilities.Parser{
|
|
||||||
{
|
|
||||||
ParserFunction: "parseTest",
|
|
||||||
Detector: capabilities.Detector{
|
|
||||||
Method: capabilities.GlobDetection,
|
|
||||||
Criteria: []string{"**/*.test"},
|
|
||||||
},
|
|
||||||
Capabilities: map[capabilities.EnrichmentMode]*capabilities.Capability{
|
|
||||||
capabilities.OfflineMode: {
|
|
||||||
License: &trueVal,
|
|
||||||
Dependencies: &capabilities.DependencyCapabilities{
|
|
||||||
Depth: []string{"direct"},
|
|
||||||
Edges: "flat",
|
|
||||||
Kinds: []string{"runtime"},
|
|
||||||
},
|
|
||||||
PackageManager: &capabilities.PackageManagerCapabilities{
|
|
||||||
Files: &capabilities.FileCapabilities{
|
|
||||||
Listing: &trueVal,
|
|
||||||
Digests: &falseVal,
|
|
||||||
},
|
|
||||||
PackageIntegrityHash: &falseVal,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
catalogers []capabilities.CatalogerEntry
|
|
||||||
mode capabilities.EnrichmentMode
|
|
||||||
wantContains []string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "renders table with cataloger data",
|
|
||||||
catalogers: catalogers,
|
|
||||||
mode: capabilities.OfflineMode,
|
|
||||||
wantContains: []string{
|
|
||||||
"test-cataloger",
|
|
||||||
"**/*.test",
|
|
||||||
"CATALOGER",
|
|
||||||
"CRITERIA",
|
|
||||||
"LICENSE",
|
|
||||||
"DEPTH",
|
|
||||||
"LISTING",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "empty catalogers list",
|
|
||||||
catalogers: []capabilities.CatalogerEntry{},
|
|
||||||
mode: capabilities.OfflineMode,
|
|
||||||
wantContains: []string{
|
|
||||||
"No catalogers found",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
doc := &capabilities.Document{
|
|
||||||
Catalogers: tt.catalogers,
|
|
||||||
}
|
|
||||||
|
|
||||||
got := renderCatalogerInfoTable(doc, tt.catalogers, tt.mode)
|
|
||||||
|
|
||||||
for _, want := range tt.wantContains {
|
|
||||||
require.Contains(t, got, want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRenderCatalogerInfoTableWithV2Capabilities(t *testing.T) {
|
|
||||||
// test cataloger with capabilities-v2
|
|
||||||
catalogers := []capabilities.CatalogerEntry{
|
|
||||||
{
|
|
||||||
Ecosystem: "javascript",
|
|
||||||
Name: "npm-cataloger",
|
|
||||||
Type: "custom",
|
|
||||||
Config: "javascript.CatalogerConfig",
|
|
||||||
CapabilitiesV2: capabilities.CapabilitySet{
|
|
||||||
{
|
|
||||||
Name: "license",
|
|
||||||
Default: false,
|
|
||||||
Conditions: []capabilities.CapabilityCondition{
|
|
||||||
{
|
|
||||||
When: map[string]interface{}{"SearchRemoteLicenses": true},
|
|
||||||
Value: true,
|
|
||||||
Comment: "License info fetched from NPM registry",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "dependency.depth",
|
|
||||||
Default: []string{"direct", "indirect"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
doc := &capabilities.Document{
|
|
||||||
Catalogers: catalogers,
|
|
||||||
}
|
|
||||||
|
|
||||||
got := renderCatalogerInfoTable(doc, catalogers, capabilities.OfflineMode)
|
|
||||||
|
|
||||||
// should include v2 capabilities section
|
|
||||||
require.Contains(t, got, "Configuration Impact on Capabilities")
|
|
||||||
require.Contains(t, got, "npm-cataloger")
|
|
||||||
require.Contains(t, got, "license")
|
|
||||||
require.Contains(t, got, "Default: false")
|
|
||||||
require.Contains(t, got, "When SearchRemoteLicenses=true: true")
|
|
||||||
require.Contains(t, got, "License info fetched from NPM registry")
|
|
||||||
require.Contains(t, got, "dependency.depth")
|
|
||||||
require.Contains(t, got, "[direct, indirect]")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFormatCapabilityValue(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
input interface{}
|
|
||||||
want string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "nil value",
|
|
||||||
input: nil,
|
|
||||||
want: "null",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "bool true",
|
|
||||||
input: true,
|
|
||||||
want: "true",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "bool false",
|
|
||||||
input: false,
|
|
||||||
want: "false",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "string value",
|
|
||||||
input: "test-value",
|
|
||||||
want: "test-value",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "string slice",
|
|
||||||
input: []string{"direct", "indirect"},
|
|
||||||
want: "[direct, indirect]",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "interface slice",
|
|
||||||
input: []interface{}{"runtime", "dev"},
|
|
||||||
want: "[runtime, dev]",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "empty slice",
|
|
||||||
input: []string{},
|
|
||||||
want: "[]",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
got := formatCapabilityValue(tt.input)
|
|
||||||
require.Equal(t, tt.want, got)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFormatWhenClause(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
input map[string]interface{}
|
|
||||||
want string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "empty when clause",
|
|
||||||
input: map[string]interface{}{},
|
|
||||||
want: "always",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "single condition",
|
|
||||||
input: map[string]interface{}{
|
|
||||||
"SearchRemoteLicenses": true,
|
|
||||||
},
|
|
||||||
want: "SearchRemoteLicenses=true",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "multiple conditions",
|
|
||||||
input: map[string]interface{}{
|
|
||||||
"SearchRemoteLicenses": true,
|
|
||||||
"UseNetwork": true,
|
|
||||||
},
|
|
||||||
// note: order may vary, but should contain both with AND
|
|
||||||
want: " AND ",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
got := formatWhenClause(tt.input)
|
|
||||||
if strings.Contains(tt.want, " AND ") {
|
|
||||||
// for multiple conditions, just check it contains AND
|
|
||||||
require.Contains(t, got, " AND ")
|
|
||||||
require.Contains(t, got, "SearchRemoteLicenses=true")
|
|
||||||
require.Contains(t, got, "UseNetwork=true")
|
|
||||||
} else {
|
|
||||||
require.Equal(t, tt.want, got)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -117,22 +117,27 @@ func extractAppConfigFields(filePath, topLevelKey string) ([]AppConfigField, err
|
|||||||
// findAppConfigStructAndDescriptions finds the main config struct and extracts field descriptions
|
// findAppConfigStructAndDescriptions finds the main config struct and extracts field descriptions
|
||||||
// from the DescribeFields method
|
// from the DescribeFields method
|
||||||
func findAppConfigStructAndDescriptions(f *ast.File, topLevelKey string) (*ast.StructType, map[string]string) {
|
func findAppConfigStructAndDescriptions(f *ast.File, topLevelKey string) (*ast.StructType, map[string]string) {
|
||||||
var configStruct *ast.StructType
|
expectedName := determineExpectedConfigName(topLevelKey)
|
||||||
descriptions := make(map[string]string)
|
configStruct := findConfigStruct(f, expectedName)
|
||||||
|
descriptions := extractDescriptionsFromDescribeFields(f)
|
||||||
|
return configStruct, descriptions
|
||||||
|
}
|
||||||
|
|
||||||
// determine the expected main config struct name based on the top-level key
|
// determineExpectedConfigName maps the top-level key to the expected config struct name
|
||||||
// e.g., "dotnet" -> "dotnetConfig", "golang" -> "golangConfig", "linux-kernel" -> "linuxKernelConfig"
|
func determineExpectedConfigName(topLevelKey string) string {
|
||||||
expectedName := topLevelKey + "Config"
|
// handle special cases first
|
||||||
// handle special case for linux-kernel
|
switch topLevelKey {
|
||||||
if topLevelKey == "linux-kernel" {
|
case "linux-kernel":
|
||||||
expectedName = "linuxKernelConfig"
|
return "linuxKernelConfig"
|
||||||
}
|
case "javascript":
|
||||||
// handle javascript
|
return "javaScriptConfig"
|
||||||
if topLevelKey == "javascript" {
|
default:
|
||||||
expectedName = "javaScriptConfig"
|
return topLevelKey + "Config"
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// find the main config struct (not nested ones)
|
// findConfigStruct searches for the config struct with the expected name in the AST
|
||||||
|
func findConfigStruct(f *ast.File, expectedName string) *ast.StructType {
|
||||||
for _, decl := range f.Decls {
|
for _, decl := range f.Decls {
|
||||||
genDecl, ok := decl.(*ast.GenDecl)
|
genDecl, ok := decl.(*ast.GenDecl)
|
||||||
if !ok || genDecl.Tok != token.TYPE {
|
if !ok || genDecl.Tok != token.TYPE {
|
||||||
@ -150,18 +155,18 @@ func findAppConfigStructAndDescriptions(f *ast.File, topLevelKey string) (*ast.S
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// match the expected main config struct name
|
|
||||||
if typeSpec.Name.Name == expectedName {
|
if typeSpec.Name.Name == expectedName {
|
||||||
configStruct = structType
|
return structType
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if configStruct != nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractDescriptionsFromDescribeFields extracts field descriptions from the DescribeFields method
|
||||||
|
func extractDescriptionsFromDescribeFields(f *ast.File) map[string]string {
|
||||||
|
descriptions := make(map[string]string)
|
||||||
|
|
||||||
// find DescribeFields method and extract descriptions
|
|
||||||
for _, decl := range f.Decls {
|
for _, decl := range f.Decls {
|
||||||
funcDecl, ok := decl.(*ast.FuncDecl)
|
funcDecl, ok := decl.(*ast.FuncDecl)
|
||||||
if !ok || funcDecl.Name.Name != "DescribeFields" {
|
if !ok || funcDecl.Name.Name != "DescribeFields" {
|
||||||
@ -194,7 +199,6 @@ func findAppConfigStructAndDescriptions(f *ast.File, topLevelKey string) (*ast.S
|
|||||||
// second argument is the description string
|
// second argument is the description string
|
||||||
description := extractStringLiteral(callExpr.Args[1])
|
description := extractStringLiteral(callExpr.Args[1])
|
||||||
if description != "" {
|
if description != "" {
|
||||||
// clean up multi-line descriptions
|
|
||||||
description = cleanDescription(description)
|
description = cleanDescription(description)
|
||||||
descriptions[fieldPath] = description
|
descriptions[fieldPath] = description
|
||||||
}
|
}
|
||||||
@ -203,7 +207,7 @@ func findAppConfigStructAndDescriptions(f *ast.File, topLevelKey string) (*ast.S
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return configStruct, descriptions
|
return descriptions
|
||||||
}
|
}
|
||||||
|
|
||||||
// extractNestedAppConfigs handles nested config structs like golang.MainModuleVersion
|
// extractNestedAppConfigs handles nested config structs like golang.MainModuleVersion
|
||||||
|
|||||||
@ -161,14 +161,18 @@ func extractCatalogerName(funcDecl *ast.FuncDecl, file *ast.File, filePath, repo
|
|||||||
// inferCatalogerNameFromCustomImpl tries to infer the cataloger name from custom cataloger implementations
|
// inferCatalogerNameFromCustomImpl tries to infer the cataloger name from custom cataloger implementations
|
||||||
// by looking for Name() method implementations or hardcoded name variables
|
// by looking for Name() method implementations or hardcoded name variables
|
||||||
func inferCatalogerNameFromCustomImpl(funcDecl *ast.FuncDecl, file *ast.File, ctx *parseContext) string {
|
func inferCatalogerNameFromCustomImpl(funcDecl *ast.FuncDecl, file *ast.File, ctx *parseContext) string {
|
||||||
// look for patterns like:
|
typeName := extractReturnTypeName(funcDecl)
|
||||||
// return &someCataloger{cfg: cfg}
|
if typeName == "" {
|
||||||
// return someCataloger{cfg: cfg}
|
return ""
|
||||||
|
}
|
||||||
|
return findNameMethodReturn(file, typeName, ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractReturnTypeName extracts the type name from the return statement of a constructor function
|
||||||
|
func extractReturnTypeName(funcDecl *ast.FuncDecl) string {
|
||||||
var typeName string
|
var typeName string
|
||||||
|
|
||||||
ast.Inspect(funcDecl.Body, func(n ast.Node) bool {
|
ast.Inspect(funcDecl.Body, func(n ast.Node) bool {
|
||||||
// look for return statements
|
|
||||||
returnStmt, ok := n.(*ast.ReturnStmt)
|
returnStmt, ok := n.(*ast.ReturnStmt)
|
||||||
if !ok || len(returnStmt.Results) == 0 {
|
if !ok || len(returnStmt.Results) == 0 {
|
||||||
return true
|
return true
|
||||||
@ -197,62 +201,29 @@ func inferCatalogerNameFromCustomImpl(funcDecl *ast.FuncDecl, file *ast.File, ct
|
|||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
if typeName == "" {
|
return typeName
|
||||||
return ""
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// now look for the Name() method on this type
|
// findNameMethodReturn finds the Name() method for the given type and extracts its return value
|
||||||
|
func findNameMethodReturn(file *ast.File, typeName string, ctx *parseContext) string {
|
||||||
for _, decl := range file.Decls {
|
for _, decl := range file.Decls {
|
||||||
funcDecl, ok := decl.(*ast.FuncDecl)
|
funcDecl, ok := decl.(*ast.FuncDecl)
|
||||||
if !ok || funcDecl.Name.Name != "Name" {
|
if !ok || funcDecl.Name.Name != "Name" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if this is a method on our type
|
|
||||||
if funcDecl.Recv == nil || len(funcDecl.Recv.List) == 0 {
|
if funcDecl.Recv == nil || len(funcDecl.Recv.List) == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
recvType := funcDecl.Recv.List[0].Type
|
recvTypeName := extractReceiverTypeName(funcDecl.Recv.List[0].Type)
|
||||||
var recvTypeName string
|
|
||||||
|
|
||||||
// handle both T and *T receivers
|
|
||||||
if ident, ok := recvType.(*ast.Ident); ok {
|
|
||||||
recvTypeName = ident.Name
|
|
||||||
} else if starExpr, ok := recvType.(*ast.StarExpr); ok {
|
|
||||||
if ident, ok := starExpr.X.(*ast.Ident); ok {
|
|
||||||
recvTypeName = ident.Name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if recvTypeName != typeName {
|
if recvTypeName != typeName {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// found the Name() method, extract the return value
|
// found the Name() method, extract the return value
|
||||||
if funcDecl.Body != nil {
|
if funcDecl.Body != nil {
|
||||||
var name string
|
if name := extractNameFromMethodBody(funcDecl.Body, ctx); name != "" {
|
||||||
ast.Inspect(funcDecl.Body, func(n ast.Node) bool {
|
|
||||||
returnStmt, ok := n.(*ast.ReturnStmt)
|
|
||||||
if !ok || len(returnStmt.Results) == 0 {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// handle string literal
|
|
||||||
if lit, ok := returnStmt.Results[0].(*ast.BasicLit); ok && lit.Kind == token.STRING {
|
|
||||||
name = strings.Trim(lit.Value, `"`)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// handle constant identifier (e.g., pomCatalogerName)
|
|
||||||
if ident, ok := returnStmt.Results[0].(*ast.Ident); ok {
|
|
||||||
name = resolveLocalConstant(ident.Name, ctx)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
if name != "" {
|
|
||||||
return name
|
return name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -261,6 +232,46 @@ func inferCatalogerNameFromCustomImpl(funcDecl *ast.FuncDecl, file *ast.File, ct
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// extractReceiverTypeName extracts the type name from a receiver type expression
|
||||||
|
func extractReceiverTypeName(recvType ast.Expr) string {
|
||||||
|
// handle both T and *T receivers
|
||||||
|
if ident, ok := recvType.(*ast.Ident); ok {
|
||||||
|
return ident.Name
|
||||||
|
}
|
||||||
|
if starExpr, ok := recvType.(*ast.StarExpr); ok {
|
||||||
|
if ident, ok := starExpr.X.(*ast.Ident); ok {
|
||||||
|
return ident.Name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractNameFromMethodBody extracts the cataloger name from a Name() method body
|
||||||
|
func extractNameFromMethodBody(body *ast.BlockStmt, ctx *parseContext) string {
|
||||||
|
var name string
|
||||||
|
ast.Inspect(body, func(n ast.Node) bool {
|
||||||
|
returnStmt, ok := n.(*ast.ReturnStmt)
|
||||||
|
if !ok || len(returnStmt.Results) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle string literal
|
||||||
|
if lit, ok := returnStmt.Results[0].(*ast.BasicLit); ok && lit.Kind == token.STRING {
|
||||||
|
name = strings.Trim(lit.Value, `"`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle constant identifier (e.g., pomCatalogerName)
|
||||||
|
if ident, ok := returnStmt.Results[0].(*ast.Ident); ok {
|
||||||
|
name = resolveLocalConstant(ident.Name, ctx)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
// extractConfigParameter extracts the config type from the first parameter of a cataloger constructor.
|
// extractConfigParameter extracts the config type from the first parameter of a cataloger constructor.
|
||||||
// Returns empty string if no config parameter is found.
|
// Returns empty string if no config parameter is found.
|
||||||
// Returns format: "packageName.StructName" (e.g., "golang.CatalogerConfig")
|
// Returns format: "packageName.StructName" (e.g., "golang.CatalogerConfig")
|
||||||
|
|||||||
@ -419,5 +419,5 @@ func DiscoverAllowedConfigStructs(repoRoot string) (map[string]bool, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, fmt.Errorf("Config struct not found in %s", configFilePath)
|
return nil, fmt.Errorf("config struct not found in %s", configFilePath)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -651,7 +651,7 @@ func RepoRoot() (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// extractBinaryClassifiers extracts all binary classifiers with their full information
|
// extractBinaryClassifiers extracts all binary classifiers with their full information
|
||||||
func extractBinaryClassifiers() []binary.Classifier {
|
func extractBinaryClassifiers() []binary.Classifier { //nolint:staticcheck
|
||||||
classifiers := binary.DefaultClassifiers()
|
classifiers := binary.DefaultClassifiers()
|
||||||
|
|
||||||
// return all classifiers (already sorted by the default function)
|
// return all classifiers (already sorted by the default function)
|
||||||
|
|||||||
@ -333,48 +333,55 @@ func updateNodeTree(rootNode *yaml.Node, doc *capabilities.Document) error {
|
|||||||
updateOrAddSection(existingMapping, newMapping, "application")
|
updateOrAddSection(existingMapping, newMapping, "application")
|
||||||
|
|
||||||
// update catalogers section (preserve comments)
|
// update catalogers section (preserve comments)
|
||||||
|
updateCatalogersSection(existingMapping, newMapping)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateCatalogersSection updates the catalogers section while preserving comments
|
||||||
|
func updateCatalogersSection(existingMapping, newMapping *yaml.Node) {
|
||||||
existingCatalogersNode := findSectionNode(existingMapping, "catalogers")
|
existingCatalogersNode := findSectionNode(existingMapping, "catalogers")
|
||||||
newCatalogersNode := findSectionNode(newMapping, "catalogers")
|
newCatalogersNode := findSectionNode(newMapping, "catalogers")
|
||||||
|
|
||||||
if existingCatalogersNode != nil && newCatalogersNode != nil {
|
if existingCatalogersNode == nil || newCatalogersNode == nil {
|
||||||
// create a map of existing cataloger nodes by name for quick lookup
|
return
|
||||||
existingByName := make(map[string]*yaml.Node)
|
|
||||||
if existingCatalogersNode.Kind == yaml.SequenceNode {
|
|
||||||
for _, catalogerNode := range existingCatalogersNode.Content {
|
|
||||||
if catalogerNode.Kind == yaml.MappingNode {
|
|
||||||
name := findFieldValue(catalogerNode, "name")
|
|
||||||
if name != "" {
|
|
||||||
existingByName[name] = catalogerNode
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// update each cataloger in the new tree with preserved comments
|
|
||||||
if newCatalogersNode.Kind == yaml.SequenceNode {
|
|
||||||
for _, newCatalogerNode := range newCatalogersNode.Content {
|
|
||||||
if newCatalogerNode.Kind != yaml.MappingNode {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
name := findFieldValue(newCatalogerNode, "name")
|
|
||||||
if existingNode := existingByName[name]; existingNode != nil {
|
|
||||||
// preserve comments from existing cataloger entry
|
|
||||||
newCatalogerNode.HeadComment = existingNode.HeadComment
|
|
||||||
newCatalogerNode.LineComment = existingNode.LineComment
|
|
||||||
newCatalogerNode.FootComment = existingNode.FootComment
|
|
||||||
|
|
||||||
// preserve field-level and nested comments
|
|
||||||
preserveFieldComments(existingNode, newCatalogerNode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// replace the catalogers content
|
|
||||||
existingCatalogersNode.Content = newCatalogersNode.Content
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
// create a map of existing cataloger nodes by name for quick lookup
|
||||||
|
existingByName := make(map[string]*yaml.Node)
|
||||||
|
if existingCatalogersNode.Kind == yaml.SequenceNode {
|
||||||
|
for _, catalogerNode := range existingCatalogersNode.Content {
|
||||||
|
if catalogerNode.Kind == yaml.MappingNode {
|
||||||
|
name := findFieldValue(catalogerNode, "name")
|
||||||
|
if name != "" {
|
||||||
|
existingByName[name] = catalogerNode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// update each cataloger in the new tree with preserved comments
|
||||||
|
if newCatalogersNode.Kind == yaml.SequenceNode {
|
||||||
|
for _, newCatalogerNode := range newCatalogersNode.Content {
|
||||||
|
if newCatalogerNode.Kind != yaml.MappingNode {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
name := findFieldValue(newCatalogerNode, "name")
|
||||||
|
if existingNode := existingByName[name]; existingNode != nil {
|
||||||
|
// preserve comments from existing cataloger entry
|
||||||
|
newCatalogerNode.HeadComment = existingNode.HeadComment
|
||||||
|
newCatalogerNode.LineComment = existingNode.LineComment
|
||||||
|
newCatalogerNode.FootComment = existingNode.FootComment
|
||||||
|
|
||||||
|
// preserve field-level and nested comments
|
||||||
|
preserveFieldComments(existingNode, newCatalogerNode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// replace the catalogers content
|
||||||
|
existingCatalogersNode.Content = newCatalogersNode.Content
|
||||||
}
|
}
|
||||||
|
|
||||||
// updateOrAddSection updates or adds a section in the existing mapping from the new mapping
|
// updateOrAddSection updates or adds a section in the existing mapping from the new mapping
|
||||||
|
|||||||
@ -65,40 +65,11 @@ type Statistics struct {
|
|||||||
func RegenerateCapabilities(yamlPath string, repoRoot string) (*Statistics, error) {
|
func RegenerateCapabilities(yamlPath string, repoRoot string) (*Statistics, error) {
|
||||||
stats := &Statistics{}
|
stats := &Statistics{}
|
||||||
|
|
||||||
// 1. Discover generic catalogers from code
|
// 1-2. Discover all cataloger data
|
||||||
fmt.Print(" → Scanning source code for generic catalogers...")
|
discovered, customCatalogerMetadata, customCatalogerPackageTypes, binaryClassifiers, allCatalogers, err := discoverAllCatalogerData(repoRoot, stats)
|
||||||
discovered, err := discoverGenericCatalogers(repoRoot)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to discover catalogers: %w", err)
|
return nil, err
|
||||||
}
|
}
|
||||||
stats.TotalGenericCatalogers = len(discovered)
|
|
||||||
fmt.Printf(" found %d\n", stats.TotalGenericCatalogers)
|
|
||||||
|
|
||||||
// 1a. Discover metadata types and package types from test-generated JSON files
|
|
||||||
fmt.Print(" → Searching for metadata type and package type information...")
|
|
||||||
customCatalogerMetadata, customCatalogerPackageTypes, err := discoverMetadataTypes(repoRoot, discovered)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to discover metadata types: %w", err)
|
|
||||||
}
|
|
||||||
fmt.Println(" done")
|
|
||||||
|
|
||||||
// 1b. Extract binary classifiers
|
|
||||||
fmt.Print(" → Extracting binary classifiers...")
|
|
||||||
binaryClassifiers := extractBinaryClassifiers()
|
|
||||||
fmt.Printf(" found %d classifiers\n", len(binaryClassifiers))
|
|
||||||
|
|
||||||
// Count parser functions
|
|
||||||
for _, disc := range discovered {
|
|
||||||
stats.TotalParserFunctions += len(disc.Parsers)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Get all package cataloger info (names and selectors)
|
|
||||||
fmt.Print(" → Fetching all cataloger info from syft...")
|
|
||||||
allCatalogers, err := allPackageCatalogerInfo()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to get cataloger info: %w", err)
|
|
||||||
}
|
|
||||||
fmt.Printf(" found %d total\n", len(allCatalogers))
|
|
||||||
|
|
||||||
// 3. Load existing YAML (if exists) - now returns both document and node tree
|
// 3. Load existing YAML (if exists) - now returns both document and node tree
|
||||||
fmt.Print(" → Loading existing packages.yaml...")
|
fmt.Print(" → Loading existing packages.yaml...")
|
||||||
@ -108,107 +79,24 @@ func RegenerateCapabilities(yamlPath string, repoRoot string) (*Statistics, erro
|
|||||||
}
|
}
|
||||||
fmt.Printf(" loaded %d entries\n", len(existing.Catalogers))
|
fmt.Printf(" loaded %d entries\n", len(existing.Catalogers))
|
||||||
|
|
||||||
// 3a. Discover cataloger config structs
|
// 3a-3c. Discover and process all config-related information
|
||||||
fmt.Print(" → Discovering cataloger config structs...")
|
discoveredConfigs, err := discoverAndFilterConfigs(repoRoot)
|
||||||
configInfoMap, err := DiscoverConfigs(repoRoot)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to discover configs: %w", err)
|
return nil, err
|
||||||
}
|
}
|
||||||
fmt.Printf(" found %d\n", len(configInfoMap))
|
|
||||||
|
|
||||||
// 3a-1. Get whitelist of allowed config structs from pkgcataloging.Config
|
discoveredAppConfigs, err := discoverAndConvertAppConfigs(repoRoot)
|
||||||
fmt.Print(" → Filtering configs by pkgcataloging.Config whitelist...")
|
|
||||||
allowedConfigs, err := DiscoverAllowedConfigStructs(repoRoot)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to discover allowed config structs: %w", err)
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// filter discovered configs to only include allowed ones
|
|
||||||
filteredConfigInfoMap := make(map[string]ConfigInfo)
|
|
||||||
for key, configInfo := range configInfoMap {
|
|
||||||
if allowedConfigs[key] {
|
|
||||||
filteredConfigInfoMap[key] = configInfo
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fmt.Printf(" %d allowed (filtered %d)\n", len(filteredConfigInfoMap), len(configInfoMap)-len(filteredConfigInfoMap))
|
|
||||||
|
|
||||||
// convert ConfigInfo to CatalogerConfigEntry format for packages.yaml
|
|
||||||
discoveredConfigs := make(map[string]capabilities.CatalogerConfigEntry)
|
|
||||||
for key, configInfo := range filteredConfigInfoMap {
|
|
||||||
fields := make([]capabilities.CatalogerConfigFieldEntry, len(configInfo.Fields))
|
|
||||||
for i, field := range configInfo.Fields {
|
|
||||||
fields[i] = capabilities.CatalogerConfigFieldEntry{
|
|
||||||
Key: field.Name,
|
|
||||||
Description: field.Description,
|
|
||||||
AppKey: field.AppKey,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
discoveredConfigs[key] = capabilities.CatalogerConfigEntry{
|
|
||||||
Fields: fields,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3b. Discover app-level configs
|
|
||||||
fmt.Print(" → Discovering app-level config fields...")
|
|
||||||
appConfigFields, err := DiscoverAppConfigs(repoRoot)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to discover app configs: %w", err)
|
|
||||||
}
|
|
||||||
fmt.Printf(" found %d\n", len(appConfigFields))
|
|
||||||
|
|
||||||
// convert to ApplicationConfigField format
|
|
||||||
discoveredAppConfigs := make([]capabilities.ApplicationConfigField, len(appConfigFields))
|
|
||||||
for i, field := range appConfigFields {
|
|
||||||
discoveredAppConfigs[i] = capabilities.ApplicationConfigField{
|
|
||||||
Key: field.Key,
|
|
||||||
Description: field.Description,
|
|
||||||
DefaultValue: field.DefaultValue,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3c. Link catalogers to their configs
|
|
||||||
fmt.Print(" → Linking catalogers to config structs...")
|
|
||||||
catalogerConfigMappings, err := LinkCatalogersToConfigs(repoRoot)
|
catalogerConfigMappings, err := LinkCatalogersToConfigs(repoRoot)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to link catalogers to configs: %w", err)
|
return nil, fmt.Errorf("failed to link catalogers to configs: %w", err)
|
||||||
}
|
}
|
||||||
fmt.Printf(" found %d mappings\n", len(catalogerConfigMappings))
|
fmt.Printf(" → Linking catalogers to config structs... found %d mappings\n", len(catalogerConfigMappings))
|
||||||
|
|
||||||
// 3c-1. Filter cataloger config mappings by exceptions
|
filteredCatalogerConfigMappings := applyConfigMappingFilters(catalogerConfigMappings)
|
||||||
// Remove any catalogers that should not have config fields
|
|
||||||
fmt.Print(" → Filtering cataloger config mappings by exceptions...")
|
|
||||||
filteredCatalogerConfigMappings := make(map[string]string)
|
|
||||||
filteredCount := 0
|
|
||||||
for catalogerName, configName := range catalogerConfigMappings {
|
|
||||||
if catalogerConfigExceptions.Has(catalogerName) {
|
|
||||||
filteredCount++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
filteredCatalogerConfigMappings[catalogerName] = configName
|
|
||||||
}
|
|
||||||
if filteredCount > 0 {
|
|
||||||
fmt.Printf(" filtered %d\n", filteredCount)
|
|
||||||
} else {
|
|
||||||
fmt.Println(" none")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3c-2. Merge manual config overrides
|
|
||||||
// Manual overrides take precedence over discovered mappings
|
|
||||||
fmt.Print(" → Merging manual config overrides...")
|
|
||||||
overrideCount := 0
|
|
||||||
for catalogerName, configName := range catalogerConfigOverrides {
|
|
||||||
if catalogerConfigExceptions.Has(catalogerName) {
|
|
||||||
// skip if this cataloger is in the exceptions list
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
filteredCatalogerConfigMappings[catalogerName] = configName
|
|
||||||
overrideCount++
|
|
||||||
}
|
|
||||||
if overrideCount > 0 {
|
|
||||||
fmt.Printf(" added %d\n", overrideCount)
|
|
||||||
} else {
|
|
||||||
fmt.Println(" none")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Build updated catalogers list
|
// 4. Build updated catalogers list
|
||||||
fmt.Println(" → Merging discovered catalogers with existing entries...")
|
fmt.Println(" → Merging discovered catalogers with existing entries...")
|
||||||
@ -244,6 +132,156 @@ func RegenerateCapabilities(yamlPath string, repoRoot string) (*Statistics, erro
|
|||||||
return stats, nil
|
return stats, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// discoverAllCatalogerData discovers all cataloger-related data including generic catalogers, metadata, binary classifiers, and all catalogers
|
||||||
|
func discoverAllCatalogerData(repoRoot string, stats *Statistics) (
|
||||||
|
map[string]DiscoveredCataloger,
|
||||||
|
map[string][]string,
|
||||||
|
map[string][]string,
|
||||||
|
[]binary.Classifier, //nolint:staticcheck
|
||||||
|
[]capabilities.CatalogerInfo,
|
||||||
|
error,
|
||||||
|
) {
|
||||||
|
// discover generic catalogers
|
||||||
|
fmt.Print(" → Scanning source code for generic catalogers...")
|
||||||
|
discovered, err := discoverGenericCatalogers(repoRoot)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, nil, nil, fmt.Errorf("failed to discover catalogers: %w", err)
|
||||||
|
}
|
||||||
|
stats.TotalGenericCatalogers = len(discovered)
|
||||||
|
fmt.Printf(" found %d\n", stats.TotalGenericCatalogers)
|
||||||
|
|
||||||
|
// discover metadata types
|
||||||
|
fmt.Print(" → Searching for metadata type and package type information...")
|
||||||
|
customCatalogerMetadata, customCatalogerPackageTypes, err := discoverMetadataTypes(repoRoot, discovered)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, nil, nil, fmt.Errorf("failed to discover metadata types: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Println(" done")
|
||||||
|
|
||||||
|
// extract binary classifiers
|
||||||
|
fmt.Print(" → Extracting binary classifiers...")
|
||||||
|
binaryClassifiers := extractBinaryClassifiers()
|
||||||
|
fmt.Printf(" found %d classifiers\n", len(binaryClassifiers))
|
||||||
|
|
||||||
|
// count parser functions
|
||||||
|
for _, disc := range discovered {
|
||||||
|
stats.TotalParserFunctions += len(disc.Parsers)
|
||||||
|
}
|
||||||
|
|
||||||
|
// get all cataloger info
|
||||||
|
fmt.Print(" → Fetching all cataloger info from syft...")
|
||||||
|
allCatalogers, err := allPackageCatalogerInfo()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, nil, nil, fmt.Errorf("failed to get cataloger info: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Printf(" found %d total\n", len(allCatalogers))
|
||||||
|
|
||||||
|
return discovered, customCatalogerMetadata, customCatalogerPackageTypes, binaryClassifiers, allCatalogers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// discoverAndFilterConfigs discovers cataloger config structs, filters by whitelist, and converts to capabilities format
|
||||||
|
func discoverAndFilterConfigs(repoRoot string) (map[string]capabilities.CatalogerConfigEntry, error) {
|
||||||
|
fmt.Print(" → Discovering cataloger config structs...")
|
||||||
|
configInfoMap, err := DiscoverConfigs(repoRoot)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to discover configs: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Printf(" found %d\n", len(configInfoMap))
|
||||||
|
|
||||||
|
fmt.Print(" → Filtering configs by pkgcataloging.Config whitelist...")
|
||||||
|
allowedConfigs, err := DiscoverAllowedConfigStructs(repoRoot)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to discover allowed config structs: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// filter discovered configs to only include allowed ones
|
||||||
|
filteredConfigInfoMap := make(map[string]ConfigInfo)
|
||||||
|
for key, configInfo := range configInfoMap {
|
||||||
|
if allowedConfigs[key] {
|
||||||
|
filteredConfigInfoMap[key] = configInfo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Printf(" %d allowed (filtered %d)\n", len(filteredConfigInfoMap), len(configInfoMap)-len(filteredConfigInfoMap))
|
||||||
|
|
||||||
|
// convert ConfigInfo to CatalogerConfigEntry format for packages.yaml
|
||||||
|
discoveredConfigs := make(map[string]capabilities.CatalogerConfigEntry)
|
||||||
|
for key, configInfo := range filteredConfigInfoMap {
|
||||||
|
fields := make([]capabilities.CatalogerConfigFieldEntry, len(configInfo.Fields))
|
||||||
|
for i, field := range configInfo.Fields {
|
||||||
|
fields[i] = capabilities.CatalogerConfigFieldEntry{
|
||||||
|
Key: field.Name,
|
||||||
|
Description: field.Description,
|
||||||
|
AppKey: field.AppKey,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
discoveredConfigs[key] = capabilities.CatalogerConfigEntry{
|
||||||
|
Fields: fields,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return discoveredConfigs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// discoverAndConvertAppConfigs discovers app-level config fields and converts them to capabilities format
|
||||||
|
func discoverAndConvertAppConfigs(repoRoot string) ([]capabilities.ApplicationConfigField, error) {
|
||||||
|
fmt.Print(" → Discovering app-level config fields...")
|
||||||
|
appConfigFields, err := DiscoverAppConfigs(repoRoot)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to discover app configs: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Printf(" found %d\n", len(appConfigFields))
|
||||||
|
|
||||||
|
// convert to ApplicationConfigField format
|
||||||
|
discoveredAppConfigs := make([]capabilities.ApplicationConfigField, len(appConfigFields))
|
||||||
|
for i, field := range appConfigFields {
|
||||||
|
discoveredAppConfigs[i] = capabilities.ApplicationConfigField{
|
||||||
|
Key: field.Key,
|
||||||
|
Description: field.Description,
|
||||||
|
DefaultValue: field.DefaultValue,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return discoveredAppConfigs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// applyConfigMappingFilters applies exceptions and manual overrides to cataloger config mappings
|
||||||
|
func applyConfigMappingFilters(catalogerConfigMappings map[string]string) map[string]string {
|
||||||
|
// filter by exceptions
|
||||||
|
fmt.Print(" → Filtering cataloger config mappings by exceptions...")
|
||||||
|
filteredCatalogerConfigMappings := make(map[string]string)
|
||||||
|
filteredCount := 0
|
||||||
|
for catalogerName, configName := range catalogerConfigMappings {
|
||||||
|
if catalogerConfigExceptions.Has(catalogerName) {
|
||||||
|
filteredCount++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
filteredCatalogerConfigMappings[catalogerName] = configName
|
||||||
|
}
|
||||||
|
if filteredCount > 0 {
|
||||||
|
fmt.Printf(" filtered %d\n", filteredCount)
|
||||||
|
} else {
|
||||||
|
fmt.Println(" none")
|
||||||
|
}
|
||||||
|
|
||||||
|
// merge manual overrides
|
||||||
|
fmt.Print(" → Merging manual config overrides...")
|
||||||
|
overrideCount := 0
|
||||||
|
for catalogerName, configName := range catalogerConfigOverrides {
|
||||||
|
if catalogerConfigExceptions.Has(catalogerName) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
filteredCatalogerConfigMappings[catalogerName] = configName
|
||||||
|
overrideCount++
|
||||||
|
}
|
||||||
|
if overrideCount > 0 {
|
||||||
|
fmt.Printf(" added %d\n", overrideCount)
|
||||||
|
} else {
|
||||||
|
fmt.Println(" none")
|
||||||
|
}
|
||||||
|
|
||||||
|
return filteredCatalogerConfigMappings
|
||||||
|
}
|
||||||
|
|
||||||
type orphanInfo struct {
|
type orphanInfo struct {
|
||||||
catalogerName string
|
catalogerName string
|
||||||
parserFunction string
|
parserFunction string
|
||||||
@ -301,11 +339,11 @@ func (r *CatalogerRegistry) AllCatalogers() []capabilities.CatalogerInfo {
|
|||||||
type EnrichmentData struct {
|
type EnrichmentData struct {
|
||||||
metadata map[string][]string
|
metadata map[string][]string
|
||||||
packageTypes map[string][]string
|
packageTypes map[string][]string
|
||||||
binaryClassifiers []binary.Classifier
|
binaryClassifiers []binary.Classifier //nolint:staticcheck
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewEnrichmentData creates a new enrichment data container
|
// NewEnrichmentData creates a new enrichment data container
|
||||||
func NewEnrichmentData(metadata, packageTypes map[string][]string, binaryClassifiers []binary.Classifier) *EnrichmentData {
|
func NewEnrichmentData(metadata, packageTypes map[string][]string, binaryClassifiers []binary.Classifier) *EnrichmentData { //nolint:staticcheck
|
||||||
return &EnrichmentData{
|
return &EnrichmentData{
|
||||||
metadata: metadata,
|
metadata: metadata,
|
||||||
packageTypes: packageTypes,
|
packageTypes: packageTypes,
|
||||||
@ -584,7 +622,7 @@ func mergeDiscoveredWithExisting(
|
|||||||
discovered map[string]DiscoveredCataloger,
|
discovered map[string]DiscoveredCataloger,
|
||||||
customMetadata map[string][]string,
|
customMetadata map[string][]string,
|
||||||
customPackageTypes map[string][]string,
|
customPackageTypes map[string][]string,
|
||||||
binaryClassifiers []binary.Classifier,
|
binaryClassifiers []binary.Classifier, //nolint:staticcheck
|
||||||
allCatalogers []capabilities.CatalogerInfo,
|
allCatalogers []capabilities.CatalogerInfo,
|
||||||
existing *capabilities.Document,
|
existing *capabilities.Document,
|
||||||
configs map[string]capabilities.CatalogerConfigEntry,
|
configs map[string]capabilities.CatalogerConfigEntry,
|
||||||
|
|||||||
@ -1308,7 +1308,7 @@ catalogers:
|
|||||||
- name: package_manager.package_integrity_hash
|
- name: package_manager.package_integrity_hash
|
||||||
default: true
|
default: true
|
||||||
evidence:
|
evidence:
|
||||||
- CocoapodsLockEntry.Checksum
|
- CocoaPodfileLockEntry.Checksum
|
||||||
- ecosystem: swift # MANUAL
|
- ecosystem: swift # MANUAL
|
||||||
name: swift-package-manager-cataloger # AUTO-GENERATED
|
name: swift-package-manager-cataloger # AUTO-GENERATED
|
||||||
type: generic # AUTO-GENERATED
|
type: generic # AUTO-GENERATED
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user