mirror of
https://github.com/anchore/syft.git
synced 2026-11-04 16:36:40 +01:00
fix(terraform): generate NVD-style terraform_provider CPEs for terraform lock packages (#5261)
* fix: generate terraform-provider-specific CPEs for terraform lock packages The terraform-lock-cataloger sets Language to Go, which causes the Go CPE heuristic to generate application-level CPEs for terraform providers. For example, registry.terraform.io/grafana/grafana produces the same CPE as the Grafana application itself, leading grype to flag providers with unrelated CVEs. This adds terraform-specific vendor/product candidate functions that generate CPEs matching the NVD convention for terraform providers (e.g. terraform-provider-grafana instead of grafana), with terraform as the target software qualifier. Signed-off-by: Tim Olshansky <456103+timols@users.noreply.github.com> * fix: align terraform provider CPEs with NVD terraform_provider convention 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:*:* Generate <namespace>:terraform_provider:<version>:...:<type> instead of <namespace>:terraform-provider-<type>:<version>:...:terraform, which never appears in NVD. Signed-off-by: Tim Olshansky <456103+timols@users.noreply.github.com> --------- Signed-off-by: Tim Olshansky <456103+timols@users.noreply.github.com>
This commit is contained in:
@@ -234,6 +234,10 @@ func candidateTargetSw(p pkg.Package) []string {
|
|||||||
// NVD records CPAN distributions with perl in target_sw, e.g.
|
// NVD records CPAN distributions with perl in target_sw, e.g.
|
||||||
// cpe:2.3:a:mojolicious:mojolicious:*:*:*:*:*:perl:*:*
|
// cpe:2.3:a:mojolicious:mojolicious:*:*:*:*:*:perl:*:*
|
||||||
return []string{"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}
|
return []string{cpe.Any}
|
||||||
@@ -332,6 +336,9 @@ func candidateVendorsByType(p pkg.Package, vendors fieldCandidateSet) fieldCandi
|
|||||||
case pkg.WordpressPluginEntry:
|
case pkg.WordpressPluginEntry:
|
||||||
vendors.clear()
|
vendors.clear()
|
||||||
vendors.union(candidateVendorsForWordpressPlugin(p))
|
vendors.union(candidateVendorsForWordpressPlugin(p))
|
||||||
|
case pkg.TerraformLockProviderEntry:
|
||||||
|
vendors.clear()
|
||||||
|
vendors.union(candidateVendorsForTerraformProvider(p))
|
||||||
}
|
}
|
||||||
return vendors
|
return vendors
|
||||||
}
|
}
|
||||||
@@ -377,6 +384,9 @@ func candidateProductSet(p pkg.Package) fieldCandidateSet {
|
|||||||
case pkg.WordpressPluginEntry:
|
case pkg.WordpressPluginEntry:
|
||||||
products.clear()
|
products.clear()
|
||||||
products.union(candidateProductsForWordpressPlugin(p))
|
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)
|
// it is never OK to have candidates with these values ["" and "*"] (since CPEs will match any other value)
|
||||||
|
|||||||
@@ -902,6 +902,38 @@ func TestGeneratePackageCPEs(t *testing.T) {
|
|||||||
},
|
},
|
||||||
expected: []string{},
|
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",
|
name: "rust package",
|
||||||
p: pkg.Package{
|
p: pkg.Package{
|
||||||
|
|||||||
@@ -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/<namespace>/<type>".
|
||||||
|
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]
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user