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
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/anchore/clio"
|
||||
@ -14,8 +16,16 @@ func Cataloger(app clio.Application) *cobra.Command {
|
||||
|
||||
cmd.AddCommand(
|
||||
CatalogerList(app),
|
||||
CatalogerInfo(app),
|
||||
)
|
||||
|
||||
// only add cataloger info command if experimental capabilities feature is enabled
|
||||
if isCapabilitiesExperimentEnabled() {
|
||||
cmd.AddCommand(CatalogerInfo(app))
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func isCapabilitiesExperimentEnabled() bool {
|
||||
return os.Getenv("SYFT_EXP_CAPABILITIES") == "true"
|
||||
}
|
||||
|
||||
@ -27,6 +27,50 @@ var (
|
||||
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"`
|
||||
Names []string // cataloger names from args
|
||||
@ -73,6 +117,7 @@ func runCatalogerInfo(opts *catalogerInfoOptions) error {
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@ -104,47 +149,6 @@ func catalogerInfoReport(opts *catalogerInfoOptions, doc *capabilities.Document,
|
||||
}
|
||||
|
||||
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 {
|
||||
Catalogers []catalogerInfo `json:"catalogers"`
|
||||
}
|
||||
@ -157,21 +161,42 @@ func renderCatalogerInfoJSON(doc *capabilities.Document, catalogers []capabiliti
|
||||
Name: cat.Name,
|
||||
Type: cat.Type,
|
||||
Selectors: cat.Selectors,
|
||||
Deprecated: isDeprecatedCataloger(cat.Selectors),
|
||||
}
|
||||
|
||||
// check if cataloger is deprecated based on selectors
|
||||
for _, selector := range 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" {
|
||||
info.Deprecated = true
|
||||
break
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
for _, parser := range cat.Parsers {
|
||||
// convert detector packages
|
||||
var pkgs []detectorPackageInfo
|
||||
for _, pkg := range parser.Detector.Packages {
|
||||
pkgs = append(pkgs, detectorPackageInfo{
|
||||
// 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,
|
||||
@ -179,55 +204,55 @@ func renderCatalogerInfoJSON(doc *capabilities.Document, catalogers []capabiliti
|
||||
Type: pkg.Type,
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
pi := patternInfo{
|
||||
// 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: pkgs,
|
||||
Packages: convertDetectorPackages(parser.Detector.Packages),
|
||||
Comment: parser.Detector.Comment,
|
||||
PackageTypes: parser.PackageTypes,
|
||||
JSONSchemaTypes: parser.JSONSchemaTypes,
|
||||
Capabilities: parser.Capabilities,
|
||||
}
|
||||
|
||||
info.Patterns = append(info.Patterns, pi)
|
||||
}
|
||||
|
||||
if len(info.Patterns) == 0 {
|
||||
info.Capabilities = cat.Capabilities
|
||||
|
||||
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,
|
||||
})
|
||||
}
|
||||
return patterns
|
||||
}
|
||||
|
||||
pi := patternInfo{
|
||||
// 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: pkgs,
|
||||
Packages: convertDetectorPackages(det.Packages),
|
||||
Comment: det.Comment,
|
||||
PackageTypes: cat.PackageTypes,
|
||||
JSONSchemaTypes: cat.JSONSchemaTypes,
|
||||
}
|
||||
info.Patterns = append(info.Patterns, pi)
|
||||
}
|
||||
PackageTypes: packageTypes,
|
||||
JSONSchemaTypes: jsonSchemaTypes,
|
||||
})
|
||||
}
|
||||
return patterns
|
||||
}
|
||||
|
||||
// add config information
|
||||
if cat.Config != "" {
|
||||
if configEntry, ok := doc.Configs[cat.Config]; ok {
|
||||
// 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: cat.Config,
|
||||
Type: configType,
|
||||
}
|
||||
for _, field := range configEntry.Fields {
|
||||
cfg.Fields = append(cfg.Fields, configFieldInfo{
|
||||
@ -236,15 +261,7 @@ func renderCatalogerInfoJSON(doc *capabilities.Document, catalogers []capabiliti
|
||||
AppKey: field.AppKey,
|
||||
})
|
||||
}
|
||||
info.Config = cfg
|
||||
}
|
||||
}
|
||||
|
||||
docOut.Catalogers = append(docOut.Catalogers, info)
|
||||
}
|
||||
|
||||
by, err := json.Marshal(docOut)
|
||||
return string(by), err
|
||||
return cfg
|
||||
}
|
||||
|
||||
func renderCatalogerInfoTable(_ *capabilities.Document, catalogers []capabilities.CatalogerEntry) string {
|
||||
@ -391,16 +408,49 @@ func extractArrayCapability(caps capabilities.CapabilitySet, name string) string
|
||||
func extractNodesCapability(caps capabilities.CapabilitySet) string {
|
||||
for _, cap := range caps {
|
||||
if cap.Name == "dependency.depth" {
|
||||
// handle various array types
|
||||
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("·")
|
||||
}
|
||||
// check if both direct and indirect are present
|
||||
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 v {
|
||||
for _, item := range items {
|
||||
if item == "direct" {
|
||||
hasDirect = true
|
||||
}
|
||||
@ -408,36 +458,7 @@ func extractNodesCapability(caps capabilities.CapabilitySet) string {
|
||||
hasIndirect = true
|
||||
}
|
||||
}
|
||||
if hasDirect && hasIndirect {
|
||||
return "transitive"
|
||||
}
|
||||
return strings.Join(v, ", ")
|
||||
case []interface{}:
|
||||
if len(v) == 0 {
|
||||
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("·")
|
||||
return hasDirect && hasIndirect
|
||||
}
|
||||
|
||||
func formatCriteria(detectors []capabilities.Detector) string {
|
||||
|
||||
@ -1,720 +1 @@
|
||||
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
|
||||
// from the DescribeFields method
|
||||
func findAppConfigStructAndDescriptions(f *ast.File, topLevelKey string) (*ast.StructType, map[string]string) {
|
||||
var configStruct *ast.StructType
|
||||
descriptions := make(map[string]string)
|
||||
expectedName := determineExpectedConfigName(topLevelKey)
|
||||
configStruct := findConfigStruct(f, expectedName)
|
||||
descriptions := extractDescriptionsFromDescribeFields(f)
|
||||
return configStruct, descriptions
|
||||
}
|
||||
|
||||
// determine the expected main config struct name based on the top-level key
|
||||
// e.g., "dotnet" -> "dotnetConfig", "golang" -> "golangConfig", "linux-kernel" -> "linuxKernelConfig"
|
||||
expectedName := topLevelKey + "Config"
|
||||
// handle special case for linux-kernel
|
||||
if topLevelKey == "linux-kernel" {
|
||||
expectedName = "linuxKernelConfig"
|
||||
}
|
||||
// handle javascript
|
||||
if topLevelKey == "javascript" {
|
||||
expectedName = "javaScriptConfig"
|
||||
// determineExpectedConfigName maps the top-level key to the expected config struct name
|
||||
func determineExpectedConfigName(topLevelKey string) string {
|
||||
// handle special cases first
|
||||
switch topLevelKey {
|
||||
case "linux-kernel":
|
||||
return "linuxKernelConfig"
|
||||
case "javascript":
|
||||
return "javaScriptConfig"
|
||||
default:
|
||||
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 {
|
||||
genDecl, ok := decl.(*ast.GenDecl)
|
||||
if !ok || genDecl.Tok != token.TYPE {
|
||||
@ -150,18 +155,18 @@ func findAppConfigStructAndDescriptions(f *ast.File, topLevelKey string) (*ast.S
|
||||
continue
|
||||
}
|
||||
|
||||
// match the expected main config struct name
|
||||
if typeSpec.Name.Name == expectedName {
|
||||
configStruct = structType
|
||||
break
|
||||
return structType
|
||||
}
|
||||
}
|
||||
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 {
|
||||
funcDecl, ok := decl.(*ast.FuncDecl)
|
||||
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
|
||||
description := extractStringLiteral(callExpr.Args[1])
|
||||
if description != "" {
|
||||
// clean up multi-line descriptions
|
||||
description = cleanDescription(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
|
||||
|
||||
@ -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
|
||||
// by looking for Name() method implementations or hardcoded name variables
|
||||
func inferCatalogerNameFromCustomImpl(funcDecl *ast.FuncDecl, file *ast.File, ctx *parseContext) string {
|
||||
// look for patterns like:
|
||||
// return &someCataloger{cfg: cfg}
|
||||
// return someCataloger{cfg: cfg}
|
||||
typeName := extractReturnTypeName(funcDecl)
|
||||
if typeName == "" {
|
||||
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
|
||||
|
||||
ast.Inspect(funcDecl.Body, func(n ast.Node) bool {
|
||||
// look for return statements
|
||||
returnStmt, ok := n.(*ast.ReturnStmt)
|
||||
if !ok || len(returnStmt.Results) == 0 {
|
||||
return true
|
||||
@ -197,42 +201,55 @@ func inferCatalogerNameFromCustomImpl(funcDecl *ast.FuncDecl, file *ast.File, ct
|
||||
return true
|
||||
})
|
||||
|
||||
if typeName == "" {
|
||||
return ""
|
||||
}
|
||||
return typeName
|
||||
}
|
||||
|
||||
// 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 {
|
||||
funcDecl, ok := decl.(*ast.FuncDecl)
|
||||
if !ok || funcDecl.Name.Name != "Name" {
|
||||
continue
|
||||
}
|
||||
|
||||
// check if this is a method on our type
|
||||
if funcDecl.Recv == nil || len(funcDecl.Recv.List) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
recvType := 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
|
||||
}
|
||||
}
|
||||
|
||||
recvTypeName := extractReceiverTypeName(funcDecl.Recv.List[0].Type)
|
||||
if recvTypeName != typeName {
|
||||
continue
|
||||
}
|
||||
|
||||
// found the Name() method, extract the return value
|
||||
if funcDecl.Body != nil {
|
||||
if name := extractNameFromMethodBody(funcDecl.Body, ctx); name != "" {
|
||||
return name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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(funcDecl.Body, func(n ast.Node) bool {
|
||||
ast.Inspect(body, func(n ast.Node) bool {
|
||||
returnStmt, ok := n.(*ast.ReturnStmt)
|
||||
if !ok || len(returnStmt.Results) == 0 {
|
||||
return true
|
||||
@ -252,13 +269,7 @@ func inferCatalogerNameFromCustomImpl(funcDecl *ast.FuncDecl, file *ast.File, ct
|
||||
|
||||
return true
|
||||
})
|
||||
if name != "" {
|
||||
return name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// extractConfigParameter extracts the config type from the first parameter of a cataloger constructor.
|
||||
|
||||
@ -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
|
||||
func extractBinaryClassifiers() []binary.Classifier {
|
||||
func extractBinaryClassifiers() []binary.Classifier { //nolint:staticcheck
|
||||
classifiers := binary.DefaultClassifiers()
|
||||
|
||||
// return all classifiers (already sorted by the default function)
|
||||
|
||||
@ -333,10 +333,20 @@ func updateNodeTree(rootNode *yaml.Node, doc *capabilities.Document) error {
|
||||
updateOrAddSection(existingMapping, newMapping, "application")
|
||||
|
||||
// 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")
|
||||
newCatalogersNode := findSectionNode(newMapping, "catalogers")
|
||||
|
||||
if existingCatalogersNode != nil && newCatalogersNode != nil {
|
||||
if existingCatalogersNode == nil || newCatalogersNode == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// create a map of existing cataloger nodes by name for quick lookup
|
||||
existingByName := make(map[string]*yaml.Node)
|
||||
if existingCatalogersNode.Kind == yaml.SequenceNode {
|
||||
@ -372,9 +382,6 @@ func updateNodeTree(rootNode *yaml.Node, doc *capabilities.Document) error {
|
||||
|
||||
// replace the catalogers content
|
||||
existingCatalogersNode.Content = newCatalogersNode.Content
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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) {
|
||||
stats := &Statistics{}
|
||||
|
||||
// 1. Discover generic catalogers from code
|
||||
fmt.Print(" → Scanning source code for generic catalogers...")
|
||||
discovered, err := discoverGenericCatalogers(repoRoot)
|
||||
// 1-2. Discover all cataloger data
|
||||
discovered, customCatalogerMetadata, customCatalogerPackageTypes, binaryClassifiers, allCatalogers, err := discoverAllCatalogerData(repoRoot, stats)
|
||||
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
|
||||
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))
|
||||
|
||||
// 3a. Discover cataloger config structs
|
||||
fmt.Print(" → Discovering cataloger config structs...")
|
||||
configInfoMap, err := DiscoverConfigs(repoRoot)
|
||||
// 3a-3c. Discover and process all config-related information
|
||||
discoveredConfigs, err := discoverAndFilterConfigs(repoRoot)
|
||||
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
|
||||
fmt.Print(" → Filtering configs by pkgcataloging.Config whitelist...")
|
||||
allowedConfigs, err := DiscoverAllowedConfigStructs(repoRoot)
|
||||
discoveredAppConfigs, err := discoverAndConvertAppConfigs(repoRoot)
|
||||
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)
|
||||
if err != nil {
|
||||
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
|
||||
// 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")
|
||||
}
|
||||
filteredCatalogerConfigMappings := applyConfigMappingFilters(catalogerConfigMappings)
|
||||
|
||||
// 4. Build updated catalogers list
|
||||
fmt.Println(" → Merging discovered catalogers with existing entries...")
|
||||
@ -244,6 +132,156 @@ func RegenerateCapabilities(yamlPath string, repoRoot string) (*Statistics, erro
|
||||
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 {
|
||||
catalogerName string
|
||||
parserFunction string
|
||||
@ -301,11 +339,11 @@ func (r *CatalogerRegistry) AllCatalogers() []capabilities.CatalogerInfo {
|
||||
type EnrichmentData struct {
|
||||
metadata map[string][]string
|
||||
packageTypes map[string][]string
|
||||
binaryClassifiers []binary.Classifier
|
||||
binaryClassifiers []binary.Classifier //nolint:staticcheck
|
||||
}
|
||||
|
||||
// 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{
|
||||
metadata: metadata,
|
||||
packageTypes: packageTypes,
|
||||
@ -584,7 +622,7 @@ func mergeDiscoveredWithExisting(
|
||||
discovered map[string]DiscoveredCataloger,
|
||||
customMetadata map[string][]string,
|
||||
customPackageTypes map[string][]string,
|
||||
binaryClassifiers []binary.Classifier,
|
||||
binaryClassifiers []binary.Classifier, //nolint:staticcheck
|
||||
allCatalogers []capabilities.CatalogerInfo,
|
||||
existing *capabilities.Document,
|
||||
configs map[string]capabilities.CatalogerConfigEntry,
|
||||
|
||||
@ -1308,7 +1308,7 @@ catalogers:
|
||||
- name: package_manager.package_integrity_hash
|
||||
default: true
|
||||
evidence:
|
||||
- CocoapodsLockEntry.Checksum
|
||||
- CocoaPodfileLockEntry.Checksum
|
||||
- ecosystem: swift # MANUAL
|
||||
name: swift-package-manager-cataloger # AUTO-GENERATED
|
||||
type: generic # AUTO-GENERATED
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user