Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 88 additions & 1 deletion pkg/iac/adapters/terraform/azure/compute/adapt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -299,12 +299,99 @@ resource "azurerm_windows_virtual_machine" "example" {
},
},
},
{
name: "with network interface security group association",
terraform: `
resource "azurerm_windows_virtual_machine" "example" {
name = "example-machine"
Comment on lines +302 to +306
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why add this test for the compute service?

Copy link
Contributor Author

@Dharma-09 Dharma-09 Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your review. Here is my reasoning:
It’s related through the VM adaptation path.

I add compute test because I want to validate the real failing path (VM -> NIC -> NSG association), not just NIC parsing in isolation. We can avoid this test by adding regression test for AZU-0068

resource_group_name = "example-resources"
location = "East US"
size = "Standard_F2"
network_interface_ids = [
azurerm_network_interface.example.id,
]
admin_username = "adminuser"
admin_password = "P@ssw0rd1234!"

os_disk {
caching = "ReadWrite"
storage_account_type = "Standard_LRS"
}
}

resource "azurerm_network_interface" "example" {
name = "example-nic"
location = "eastus"
resource_group_name = "example-rg"

ip_configuration {
name = "internal"
public_ip_address_id = "test-public-ip-id"
}
}

resource "azurerm_network_security_group" "example" {
name = "example-nsg"
location = "eastus"
resource_group_name = "example-rg"

security_rule {
name = "test123"
priority = 100
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "*"
source_address_prefix = "*"
destination_address_prefix = "*"
}
}

resource "azurerm_network_interface_security_group_association" "example" {
network_interface_id = azurerm_network_interface.example.id
network_security_group_id = azurerm_network_security_group.example.id
}
`,
expected: compute.WindowsVirtualMachine{
VirtualMachine: compute.VirtualMachine{
NetworkInterfaces: []network.NetworkInterface{
{
HasPublicIP: iacTypes.BoolTest(true),
PublicIPAddress: iacTypes.StringTest("test-public-ip-id"),
IPConfigurations: []network.IPConfiguration{
{
HasPublicIP: iacTypes.BoolTest(true),
PublicIPAddress: iacTypes.StringTest("test-public-ip-id"),
},
},
SecurityGroups: []network.SecurityGroup{
{
Rules: []network.SecurityGroupRule{
{
Allow: iacTypes.BoolTest(true),
Protocol: iacTypes.StringTest("Tcp"),
DestinationAddresses: []iacTypes.StringValue{iacTypes.StringTest("*")},
DestinationPorts: []common.PortRange{common.FullPortRange(iacTypes.NewTestMetadata())},
SourceAddresses: []iacTypes.StringValue{iacTypes.StringTest("*")},
SourcePorts: []common.PortRange{common.FullPortRange(iacTypes.NewTestMetadata())},
},
},
},
},
},
},
},
},
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
modules := tftestutil.CreateModulesFromSource(t, test.terraform, ".tf")
adapted := adaptWindowsVM(modules.GetBlocks()[0], modules)
resources := modules.GetResourcesByType("azurerm_windows_virtual_machine", AzureVirtualMachine)
require.NotEmpty(t, resources)
adapted := adaptWindowsVM(resources[0], modules)
testutil.AssertDefsecEqual(t, test.expected, adapted)
})
}
Expand Down
48 changes: 43 additions & 5 deletions pkg/iac/adapters/terraform/azure/network/adapt.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/aquasecurity/trivy/pkg/iac/providers/azure/network"
"github.com/aquasecurity/trivy/pkg/iac/terraform"
iacTypes "github.com/aquasecurity/trivy/pkg/iac/types"
"github.com/aquasecurity/trivy/pkg/set"
xslices "github.com/aquasecurity/trivy/pkg/x/slices"
)

Expand Down Expand Up @@ -135,11 +136,7 @@ func AdaptNetworkInterface(resource *terraform.Block, modules terraform.Modules)
SubnetID: iacTypes.StringDefault("", resource.GetMetadata()),
}

if nsgAttr := resource.GetAttribute("network_security_group_id"); nsgAttr.IsNotNil() {
if referencedNSG, err := modules.GetReferencedBlock(nsgAttr, resource); err == nil {
ni.SecurityGroups = []network.SecurityGroup{adaptSecurityGroupFromBlock(referencedNSG)}
}
}
ni.SecurityGroups = resolveNetworkInterfaceSecurityGroups(resource, modules)

ipConfigs := resource.GetBlocks("ip_configuration")
ni.IPConfigurations = make([]network.IPConfiguration, 0, len(ipConfigs))
Expand All @@ -156,6 +153,47 @@ func AdaptNetworkInterface(resource *terraform.Block, modules terraform.Modules)
return ni
}

func resolveNetworkInterfaceSecurityGroups(resource *terraform.Block, modules terraform.Modules) []network.SecurityGroup {
associations := modules.GetReferencingResources(
resource,
"azurerm_network_interface_security_group_association",
"network_interface_id",
)
seen := set.New[string]()
securityGroups := make([]network.SecurityGroup, 0, len(associations)+1)

addSecurityGroup := func(attr *terraform.Attribute, parent *terraform.Block) {
if attr == nil || attr.IsNil() {
return
}

referencedNSG, err := modules.GetReferencedBlock(attr, parent)
if err != nil || referencedNSG == nil {
return
}

if seen.Contains(referencedNSG.ID()) {
return
}
seen.Append(referencedNSG.ID())
securityGroups = append(securityGroups, adaptSecurityGroupFromBlock(referencedNSG))
}

// Backward compatibility for deprecated inline NIC NSG association.
addSecurityGroup(resource.GetAttribute("network_security_group_id"), resource)

// Current provider behavior uses explicit association resources.
for _, association := range associations {
addSecurityGroup(association.GetAttribute("network_security_group_id"), association)
}

if len(securityGroups) == 0 {
return nil
}

return securityGroups
}

func adaptSecurityGroupFromBlock(resource *terraform.Block) network.SecurityGroup {
return network.SecurityGroup{
Metadata: resource.GetMetadata(),
Expand Down
73 changes: 73 additions & 0 deletions pkg/iac/adapters/terraform/azure/network/adapt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,79 @@ func Test_adaptWatcherLog(t *testing.T) {
}
}

func Test_AdaptNetworkInterface_AssociationSecurityGroup(t *testing.T) {
tests := []struct {
name string
terraform string
expected int
}{
{
name: "security group via association resource",
terraform: `
resource "azurerm_network_security_group" "example" {
name = "example-nsg"
}

resource "azurerm_network_interface" "example" {
name = "example-nic"
location = "eastus"
resource_group_name = "example-rg"

ip_configuration {
name = "primary"
subnet_id = "subnet-primary-id"
private_ip_address_allocation = "Dynamic"
}
}

resource "azurerm_network_interface_security_group_association" "example" {
network_interface_id = azurerm_network_interface.example.id
network_security_group_id = azurerm_network_security_group.example.id
}
`,
expected: 1,
},
{
name: "security group deduplicated when legacy and association both set",
terraform: `
resource "azurerm_network_security_group" "example" {
name = "example-nsg"
}

resource "azurerm_network_interface" "example" {
name = "example-nic"
location = "eastus"
resource_group_name = "example-rg"
network_security_group_id = azurerm_network_security_group.example.id

ip_configuration {
name = "primary"
subnet_id = "subnet-primary-id"
private_ip_address_allocation = "Dynamic"
}
}

resource "azurerm_network_interface_security_group_association" "example" {
network_interface_id = azurerm_network_interface.example.id
network_security_group_id = azurerm_network_security_group.example.id
}
`,
expected: 1,
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
modules := tftestutil.CreateModulesFromSource(t, test.terraform, ".tf")
nics := modules.GetResourcesByType("azurerm_network_interface")
require.Len(t, nics, 1)

adapted := AdaptNetworkInterface(nics[0], modules)
require.Len(t, adapted.SecurityGroups, test.expected)
})
}
}

func TestLines(t *testing.T) {
src := `
resource "azurerm_network_security_group" "example" {
Expand Down