Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Server vulnerability assessment resource #10030

Merged
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
10d4fac
Initial work on new resource server_vulnerability_assessment_resource
martenbohlin Dec 30, 2020
9446b51
Fixed import of server_vulnerability_assessment_resource
martenbohlin Dec 30, 2020
3dc89c6
Fixed lint errors.
martenbohlin Jan 1, 2021
a852f12
Use azurerm_linux_virtual_machine in favor of azurerm_virtual_machine…
martenbohlin Jan 7, 2021
ec6c4b7
Merge remote-tracking branch 'origin/master' into server_vulnerabilit…
martenbohlin Jan 7, 2021
25d81d9
Fied lint errors.
martenbohlin Jan 7, 2021
8e59bda
Allow applying azurerm_server_vulnerability_assessment on Azure ARC (…
martenbohlin Jan 8, 2021
b41219e
Added note in documentation that Azure Defender has to be enabled for…
martenbohlin Jan 9, 2021
6767de6
Merge remote-tracking branch 'origin/master' into server_vulnerabilit…
martenbohlin Jan 9, 2021
a02734c
Added reference to bug in azure-sdk-for-go.
martenbohlin Jan 10, 2021
2ffd690
Improved the azurerm_server_vulnerability_assessment resource by usin…
martenbohlin Jan 10, 2021
a341ee1
Fixed feedback from neil-yechenwei on pull request #10030.
martenbohlin Jan 12, 2021
85288dd
Merge remote-tracking branch 'origin/master' into server_vulnerabilit…
martenbohlin Jan 12, 2021
29d2fc3
Fixed error in documentation meta data.
martenbohlin Jan 12, 2021
2bc6837
The refrenced bug was closed as duplicate, so update the reference to…
martenbohlin Jan 12, 2021
ed9f9c8
Added test asserting that server vulnerability assessment resource gi…
martenbohlin Jan 17, 2021
f81be24
Merge remote-tracking branch 'origin/master' into server_vulnerabilit…
martenbohlin Jan 17, 2021
828015d
Merge remote-tracking branch 'origin/master' into server_vulnerabilit…
martenbohlin Jan 20, 2021
eb51b02
Merge remote-tracking branch 'origin/master' into server_vulnerabilit…
martenbohlin Jan 21, 2021
9e233b7
Merge remote-tracking branch 'origin/master' into server_vulnerabilit…
martenbohlin Jan 30, 2021
b19e769
Revert changes to resourceid since it caused problems with other reso…
martenbohlin Jan 30, 2021
54b7f7d
Changed location in documentation.
martenbohlin Mar 3, 2021
3b7f573
Merge remote-tracking branch 'origin/master' into server_vulnerabilit…
martenbohlin Mar 3, 2021
412a716
Fixed formating after merge.
martenbohlin Mar 3, 2021
bd5cb3f
Improved documentation.
martenbohlin Mar 6, 2021
b493b59
Renamed azurerm_server_vulnerability_assessment to azurerm_security_c…
martenbohlin Mar 6, 2021
a4892eb
Merge remote-tracking branch 'origin/master' into server_vulnerabilit…
martenbohlin Mar 6, 2021
ac3cc59
Merge remote-tracking branch 'origin/master' into server_vulnerabilit…
martenbohlin Mar 9, 2021
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
12 changes: 7 additions & 5 deletions azurerm/helpers/azure/resourceid.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ func ParseAzureResourceID(id string) (*ResourceID, error) {
return nil, fmt.Errorf("The number of path segments is not divisible by 2 in %q", path)
}

var subscriptionID string
var subscriptionID, provider string

// Put the constituent key-value pairs into a map
componentMap := make(map[string]string, len(components)/2)
Expand All @@ -54,9 +54,12 @@ func ParseAzureResourceID(id string) (*ResourceID, error) {

// Catch the subscriptionID before it can be overwritten by another "subscriptions"
// value in the ID which is the case for the Service Bus subscription resource
if key == "subscriptions" && subscriptionID == "" {
switch {
case key == "subscriptions" && subscriptionID == "":
subscriptionID = value
} else {
case key == "providers" && provider == "": // The same for provider for serverVulnerabilityAssessment resource
provider = value
default:
componentMap[key] = value
}
}
Expand All @@ -83,9 +86,8 @@ func ParseAzureResourceID(id string) (*ResourceID, error) {
}

// It is OK not to have a provider in the case of a resource group
if provider, ok := componentMap["providers"]; ok {
if provider != "" {
idObj.Provider = provider
delete(componentMap, "providers")
}

return idObj, nil
Expand Down
33 changes: 19 additions & 14 deletions azurerm/internal/services/securitycenter/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@ import (
)

type Client struct {
ContactsClient *security.ContactsClient
PricingClient *security.PricingsClient
WorkspaceClient *security.WorkspaceSettingsClient
AdvancedThreatProtectionClient *security.AdvancedThreatProtectionClient
AutoProvisioningClient *security.AutoProvisioningSettingsClient
SettingClient *security.SettingsClient
AutomationsClient *security.AutomationsClient
ContactsClient *security.ContactsClient
PricingClient *security.PricingsClient
WorkspaceClient *security.WorkspaceSettingsClient
AdvancedThreatProtectionClient *security.AdvancedThreatProtectionClient
AutoProvisioningClient *security.AutoProvisioningSettingsClient
SettingClient *security.SettingsClient
AutomationsClient *security.AutomationsClient
ServerVulnerabilityAssessmentClient *security.ServerVulnerabilityAssessmentClient
}

func NewClient(o *common.ClientOptions) *Client {
Expand All @@ -39,13 +40,17 @@ func NewClient(o *common.ClientOptions) *Client {
AutomationsClient := security.NewAutomationsClientWithBaseURI(o.ResourceManagerEndpoint, o.SubscriptionId, ascLocation)
o.ConfigureClient(&AutomationsClient.Client, o.ResourceManagerAuthorizer)

ServerVulnerabilityAssessmentClient := security.NewServerVulnerabilityAssessmentClientWithBaseURI(o.ResourceManagerEndpoint, o.SubscriptionId, ascLocation)
o.ConfigureClient(&ServerVulnerabilityAssessmentClient.Client, o.ResourceManagerAuthorizer)

return &Client{
ContactsClient: &ContactsClient,
PricingClient: &PricingClient,
WorkspaceClient: &WorkspaceClient,
AdvancedThreatProtectionClient: &AdvancedThreatProtectionClient,
AutoProvisioningClient: &AutoProvisioningClient,
SettingClient: &SettingClient,
AutomationsClient: &AutomationsClient,
ContactsClient: &ContactsClient,
PricingClient: &PricingClient,
WorkspaceClient: &WorkspaceClient,
AdvancedThreatProtectionClient: &AdvancedThreatProtectionClient,
AutoProvisioningClient: &AutoProvisioningClient,
SettingClient: &SettingClient,
AutomationsClient: &AutomationsClient,
ServerVulnerabilityAssessmentClient: &ServerVulnerabilityAssessmentClient,
}
}
1 change: 1 addition & 0 deletions azurerm/internal/services/securitycenter/registration.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,6 @@ func (r Registration) SupportedResources() map[string]*schema.Resource {
"azurerm_security_center_workspace": resourceSecurityCenterWorkspace(),
"azurerm_security_center_automation": resourceSecurityCenterAutomation(),
"azurerm_security_center_auto_provisioning": resourceSecurityCenterAutoProvisioning(),
"azurerm_server_vulnerability_assessment": resourceServerVulnerabilityAssessment(),
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
package securitycenter

import (
"fmt"
"log"
"time"

"github.com/Azure/azure-sdk-for-go/services/preview/security/mgmt/v3.0/security"

"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
"github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/azure"
"github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/tf"
"github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/clients"
"github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/timeouts"
"github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils"
)

func resourceServerVulnerabilityAssessment() *schema.Resource {
return &schema.Resource{
Create: resourceServerVulnerabilityAssessmentCreate,
Read: resourceServerVulnerabilityAssessmentRead,
Delete: resourceServerVulnerabilityAssessmentDelete,

Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
},

Timeouts: &schema.ResourceTimeout{
Create: schema.DefaultTimeout(5 * time.Minute),
Read: schema.DefaultTimeout(5 * time.Minute),
Update: schema.DefaultTimeout(5 * time.Minute),
Delete: schema.DefaultTimeout(5 * time.Minute),
},

Schema: map[string]*schema.Schema{
"virtual_machine_id": {
martenbohlin marked this conversation as resolved.
Show resolved Hide resolved
Type: schema.TypeString,
Required: true,
ForceNew: true,
martenbohlin marked this conversation as resolved.
Show resolved Hide resolved
ValidateFunc: azure.ValidateResourceID,
},
},
}
}

func resourceServerVulnerabilityAssessmentCreate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*clients.Client).SecurityCenter.ServerVulnerabilityAssessmentClient
ctx, cancel := timeouts.ForCreateUpdate(meta.(*clients.Client).StopContext, d)
defer cancel()

resourceGroup, provider, resourceType, name, _, err := parseVirtualMacineId(d.Get("virtual_machine_id").(string))
if err != nil {
return err
}

vulnerabilityAssessment, err := client.Get(ctx, resourceGroup, provider, resourceType, name)
if err != nil {
if !utils.ResponseWasNotFound(vulnerabilityAssessment.Response) {
return fmt.Errorf("checking for presence of existing Advanced Threat Protection for %q/%q: %+v", resourceGroup, name, err)
}
}

if vulnerabilityAssessment.ID != nil && *vulnerabilityAssessment.ID != "" {
return tf.ImportAsExistsError("azurerm_server_vulnerability_assessment", *vulnerabilityAssessment.ID)
}

vulnerabilityAssessment, err = client.CreateOrUpdate(ctx, resourceGroup, provider, resourceType, name)
if err != nil {
return fmt.Errorf("create Server Vulnerability Assessment for %q/%q: %+v", resourceGroup, name, err)
}

if vulnerabilityAssessment.ID != nil {
d.SetId(*vulnerabilityAssessment.ID)
}

// Take some time for resource to be visible after creating. Hence the retry logic
readError := resourceServerVulnerabilityAssessmentRead(d, meta)
for retry := 1; d.Id() == "" && retry < 11; retry++ {
time.Sleep(time.Duration(retry) * time.Second)
readError = resourceServerVulnerabilityAssessmentRead(d, meta)
}
return readError
}

func resourceServerVulnerabilityAssessmentRead(d *schema.ResourceData, meta interface{}) error {
client := meta.(*clients.Client).SecurityCenter.ServerVulnerabilityAssessmentClient
ctx, cancel := timeouts.ForRead(meta.(*clients.Client).StopContext, d)
defer cancel()

vmId := d.Get("virtual_machine_id").(string)
var idToParse string
if vmId != "" {
idToParse = vmId
} else {
idToParse = d.Id()
}
resourceGroup, provider, resourceType, name, id, err := parseVirtualMacineId(idToParse)
if err != nil {
return err
}

resp, err := client.Get(ctx, resourceGroup, provider, resourceType, name)
if err != nil {
if utils.ResponseWasNotFound(resp.Response) {
log.Printf("Server Vulnerability Assessment was not found for %q/%q: %+v", resourceGroup, name, err)
d.SetId("")
return nil
}

return fmt.Errorf("retrieving Server Vulnerability Assessment status for %q/%q: %+v", resourceGroup, name, err)
}

d.SetId(*resp.ID)
if vmId == "" {
// Need to set virtual_machine_id when we do a import
_ = d.Set("virtual_machine_id", "/subscriptions/"+id.SubscriptionID+
"/resourceGroups/"+id.ResourceGroup+
"/providers/"+provider+"/"+resourceType+"/"+id.Path[resourceType])
}

return nil
}

func resourceServerVulnerabilityAssessmentDelete(d *schema.ResourceData, meta interface{}) error {
client := meta.(*clients.Client).SecurityCenter.ServerVulnerabilityAssessmentClient
ctx, cancel := timeouts.ForDelete(meta.(*clients.Client).StopContext, d)
defer cancel()

resourceGroup, provider, resourceType, name, _, err := parseVirtualMacineId(d.Get("virtual_machine_id").(string))
if err != nil {
return err
}

// Can not delete if still in provisioning state. Wait for it to complete.
for retry := 1; retry <= 20; retry++ {
response, err := client.Get(ctx, resourceGroup, provider, resourceType, name)
if err != nil || response.ProvisioningState != security.ProvisioningState1Provisioning {
break
}
time.Sleep(time.Duration(retry) * time.Second)
}

response, err := client.Delete(ctx, resourceGroup, provider, resourceType, name)
if err != nil {
if utils.ResponseWasStatusCode(response, 202) {
martenbohlin marked this conversation as resolved.
Show resolved Hide resolved
return nil
} else {
return fmt.Errorf("removing Server Vulnerability Assessment for %q/%q: %+v\n", resourceGroup, name, err)
}
} else {
return nil
}
}

func parseVirtualMacineId(stringId string) (string, string, string, string, *azure.ResourceID, error) {
martenbohlin marked this conversation as resolved.
Show resolved Hide resolved
id, err := azure.ParseAzureResourceID(stringId)
if err != nil {
return "", "", "", "", nil, fmt.Errorf("server vulnerability assessment could not parse virtual_machine_id %q %+v", stringId, err)
}
var resourceType string
switch id.Provider {
case "Microsoft.Compute":
resourceType = "virtualMachines"
case "Microsoft.HybridCompute":
resourceType = "machines"
default:
return "", "", "", "", nil, fmt.Errorf("server vulnerability assessment virtual_machine_id references unsuported provider %q, %q", id.Provider, stringId)
}

name := id.Path[resourceType]
if name == "" {
return "", "", "", "", nil, fmt.Errorf("server vulnerability assessment virtual_machine_id is missing name for %s %q", resourceType, stringId)
}

return id.ResourceGroup, id.Provider, resourceType, name, id, nil
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package securitycenter_test

import (
"context"
"fmt"
"testing"

"github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/azure"

"github.com/hashicorp/terraform-plugin-sdk/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/terraform"
"github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/acceptance"
"github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/acceptance/check"
"github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/clients"
"github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils"
)

type ServerVulnerabilityAssessmentResource struct {
martenbohlin marked this conversation as resolved.
Show resolved Hide resolved
}

func TestAccServerVulnerabilityAssessment_basic(t *testing.T) {
data := acceptance.BuildTestData(t, "azurerm_server_vulnerability_assessment", "test")
r := ServerVulnerabilityAssessmentResource{}

data.ResourceTest(t, r, []resource.TestStep{
{
Config: r.basicCfg(data),
Check: resource.ComposeTestCheckFunc(
check.That(data.ResourceName).ExistsInAzure(r),
),
},
data.ImportStep(),
})
}

func (ServerVulnerabilityAssessmentResource) Exists(ctx context.Context, clients *clients.Client, state *terraform.InstanceState) (*bool, error) {
Copy link
Contributor

@neil-yechenwei neil-yechenwei Jan 11, 2021

Choose a reason for hiding this comment

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

Seems this function should also be compatible with Hybrid Machine, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have no way to automatically create a Hybrid Machine. I have only been able to verify that manually.

Copy link
Contributor

@neil-yechenwei neil-yechenwei Jan 13, 2021

Choose a reason for hiding this comment

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

Maybe you can manually create it first via azure rest api and hard-code the resource id of this resource in test case until this Exists function is code complete. Does it make sense?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That will be a bit to complicated for me. But if you want to give it a go I think we can include it.

id, err := azure.ParseAzureResourceID(state.ID)
if err != nil {
return nil, err
}

resourceGroup := id.ResourceGroup
name := id.Path["virtualMachines"]

resp, err := clients.SecurityCenter.ServerVulnerabilityAssessmentClient.Get(ctx, resourceGroup, "Microsoft.Compute", "virtualMachines", name)
if err != nil {
return nil, fmt.Errorf("reading Server Vulnerability Assessment (%s/%s): %+v", resourceGroup, name, err)
}

return utils.Bool(resp.ID != nil), nil
}

func (ServerVulnerabilityAssessmentResource) basicCfg(data acceptance.TestData) string {
return fmt.Sprintf(`
provider "azurerm" {
features {}
}

resource "azurerm_resource_group" "test" {
name = "acctestRG-sc-%[1]d"
martenbohlin marked this conversation as resolved.
Show resolved Hide resolved
location = "%[2]s"
}

resource "azurerm_virtual_network" "test" {
name = "net-%[1]d"
martenbohlin marked this conversation as resolved.
Show resolved Hide resolved
resource_group_name = azurerm_resource_group.test.name
address_space = ["192.168.1.0/24"]
location = azurerm_resource_group.test.location
}

resource "azurerm_subnet" "test" {
name = "snet-%[1]d"
martenbohlin marked this conversation as resolved.
Show resolved Hide resolved
resource_group_name = azurerm_resource_group.test.name
virtual_network_name = azurerm_virtual_network.test.name
address_prefixes = ["192.168.1.0/24"]
}

resource "azurerm_network_interface" "test" {
name = "vm-%[1]d"
martenbohlin marked this conversation as resolved.
Show resolved Hide resolved
location = azurerm_resource_group.test.location
resource_group_name = azurerm_resource_group.test.name

ip_configuration {
name = "vm-%[1]d"
subnet_id = azurerm_subnet.test.id
private_ip_address_allocation = "Dynamic"
}
}

resource "azurerm_linux_virtual_machine" "test" {
name = "vm-%[1]d"
martenbohlin marked this conversation as resolved.
Show resolved Hide resolved
location = azurerm_resource_group.test.location
resource_group_name = azurerm_resource_group.test.name

size = "Standard_B1s"
admin_username = "testadmin"
admin_password = "Password1234!"
disable_password_authentication = false

source_image_reference {
publisher = "OpenLogic"
offer = "CentOS"
sku = "7.5"
version = "latest"
}

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

network_interface_ids = [azurerm_network_interface.test.id]
}

resource "azurerm_server_vulnerability_assessment" "test" {
virtual_machine_id = azurerm_linux_virtual_machine.test.id
}
`, data.RandomInteger, data.Locations.Primary)
}
4 changes: 4 additions & 0 deletions website/azurerm.erb
Original file line number Diff line number Diff line change
Expand Up @@ -2779,6 +2779,10 @@
<li>
<a href="/docs/providers/azurerm/r/security_center_workspace.html">azurerm_security_center_workspace</a>
</li>

<li>
<a href="/docs/providers/azurerm/r/azurerm_server_vulnerability_assessment.html">azurerm_server_vulnerability_assessment</a>
</li>
</ul>
</li>

Expand Down
Loading