diff --git a/syft/pkg/cataloger/internal/cpegenerate/generate.go b/syft/pkg/cataloger/internal/cpegenerate/generate.go index ae03cabe7..b270bea6b 100644 --- a/syft/pkg/cataloger/internal/cpegenerate/generate.go +++ b/syft/pkg/cataloger/internal/cpegenerate/generate.go @@ -234,6 +234,10 @@ func candidateTargetSw(p pkg.Package) []string { // NVD records CPAN distributions with perl in target_sw, e.g. // cpe:2.3:a:mojolicious:mojolicious:*:*:*:*:*:perl:*:* return []string{"perl"} + case p.Type == pkg.TerraformPkg: + // NVD records terraform providers with the provider type in target_sw, e.g. + // cpe:2.3:a:hashicorp:terraform_provider:*:*:*:*:*:vault:*:* + return candidateTargetSwForTerraformProvider(p) } return []string{cpe.Any} @@ -332,6 +336,9 @@ func candidateVendorsByType(p pkg.Package, vendors fieldCandidateSet) fieldCandi case pkg.WordpressPluginEntry: vendors.clear() vendors.union(candidateVendorsForWordpressPlugin(p)) + case pkg.TerraformLockProviderEntry: + vendors.clear() + vendors.union(candidateVendorsForTerraformProvider(p)) } return vendors } @@ -377,6 +384,9 @@ func candidateProductSet(p pkg.Package) fieldCandidateSet { case pkg.WordpressPluginEntry: products.clear() products.union(candidateProductsForWordpressPlugin(p)) + case pkg.TerraformLockProviderEntry: + products.clear() + products.union(candidateProductsForTerraformProvider(p)) } // it is never OK to have candidates with these values ["" and "*"] (since CPEs will match any other value) diff --git a/syft/pkg/cataloger/internal/cpegenerate/generate_test.go b/syft/pkg/cataloger/internal/cpegenerate/generate_test.go index 4b849a709..6a0730890 100644 --- a/syft/pkg/cataloger/internal/cpegenerate/generate_test.go +++ b/syft/pkg/cataloger/internal/cpegenerate/generate_test.go @@ -902,6 +902,38 @@ func TestGeneratePackageCPEs(t *testing.T) { }, expected: []string{}, }, + { + name: "terraform provider generates NVD terraform_provider CPEs not application CPEs", + p: pkg.Package{ + Name: "registry.terraform.io/grafana/grafana", + Version: "4.45.2", + Type: pkg.TerraformPkg, + Language: pkg.Go, + Metadata: pkg.TerraformLockProviderEntry{ + URL: "registry.terraform.io/grafana/grafana", + Version: "4.45.2", + }, + }, + expected: []string{ + "cpe:2.3:a:grafana:terraform_provider:4.45.2:*:*:*:*:grafana:*:*", + }, + }, + { + name: "terraform provider with different namespace and type", + p: pkg.Package{ + Name: "registry.terraform.io/hashicorp/aws", + Version: "5.72.1", + Type: pkg.TerraformPkg, + Language: pkg.Go, + Metadata: pkg.TerraformLockProviderEntry{ + URL: "registry.terraform.io/hashicorp/aws", + Version: "5.72.1", + }, + }, + expected: []string{ + "cpe:2.3:a:hashicorp:terraform_provider:5.72.1:*:*:*:*:aws:*:*", + }, + }, { name: "rust package", p: pkg.Package{ diff --git a/syft/pkg/cataloger/internal/cpegenerate/terraform.go b/syft/pkg/cataloger/internal/cpegenerate/terraform.go new file mode 100644 index 000000000..67f5ce495 --- /dev/null +++ b/syft/pkg/cataloger/internal/cpegenerate/terraform.go @@ -0,0 +1,56 @@ +package cpegenerate + +import ( + "strings" + + "github.com/anchore/syft/syft/cpe" + "github.com/anchore/syft/syft/pkg" +) + +// NVD records terraform providers with a fixed "terraform_provider" product and the provider type in target_sw, e.g. +// cpe:2.3:a:hashicorp:terraform_provider:*:*:*:*:*:vault:*:* +const terraformProviderProduct = "terraform_provider" + +func candidateVendorsForTerraformProvider(p pkg.Package) fieldCandidateSet { + vendors := newFieldCandidateSet() + + namespace, _ := parseTerraformProviderName(p.Name) + if namespace != "" { + vendors.addValue(namespace) + } + + return vendors +} + +func candidateProductsForTerraformProvider(p pkg.Package) fieldCandidateSet { + products := newFieldCandidateSet() + + _, providerType := parseTerraformProviderName(p.Name) + if providerType != "" { + products.add(fieldCandidate{ + value: terraformProviderProduct, + disallowSubSelections: true, + disallowDelimiterVariations: true, + }) + } + + return products +} + +func candidateTargetSwForTerraformProvider(p pkg.Package) []string { + _, providerType := parseTerraformProviderName(p.Name) + if providerType == "" { + return []string{cpe.Any} + } + return []string{providerType} +} + +// parseTerraformProviderName extracts the namespace and type from a terraform provider +// source address. The expected format is "registry.terraform.io//". +func parseTerraformProviderName(name string) (namespace, providerType string) { + parts := strings.Split(name, "/") + if len(parts) < 3 { + return "", "" + } + return parts[len(parts)-2], parts[len(parts)-1] +} diff --git a/syft/pkg/cataloger/internal/cpegenerate/terraform_test.go b/syft/pkg/cataloger/internal/cpegenerate/terraform_test.go new file mode 100644 index 000000000..a21b37ddc --- /dev/null +++ b/syft/pkg/cataloger/internal/cpegenerate/terraform_test.go @@ -0,0 +1,172 @@ +package cpegenerate + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/anchore/syft/syft/cpe" + "github.com/anchore/syft/syft/pkg" +) + +func Test_candidateVendorsForTerraformProvider(t *testing.T) { + tests := []struct { + name string + pkg pkg.Package + expected []string + }{ + { + name: "hashicorp/aws provider", + pkg: pkg.Package{ + Name: "registry.terraform.io/hashicorp/aws", + }, + expected: []string{"hashicorp"}, + }, + { + name: "grafana/grafana provider", + pkg: pkg.Package{ + Name: "registry.terraform.io/grafana/grafana", + }, + expected: []string{"grafana"}, + }, + { + name: "hashicorp/google provider", + pkg: pkg.Package{ + Name: "registry.terraform.io/hashicorp/google", + }, + expected: []string{"hashicorp"}, + }, + { + name: "malformed name with fewer than 3 segments", + pkg: pkg.Package{ + Name: "hashicorp/aws", + }, + expected: []string{}, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + actual := candidateVendorsForTerraformProvider(test.pkg).uniqueValues() + assert.ElementsMatch(t, test.expected, actual) + }) + } +} + +func Test_candidateProductsForTerraformProvider(t *testing.T) { + tests := []struct { + name string + pkg pkg.Package + expected []string + }{ + { + name: "hashicorp/aws provider", + pkg: pkg.Package{ + Name: "registry.terraform.io/hashicorp/aws", + }, + expected: []string{"terraform_provider"}, + }, + { + name: "grafana/grafana provider", + pkg: pkg.Package{ + Name: "registry.terraform.io/grafana/grafana", + }, + expected: []string{"terraform_provider"}, + }, + { + name: "hashicorp/google provider", + pkg: pkg.Package{ + Name: "registry.terraform.io/hashicorp/google", + }, + expected: []string{"terraform_provider"}, + }, + { + name: "malformed name with fewer than 3 segments", + pkg: pkg.Package{ + Name: "hashicorp/aws", + }, + expected: []string{}, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + actual := candidateProductsForTerraformProvider(test.pkg).uniqueValues() + assert.ElementsMatch(t, test.expected, actual) + }) + } +} + +func Test_candidateTargetSwForTerraformProvider(t *testing.T) { + tests := []struct { + name string + pkg pkg.Package + expected []string + }{ + { + name: "hashicorp/aws provider", + pkg: pkg.Package{ + Name: "registry.terraform.io/hashicorp/aws", + }, + expected: []string{"aws"}, + }, + { + name: "grafana/grafana provider", + pkg: pkg.Package{ + Name: "registry.terraform.io/grafana/grafana", + }, + expected: []string{"grafana"}, + }, + { + name: "malformed name with fewer than 3 segments", + pkg: pkg.Package{ + Name: "hashicorp/aws", + }, + expected: []string{cpe.Any}, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + assert.Equal(t, test.expected, candidateTargetSwForTerraformProvider(test.pkg)) + }) + } +} + +func Test_parseTerraformProviderName(t *testing.T) { + tests := []struct { + name string + input string + expectedNamespace string + expectedType string + }{ + { + name: "standard provider", + input: "registry.terraform.io/hashicorp/aws", + expectedNamespace: "hashicorp", + expectedType: "aws", + }, + { + name: "provider where namespace matches type", + input: "registry.terraform.io/grafana/grafana", + expectedNamespace: "grafana", + expectedType: "grafana", + }, + { + name: "too few segments", + input: "hashicorp/aws", + expectedNamespace: "", + expectedType: "", + }, + { + name: "single segment", + input: "aws", + expectedNamespace: "", + expectedType: "", + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + namespace, providerType := parseTerraformProviderName(test.input) + assert.Equal(t, test.expectedNamespace, namespace) + assert.Equal(t, test.expectedType, providerType) + }) + } +}