fix(misconf): ensure boolean metadata values are correctly interpreted (#9770)

Signed-off-by: nikpivkin <nikita.pivkin@smartforce.io>
This commit is contained in:
Nikita Pivkin
2025-11-19 12:56:10 +06:00
committed by GitHub
parent c8d5ab7690
commit a6ceff7e83
8 changed files with 182 additions and 42 deletions

View File

@@ -1,8 +1,6 @@
package compute
import (
"github.com/zclconf/go-cty/cty"
"github.com/aquasecurity/trivy/pkg/iac/providers/google/compute"
"github.com/aquasecurity/trivy/pkg/iac/terraform"
iacTypes "github.com/aquasecurity/trivy/pkg/iac/types"
@@ -31,9 +29,6 @@ func adaptInstances(modules terraform.Modules) (instances []compute.Instance) {
OSLoginEnabled: iacTypes.BoolDefault(true, instanceBlock.GetMetadata()),
EnableProjectSSHKeyBlocking: iacTypes.BoolDefault(false, instanceBlock.GetMetadata()),
EnableSerialPort: iacTypes.BoolDefault(false, instanceBlock.GetMetadata()),
NetworkInterfaces: nil,
BootDisks: nil,
AttachedDisks: nil,
}
// network interfaces
@@ -60,16 +55,11 @@ func adaptInstances(modules terraform.Modules) (instances []compute.Instance) {
}
// metadata
if metadataAttr := instanceBlock.GetAttribute("metadata"); metadataAttr.IsNotNil() {
if val := metadataAttr.MapValue("enable-oslogin"); val.Type() == cty.Bool {
instance.OSLoginEnabled = iacTypes.BoolExplicit(val.True(), metadataAttr.GetMetadata())
}
if val := metadataAttr.MapValue("block-project-ssh-keys"); val.Type() == cty.Bool {
instance.EnableProjectSSHKeyBlocking = iacTypes.BoolExplicit(val.True(), metadataAttr.GetMetadata())
}
if val := metadataAttr.MapValue("serial-port-enable"); val.Type() == cty.Bool {
instance.EnableSerialPort = iacTypes.BoolExplicit(val.True(), metadataAttr.GetMetadata())
}
if attr := instanceBlock.GetAttribute("metadata"); attr.IsNotNil() {
flags := parseMetadataFlags(attr)
instance.OSLoginEnabled = flags.EnableOSLogin
instance.EnableProjectSSHKeyBlocking = flags.BlockProjectSSHKeys
instance.EnableSerialPort = flags.EnableSerialPort
}
// disks

View File

@@ -81,10 +81,8 @@ func Test_adaptInstances(t *testing.T) {
IsDefault: iacTypes.Bool(false, iacTypes.NewTestMetadata()),
},
CanIPForward: iacTypes.Bool(true, iacTypes.NewTestMetadata()),
OSLoginEnabled: iacTypes.Bool(false, iacTypes.NewTestMetadata()),
EnableProjectSSHKeyBlocking: iacTypes.Bool(true, iacTypes.NewTestMetadata()),
EnableSerialPort: iacTypes.Bool(true, iacTypes.NewTestMetadata()),
BootDisks: []compute.Disk{
{
Metadata: iacTypes.NewTestMetadata(),
@@ -155,6 +153,26 @@ func Test_adaptInstances(t *testing.T) {
},
},
},
{
name: "handles metadata values in various formats",
terraform: `resource "google_compute_instance" "example" {
name = "test"
metadata = {
enable-oslogin = "True"
block-project-ssh-keys = 1
serial-port-enable = "yes"
}
}`,
expected: []compute.Instance{
{
Name: iacTypes.StringTest("test"),
OSLoginEnabled: iacTypes.BoolTest(true),
EnableSerialPort: iacTypes.BoolTest(true),
EnableProjectSSHKeyBlocking: iacTypes.BoolTest(true),
},
},
},
}
for _, test := range tests {

View File

@@ -1,27 +1,49 @@
package compute
import (
"github.com/zclconf/go-cty/cty"
"github.com/aquasecurity/trivy/pkg/iac/providers/google/compute"
"github.com/aquasecurity/trivy/pkg/iac/terraform"
iacTypes "github.com/aquasecurity/trivy/pkg/iac/types"
)
// TODO: add support for google_compute_project_metadata_item
// https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/compute_project_metadata_item
func adaptProjectMetadata(modules terraform.Modules) compute.ProjectMetadata {
metadata := compute.ProjectMetadata{
Metadata: iacTypes.NewUnmanagedMetadata(),
EnableOSLogin: iacTypes.BoolUnresolvable(
iacTypes.NewUnmanagedMetadata(),
),
Metadata: iacTypes.NewUnmanagedMetadata(),
EnableOSLogin: iacTypes.BoolUnresolvable(iacTypes.NewUnmanagedMetadata()),
}
for _, metadataBlock := range modules.GetResourcesByType("google_compute_project_metadata") {
metadata.Metadata = metadataBlock.GetMetadata()
if metadataAttr := metadataBlock.GetAttribute("metadata"); metadataAttr.IsNotNil() {
if val := metadataAttr.MapValue("enable-oslogin"); val.Type() == cty.Bool {
metadata.EnableOSLogin = iacTypes.BoolExplicit(val.True(), metadataAttr.GetMetadata())
}
if attr := metadataBlock.GetAttribute("metadata"); attr.IsNotNil() {
flags := parseMetadataFlags(attr)
metadata.EnableOSLogin = flags.EnableOSLogin
}
}
return metadata
}
func parseMetadataFlags(attr *terraform.Attribute) compute.MetadataFlags {
flags := compute.MetadataFlags{
EnableOSLogin: iacTypes.BoolDefault(false, attr.GetMetadata()),
BlockProjectSSHKeys: iacTypes.BoolDefault(false, attr.GetMetadata()),
EnableSerialPort: iacTypes.BoolDefault(false, attr.GetMetadata()),
}
if attr.IsNil() {
return flags
}
meta := attr.GetMetadata()
if val, ok := iacTypes.BoolFromCtyValue(attr.MapValue("enable-oslogin"), meta); ok {
flags.EnableOSLogin = val
}
if val, ok := iacTypes.BoolFromCtyValue(attr.MapValue("block-project-ssh-keys"), meta); ok {
flags.BlockProjectSSHKeys = val
}
if val, ok := iacTypes.BoolFromCtyValue(attr.MapValue("serial-port-enable"), meta); ok {
flags.EnableSerialPort = val
}
return flags
}

View File

@@ -25,8 +25,7 @@ func Test_adaptProjectMetadata(t *testing.T) {
}
`,
expected: compute.ProjectMetadata{
Metadata: iacTypes.NewTestMetadata(),
EnableOSLogin: iacTypes.Bool(true, iacTypes.NewTestMetadata()),
EnableOSLogin: iacTypes.BoolTest(true),
},
},
{
@@ -38,8 +37,21 @@ func Test_adaptProjectMetadata(t *testing.T) {
}
`,
expected: compute.ProjectMetadata{
Metadata: iacTypes.NewTestMetadata(),
EnableOSLogin: iacTypes.Bool(false, iacTypes.NewTestMetadata()),
EnableOSLogin: iacTypes.BoolTest(false),
},
},
{
name: "handles metadata values in various formats",
terraform: `resource "google_compute_project_metadata" "example" {
metadata = {
enable-oslogin = "TRUE"
block-project-ssh-keys = 1
serial-port-enable = "yes"
}
}
`,
expected: compute.ProjectMetadata{
EnableOSLogin: iacTypes.BoolTest(true),
},
},
}

View File

@@ -2,7 +2,6 @@ package gke
import (
"github.com/google/uuid"
"github.com/zclconf/go-cty/cty"
"github.com/aquasecurity/trivy/pkg/iac/providers/google/gke"
"github.com/aquasecurity/trivy/pkg/iac/terraform"
@@ -285,16 +284,8 @@ func adaptNodeConfig(resource *terraform.Block) gke.NodeConfig {
if metadata := resource.GetAttribute("metadata"); metadata.IsNotNil() {
disableLegacy := metadata.MapValue("disable-legacy-endpoints")
if disableLegacy.IsKnown() {
var enableLegacyEndpoints bool
switch disableLegacy.Type() {
case cty.Bool:
enableLegacyEndpoints = disableLegacy.False()
case cty.String:
enableLegacyEndpoints = disableLegacy.AsString() == "false"
}
config.EnableLegacyEndpoints = iacTypes.Bool(enableLegacyEndpoints, metadata.GetMetadata())
if val, ok := iacTypes.BoolFromCtyValue(disableLegacy, metadata.GetMetadata()); ok {
config.EnableLegacyEndpoints = val.Invert()
}
}

View File

@@ -4,6 +4,12 @@ import (
iacTypes "github.com/aquasecurity/trivy/pkg/iac/types"
)
type MetadataFlags struct {
EnableOSLogin iacTypes.BoolValue
BlockProjectSSHKeys iacTypes.BoolValue
EnableSerialPort iacTypes.BoolValue
}
type ProjectMetadata struct {
Metadata iacTypes.Metadata
EnableOSLogin iacTypes.BoolValue

View File

@@ -2,6 +2,9 @@ package types
import (
"encoding/json"
"strings"
"github.com/zclconf/go-cty/cty"
)
type BoolValue struct {
@@ -89,8 +92,55 @@ func (b BoolValue) IsFalse() bool {
return !b.Value()
}
func (b BoolValue) Invert() BoolValue {
return BoolValue{
BaseAttribute: b.BaseAttribute,
value: !b.value,
}
}
func (b BoolValue) ToRego() any {
m := b.metadata.ToRego().(map[string]any)
m["value"] = b.Value()
return m
}
// BoolFromCtyValue converts a cty.Value to iacTypes.BoolValue.
// Returns the BoolValue and true if conversion to bool succeeded.
func BoolFromCtyValue(val cty.Value, metadata Metadata) (BoolValue, bool) {
if val.IsNull() || !val.IsKnown() {
return BoolUnresolvable(metadata), false
}
unmarked, _ := val.Unmark()
v, ok := ctyToBool(unmarked)
if !ok {
return BoolUnresolvable(metadata), false
}
return BoolExplicit(v, metadata), true
}
func ctyToBool(val cty.Value) (bool, bool) {
switch val.Type() {
case cty.Bool:
return val.True(), true
case cty.String:
switch strings.ToLower(val.AsString()) {
case "true", "yes", "y", "1":
return true, true
case "false", "no", "n", "0":
return false, true
}
case cty.Number:
v, _ := val.AsBigFloat().Int64()
switch v {
case 1:
return true, true
case 0:
return false, true
}
}
return false, false
}

View File

@@ -6,6 +6,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zclconf/go-cty/cty"
)
var fakeMetadata = NewMetadata(NewRange("main.tf", 123, 123, "", nil), "")
@@ -43,3 +44,53 @@ func Test_BoolJSON(t *testing.T) {
assert.Equal(t, val, restored)
}
func TestGetBoolFromValue(t *testing.T) {
metadata := NewTestMetadata()
tests := []struct {
name string
ctyVal cty.Value
expected bool
ok bool
}{
// Bool
{"bool true", cty.BoolVal(true), true, true},
{"bool false", cty.BoolVal(false), false, true},
// Strings (true)
{"string 'true'", cty.StringVal("true"), true, true},
{"string 'TRUE'", cty.StringVal("TRUE"), true, true},
{"string 'yes'", cty.StringVal("yes"), true, true},
{"string '1'", cty.StringVal("1"), true, true},
// Strings (false)
{"string 'false'", cty.StringVal("false"), false, true},
{"string 'NO'", cty.StringVal("NO"), false, true},
{"string '0'", cty.StringVal("0"), false, true},
// Numbers
{"number 1", cty.NumberIntVal(1), true, true},
{"number 0", cty.NumberIntVal(0), false, true},
{"number 42 (invalid)", cty.NumberIntVal(42), false, false},
// Null / Unknown
{"null", cty.NullVal(cty.Bool), false, false},
{"unknown", cty.UnknownVal(cty.Bool), false, false},
// Invalid string
{"string 'maybe'", cty.StringVal("maybe"), false, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, ok := BoolFromCtyValue(tt.ctyVal, metadata)
assert.Equal(t, tt.ok, ok)
if ok {
assert.Equal(t, tt.expected, got.Value())
} else {
assert.False(t, got.Value())
}
})
}
}