mirror of
https://github.com/anchore/syft.git
synced 2026-11-04 08:26:38 +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.
|
||||
// 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)
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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