diff --git a/azure-test/tests/azure_key_vault_certificate/test-get-expected.json b/azure-test/tests/azure_key_vault_certificate/test-get-expected.json new file mode 100644 index 00000000..f2d090cd --- /dev/null +++ b/azure-test/tests/azure_key_vault_certificate/test-get-expected.json @@ -0,0 +1,6 @@ +[ + { + "id": "{{ output.resource_id.value }}", + "name": "{{ resourceName }}" + } +] \ No newline at end of file diff --git a/azure-test/tests/azure_key_vault_certificate/test-get-query.sql b/azure-test/tests/azure_key_vault_certificate/test-get-query.sql new file mode 100644 index 00000000..e5cb892f --- /dev/null +++ b/azure-test/tests/azure_key_vault_certificate/test-get-query.sql @@ -0,0 +1,3 @@ +select name, id +from azure.azure_key_vault_certificate +where name = '{{resourceName}}' and vault_name = '{{resourceName}}' diff --git a/azure-test/tests/azure_key_vault_certificate/test-hydrate-expected.json b/azure-test/tests/azure_key_vault_certificate/test-hydrate-expected.json new file mode 100644 index 00000000..15a69a3c --- /dev/null +++ b/azure-test/tests/azure_key_vault_certificate/test-hydrate-expected.json @@ -0,0 +1,7 @@ +[ + { + "id": "{{ output.resource_id.value }}", + "name": "{{resourceName}}", + "vault_name": "{{resourceName}}" + } +] \ No newline at end of file diff --git a/azure-test/tests/azure_key_vault_certificate/test-hydrate-query.sql b/azure-test/tests/azure_key_vault_certificate/test-hydrate-query.sql new file mode 100644 index 00000000..13ee222d --- /dev/null +++ b/azure-test/tests/azure_key_vault_certificate/test-hydrate-query.sql @@ -0,0 +1,3 @@ +select name, vault_name, id +from azure.azure_key_vault_certificate +where name = '{{resourceName}}' and title = '{{resourceName}}' diff --git a/azure-test/tests/azure_key_vault_certificate/test-list-expected.json b/azure-test/tests/azure_key_vault_certificate/test-list-expected.json new file mode 100644 index 00000000..db8cea80 --- /dev/null +++ b/azure-test/tests/azure_key_vault_certificate/test-list-expected.json @@ -0,0 +1,6 @@ +[ + { + "id": "{{ output.resource_id.value }}", + "name": "{{resourceName}}" + } +] \ No newline at end of file diff --git a/azure-test/tests/azure_key_vault_certificate/test-list-query.sql b/azure-test/tests/azure_key_vault_certificate/test-list-query.sql new file mode 100644 index 00000000..af7c63ac --- /dev/null +++ b/azure-test/tests/azure_key_vault_certificate/test-list-query.sql @@ -0,0 +1,3 @@ +select id, name +from azure.azure_key_vault_certificate +where name = '{{resourceName}}' diff --git a/azure-test/tests/azure_key_vault_certificate/test-not-found-expected.json b/azure-test/tests/azure_key_vault_certificate/test-not-found-expected.json new file mode 100644 index 00000000..ec747fa4 --- /dev/null +++ b/azure-test/tests/azure_key_vault_certificate/test-not-found-expected.json @@ -0,0 +1 @@ +null \ No newline at end of file diff --git a/azure-test/tests/azure_key_vault_certificate/test-not-found-query.sql b/azure-test/tests/azure_key_vault_certificate/test-not-found-query.sql new file mode 100644 index 00000000..bf4b5445 --- /dev/null +++ b/azure-test/tests/azure_key_vault_certificate/test-not-found-query.sql @@ -0,0 +1,3 @@ +select name, akas, tags, title +from azure.azure_key_vault_certificate +where name = 'dummy-{{resourceName}}' and vault_name = '{{resourceName}}' diff --git a/azure-test/tests/azure_key_vault_certificate/variables.tf b/azure-test/tests/azure_key_vault_certificate/variables.tf new file mode 100644 index 00000000..ae8ec3b8 --- /dev/null +++ b/azure-test/tests/azure_key_vault_certificate/variables.tf @@ -0,0 +1,171 @@ +variable "resource_name" { + type = string + default = "turbot-test-20200125-create-update" + description = "Name of the resource used throughout the test." +} + +variable "azure_environment" { + type = string + default = "public" + description = "Azure environment used for the test." +} + +variable "azure_subscription" { + type = string + default = "3510ae4d-530b-497d-8f30-53c0616fc6c1" + description = "Azure subscription used for the test." +} + +provider "azurerm" { + environment = var.azure_environment + subscription_id = var.azure_subscription + features {} +} + +data "azurerm_client_config" "current" {} + +data "null_data_source" "resource" { + inputs = { + scope = "azure:///subscriptions/${data.azurerm_client_config.current.subscription_id}" + } +} + +resource "azurerm_resource_group" "named_test_resource" { + name = var.resource_name + location = "West US" +} + +resource "azurerm_key_vault" "example" { + name = var.resource_name + location = azurerm_resource_group.named_test_resource.location + resource_group_name = azurerm_resource_group.named_test_resource.name + tenant_id = data.azurerm_client_config.current.tenant_id + sku_name = "standard" + soft_delete_retention_days = 7 + + access_policy { + tenant_id = data.azurerm_client_config.current.tenant_id + object_id = data.azurerm_client_config.current.object_id + + certificate_permissions = [ + "Create", + "Delete", + "DeleteIssuers", + "Get", + "GetIssuers", + "Import", + "List", + "ListIssuers", + "ManageContacts", + "ManageIssuers", + "Purge", + "SetIssuers", + "Update", + ] + + key_permissions = [ + "Backup", + "Create", + "Decrypt", + "Delete", + "Encrypt", + "Get", + "Import", + "List", + "Purge", + "Recover", + "Restore", + "Sign", + "UnwrapKey", + "Update", + "Verify", + "WrapKey", + ] + + secret_permissions = [ + "Backup", + "Delete", + "Get", + "List", + "Purge", + "Recover", + "Restore", + "Set", + ] + } +} + +resource "azurerm_key_vault_certificate" "example" { + depends_on = [azurerm_key_vault.example] + name = var.resource_name + key_vault_id = azurerm_key_vault.example.id + + certificate_policy { + issuer_parameters { + name = "Self" + } + + key_properties { + exportable = true + key_size = 2048 + key_type = "RSA" + reuse_key = true + } + + lifetime_action { + action { + action_type = "AutoRenew" + } + + trigger { + days_before_expiry = 30 + } + } + + secret_properties { + content_type = "application/x-pkcs12" + } + + x509_certificate_properties { + # Server Authentication = 1.3.6.1.5.5.7.3.1 + # Client Authentication = 1.3.6.1.5.5.7.3.2 + extended_key_usage = ["1.3.6.1.5.5.7.3.1"] + + key_usage = [ + "cRLSign", + "dataEncipherment", + "digitalSignature", + "keyAgreement", + "keyCertSign", + "keyEncipherment", + ] + + subject_alternative_names { + dns_names = ["internal.contoso.com", "domain.hello.world"] + } + + subject = "CN=hello-world" + validity_in_months = 12 + } + } +} + +output "resource_aka" { + value = "azure://${azurerm_key_vault_certificate.example.id}" +} + +output "resource_aka_lower" { + value = "azure://${lower(azurerm_key_vault_certificate.example.id)}" +} + +output "resource_id" { + value = azurerm_key_vault_certificate.example.id +} + +output "subscription_id" { + value = var.azure_subscription +} + +output "resource_name" { + value = var.resource_name +} \ No newline at end of file diff --git a/azure/plugin.go b/azure/plugin.go index ca32eb1d..d1624245 100644 --- a/azure/plugin.go +++ b/azure/plugin.go @@ -113,6 +113,7 @@ func Plugin(ctx context.Context) *plugin.Plugin { "azure_iothub": tableAzureIotHub(ctx), "azure_iothub_dps": tableAzureIotHubDps(ctx), "azure_key_vault": tableAzureKeyVault(ctx), + "azure_key_vault_certificate": tableAzureKeyVaultCertificate(ctx), "azure_key_vault_deleted_vault": tableAzureKeyVaultDeletedVault(ctx), "azure_key_vault_key": tableAzureKeyVaultKey(ctx), "azure_key_vault_key_version": tableAzureKeyVaultKeyVersion(ctx), diff --git a/azure/table_azure_key_vault_certificate.go b/azure/table_azure_key_vault_certificate.go new file mode 100644 index 00000000..0abf7b86 --- /dev/null +++ b/azure/table_azure_key_vault_certificate.go @@ -0,0 +1,358 @@ +package azure + +import ( + "context" + "strings" + + "github.com/Azure/azure-sdk-for-go/services/keyvault/2016-10-01/keyvault" + keyVaultp1 "github.com/Azure/azure-sdk-for-go/services/keyvault/mgmt/2019-09-01/keyvault" + "github.com/turbot/steampipe-plugin-sdk/v5/grpc/proto" + "github.com/turbot/steampipe-plugin-sdk/v5/plugin/transform" + + "github.com/turbot/steampipe-plugin-sdk/v5/plugin" +) + +//// TABLE DEFINITION + +func tableAzureKeyVaultCertificate(_ context.Context) *plugin.Table { + return &plugin.Table{ + Name: "azure_key_vault_certificate", + Description: "Azure Key Vault Certificate", + Get: &plugin.GetConfig{ + KeyColumns: plugin.AllColumns([]string{"vault_name", "name"}), + Hydrate: getKeyVaultCertificate, + IgnoreConfig: &plugin.IgnoreConfig{ + ShouldIgnoreErrorFunc: isNotFoundError([]string{"ResourceNotFound", "404", "SecretDisabled"}), + }, + }, + List: &plugin.ListConfig{ + Hydrate: listKeyVaultCertificates, + ParentHydrate: listKeyVaults, + KeyColumns: plugin.KeyColumnSlice{ + { + Name: "vault_name", Require: plugin.Optional, + }, + }, + IgnoreConfig: &plugin.IgnoreConfig{ + ShouldIgnoreErrorFunc: isNotFoundError([]string{"ResourceNotFound", "ResourceGroupNotFound", "404"}), + }, + }, + Columns: azureColumns([]*plugin.Column{ + { + Name: "name", + Description: "Name of the certificate.", + Type: proto.ColumnType_STRING, + Transform: transform.FromP(getCertificateNameAndVaultName, "Name"), + }, + { + Name: "vault_name", + Description: "The name of the vault.", + Type: proto.ColumnType_STRING, + Transform: transform.FromP(getCertificateNameAndVaultName, "VaultName"), + }, + // We are getting the ID value from Get API call correctly not from List API call + // Get Response: https://turbottest94388.vault.azure.net/certificates/turbottest94388/beaf55112a214cd88aa500fcee10b0f4 + // List Response: https://turbottest94388.vault.azure.net/certificates/turbottest94388 + { + Name: "id", + Description: "Certificate identifier.", + Type: proto.ColumnType_STRING, + Hydrate: getKeyVaultCertificate, + Transform: transform.FromGo(), + }, + { + Name: "x509_thumbprint", + Description: "Thumbprint of the certificate. A URL-encoded base64 string.", + Type: proto.ColumnType_STRING, + Transform: transform.FromField("X509Thumbprint"), + }, + { + Name: "recovery_level", + Description: "Reflects the deletion recovery level currently in effect for certificates in the current vault. If it contains 'Purgeable', the certificate can be permanently deleted by a privileged user; otherwise, only the system can purge the certificate, at the end of the retention interval. Possible values include: 'Purgeable', 'RecoverablePurgeable', 'Recoverable', 'RecoverableProtectedSubscription'.", + Type: proto.ColumnType_STRING, + Hydrate: getKeyVaultCertificate, + Transform: transform.FromField("Attributes.RecoveryLevel"), + }, + { + Name: "enabled", + Description: "Determines whether the object is enabled.", + Type: proto.ColumnType_BOOL, + Transform: transform.FromField("Attributes.Enabled"), + }, + { + Name: "not_before", + Description: "Not before date in UTC.", + Type: proto.ColumnType_TIMESTAMP, + Transform: transform.FromP(convertPointerUnixTimestampToTimestamp, "NotBefore").Transform(transform.UnixMsToTimestamp).NullIfZero(), + }, + { + Name: "expires", + Description: "Expiry date in UTC.", + Type: proto.ColumnType_TIMESTAMP, + Transform: transform.FromP(convertPointerUnixTimestampToTimestamp, "Expires").Transform(transform.UnixMsToTimestamp).NullIfZero(), + }, + { + Name: "created", + Description: "Creation time in UTC.", + Type: proto.ColumnType_TIMESTAMP, + Transform: transform.FromP(convertPointerUnixTimestampToTimestamp, "Created").Transform(transform.UnixMsToTimestamp), + }, + { + Name: "updated", + Description: "Last updated time in UTC.", + Type: proto.ColumnType_TIMESTAMP, + Transform: transform.FromP(convertPointerUnixTimestampToTimestamp, "Updated").Transform(transform.UnixMsToTimestamp).NullIfZero(), + }, + { + Name: "key_id", + Description: "The key id.", + Type: proto.ColumnType_STRING, + Hydrate: getKeyVaultCertificate, + Transform: transform.FromField("Kid"), + }, + { + Name: "secret_id", + Description: "The secret id.", + Type: proto.ColumnType_STRING, + Hydrate: getKeyVaultCertificate, + Transform: transform.FromField("Sid"), + }, + { + Name: "content_type", + Description: "The content type of the secret.", + Type: proto.ColumnType_STRING, + Hydrate: getKeyVaultCertificate, + }, + { + Name: "cer", + Description: "CER contents of x509 certificate.", + Type: proto.ColumnType_JSON, + Hydrate: getKeyVaultCertificate, + }, + { + Name: "key_properties", + Description: "Properties of the key backing a certificate.", + Type: proto.ColumnType_JSON, + Hydrate: getKeyVaultCertificate, + Transform: transform.FromField("Policy.KeyProperties"), + }, + { + Name: "secret_properties", + Description: "Properties of the secret backing a certificate.", + Type: proto.ColumnType_JSON, + Hydrate: getKeyVaultCertificate, + Transform: transform.FromField("Policy.SecretProperties"), + }, + { + Name: "x509_certificate_properties", + Description: "Properties of the X509 component of a certificate.", + Type: proto.ColumnType_JSON, + Hydrate: getKeyVaultCertificate, + Transform: transform.FromField("Policy.X509CertificateProperties"), + }, + { + Name: "lifetime_actions", + Description: "Actions that will be performed by Key Vault over the lifetime of a certificate.", + Type: proto.ColumnType_JSON, + Hydrate: getKeyVaultCertificate, + Transform: transform.FromField("Policy.LifetimeActions"), + }, + { + Name: "issuer_parameters", + Description: "Parameters for the issuer of the X509 component of a certificate.", + Type: proto.ColumnType_JSON, + Hydrate: getKeyVaultCertificate, + Transform: transform.FromField("Policy.IssuerParameters"), + }, + + // Steampipe standard columns + { + Name: "title", + Description: ColumnDescriptionTitle, + Type: proto.ColumnType_STRING, + Transform: transform.FromP(getCertificateNameAndVaultName, "Name"), + }, + { + Name: "tags", + Description: ColumnDescriptionTags, + Type: proto.ColumnType_JSON, + }, + { + Name: "akas", + Description: ColumnDescriptionAkas, + Hydrate: getKeyVaultCertificate, + Type: proto.ColumnType_JSON, + Transform: transform.FromField("ID").Transform(idToAkas), + }, + + // We will not get the location and resource groups for the certificate because they are based on vault. + // Azure standard columns + }), + } +} + +//// LIST FUNCTION + +func listKeyVaultCertificates(ctx context.Context, d *plugin.QueryData, h *plugin.HydrateData) (interface{}, error) { + // Get the details of vault + vault := h.Item.(keyVaultp1.Resource) + + vaultName := d.EqualsQualString("vault_name") + + if vaultName != "" && vaultName != *vault.Name{ + return nil, nil + } + + // Create session + session, err := GetNewSession(ctx, d, "VAULT") + if err != nil { + plugin.Logger(ctx).Error("azure_key_vault_certificate.listKeyVaultCertificates", "session_error", err) + return nil, err + } + vaultURI := "https://" + *vault.Name + ".vault.azure.net/" + + client := keyvault.New() + client.Authorizer = session.Authorizer + + maxresult := int32(25) + + result, err := client.GetCertificates(ctx, vaultURI, &maxresult) + if err != nil { + plugin.Logger(ctx).Error("azure_key_vault_certificate.listKeyVaultCertificates", "api_error", err) + return nil, err + } + for _, cert := range result.Values() { + d.StreamListItem(ctx, cert) + + // Check if context has been cancelled or if the limit has been hit (if specified) + // if there is a limit, it will return the number of rows required to reach this limit + if d.RowsRemaining(ctx) == 0 { + return nil, nil + } + } + + for result.NotDone() { + err = result.NextWithContext(ctx) + if err != nil { + return nil, err + } + + for _, cert := range result.Values() { + d.StreamListItem(ctx, cert) + + // Check if context has been cancelled or if the limit has been hit (if specified) + // if there is a limit, it will return the number of rows required to reach this limit + if d.RowsRemaining(ctx) == 0 { + return nil, nil + } + } + } + + return nil, err +} + +//// HYDRATE FUNCTIONS + +func getKeyVaultCertificate(ctx context.Context, d *plugin.QueryData, h *plugin.HydrateData) (interface{}, error) { + + var vaultName, name string + if h.Item != nil { + data := h.Item.(keyvault.CertificateItem) + splitID := strings.Split(*data.ID, "/") + vaultName = strings.Split(splitID[2], ".")[0] + name = splitID[4] + + // Operation get is not allowed on a disabled certificate + if !*data.Attributes.Enabled { + return nil, nil + } + } else { + vaultName = d.EqualsQuals["vault_name"].GetStringValue() + name = d.EqualsQuals["name"].GetStringValue() + } + + // Create session + session, err := GetNewSession(ctx, d, "VAULT") + if err != nil { + plugin.Logger(ctx).Error("azure_key_vault_certificate.getKeyVaultCertificate", "session_error", err) + return nil, err + } + + client := keyvault.New() + client.Authorizer = session.Authorizer + + vaultURI := "https://" + vaultName + ".vault.azure.net/" + + op, err := client.GetCertificate(ctx, vaultURI, name, "") + if err != nil { + plugin.Logger(ctx).Error("azure_key_vault_certificate.getKeyVaultCertificate", "api_error", err) + return nil, err + } + + // In some cases resource does not give any notFound error + // instead of notFound error, it returns empty data + if op.ID != nil { + return op, nil + } + + return nil, nil +} + +//// TRANSFORM FUNCTION + +func convertPointerUnixTimestampToTimestamp(_ context.Context, d *transform.TransformData) (interface{}, error) { + param := d.Param.(string) + result := make(map[string]interface{}, 0) + switch item := d.HydrateItem.(type) { + case keyvault.CertificateItem: + a := item.Attributes + if a != nil { + if a.Created != nil { + result["Created"] = a.Created.Duration() + } + if a.Expires != nil { + result["Expires"] = a.Expires.Duration() + } + if a.NotBefore != nil { + result["NotBefore"] = a.NotBefore.Duration() + } + if a.Updated != nil { + result["Updated"] = a.Updated.Duration() + } + } + case keyvault.CertificateBundle: + a := item.Attributes + if a != nil { + if a.Created != nil { + result["Created"] = a.Created.Duration() + } + if a.Expires != nil { + result["Expires"] = a.Expires.Duration() + } + if a.NotBefore != nil { + result["NotBefore"] = a.NotBefore.Duration() + } + if a.Updated != nil { + result["Updated"] = a.Updated.Duration() + } + } + } + + return result[param], nil +} + +func getCertificateNameAndVaultName(_ context.Context, d *transform.TransformData) (interface{}, error) { + param := d.Param.(string) + result := make(map[string]interface{}, 0) + if d.HydrateItem != nil { + switch item := d.HydrateItem.(type) { + case keyvault.CertificateItem: + result["Name"] = strings.Split(*item.ID, "/")[4] + result["VaultName"] = strings.Split(result["Name"].(string), ".")[0] + case keyvault.CertificateBundle: + result["Name"] = strings.Split(*item.ID, "/")[4] + result["VaultName"] = strings.Split(result["Name"].(string), ".")[0] + } + } + return result[param], nil +} diff --git a/docs/tables/azure_key_vault_certificate.md b/docs/tables/azure_key_vault_certificate.md new file mode 100644 index 00000000..1c705f04 --- /dev/null +++ b/docs/tables/azure_key_vault_certificate.md @@ -0,0 +1,149 @@ +--- +title: "Steampipe Table: azure_key_vault_certificate - Query Azure Key Vault Certificates using SQL" +description: "Allows users to query Azure Key Vault Certificates, providing access to certificate details, including key ID, key properties, and secret properties." +--- + +# Table: azure_key_vault_certificate - Query Azure Key Vault Certificates using SQL + +Azure Key Vault is a cloud service that provides a secure store for secrets, keys, and certificates. Key Vault certificates are managed digital certificates that aid in secure communication and identity verification, which are essential in various IT and cloud scenarios. + +## Table Usage Guide + +The `azure_key_vault_certificate` table allows users to explore and manage certificates within Azure Key Vault. This table is especially useful for security engineers and cloud administrators who need to oversee the state, configuration, and properties of certificates stored in Azure Key Vault. + +## Examples + +### Basic info +Review the general status and details of your Azure Key Vault certificates. This query is fundamental for routine checks and ensuring that certificates are up-to-date and correctly enabled. + +```sql+postgres +select + name, + vault_name, + enabled, + created, + updated +from + azure_key_vault_certificate; +``` + +```sql+sqlite +select + name, + vault_name, + enabled, + created, + updated +from + azure_key_vault_certificate; +``` + +### List disabled certificates +Identify certificates that are currently disabled in Azure Key Vault. This query helps maintain proper security measures and effective access control. + +```sql+postgres +select + name, + vault_name, + enabled +from + azure_key_vault_certificate +where + not enabled; +``` + +```sql+sqlite +select + name, + vault_name, + enabled +from + azure_key_vault_certificate +where + not enabled; +``` + +### List certificates expiring in 10 days +Discover certificates within Azure Key Vault that are nearing their expiration date. This is crucial for proactive certificate renewal and avoiding potential security risks. + +```sql+postgres +select + name, + enabled, + not_before, + created, + expires +from + azure_key_vault_certificate +where + expires <= now() + interval '10 days'; +``` + +```sql+sqlite +select + name, + enabled, + not_before, + created, + expires +from + azure_key_vault_certificate +where + datetime(expires) <= datetime('now', '+10 days'); +``` + +### Get key properties of certificates +Analyze the key properties of certificates, including their exportability, type, size, and reuse policies. This information is vital for understanding the security and operational characteristics of each certificate. + +```sql+postgres +select + name, + id, + key_properties ->> 'Exportable' as exportable, + key_properties ->> 'KeyType' as key_type, + key_properties ->> 'KeySize' as key_size, + key_properties ->> 'ReuseKey' as reuse_key +from + azure_key_vault_certificate; +``` + +```sql+sqlite +select + name, + id, + json_extract(key_properties, '$.Exportable') as exportable, + json_extract(key_properties, '$.KeyType') as key_type, + json_extract(key_properties, '$.KeySize') as key_size, + json_extract(key_properties, '$.ReuseKey') as reuse_key +from + azure_key_vault_certificate; +``` + +### Get X509 properties of certificates +Examine the X509 properties of certificates, such as the subject, extended key usage (EKUs), alternative names, key usage, and validity. This query is crucial for detailed certificate analysis and compliance checks. + +```sql+postgres +select + name, + id, + x509_certificate_properties ->> 'Subject' as subject, + x509_certificate_properties -> 'Ekus' as ekus, + x509_certificate_properties -> 'SubjectAlternativeNames' as subject_alternative_names, + x509_certificate_properties ->> 'KeyUsage' as key_usage, + x509_certificate_properties ->> 'ValidityInMonths' as validity_in_months +from + azure_key_vault_certificate; +``` + +```sql+sqlite +select + name, + id, + json_extract(x509_certificate_properties, '$.Subject') as subject, + json_extract(x509_certificate_properties, '$.Ekus') as ekus, + json_extract(x509_certificate_properties, '$.SubjectAlternativeNames') as subject_alternative_names, + json_extract(x509_certificate_properties, '$.KeyUsage') as key_usage, + json_extract(x509_certificate_properties, '$.ValidityInMonths') as validity_in_months +from + azure_key_vault_certificate; +```