Skip to content

Commit

Permalink
feat(cli): display status of Azure compliance subscriptions
Browse files Browse the repository at this point in the history
Signed-off-by: Salim Afiune Maya <[email protected]>
  • Loading branch information
afiune committed Feb 1, 2022
1 parent 30e8448 commit c479863
Show file tree
Hide file tree
Showing 4 changed files with 207 additions and 40 deletions.
16 changes: 16 additions & 0 deletions cli/cmd/compliance_aws.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,13 @@ var (
Long: `List all AWS accounts configured in your account.`,
Args: cobra.NoArgs,
RunE: func(_ *cobra.Command, _ []string) error {
cli.StartProgress(" Fetching compliance information...")
awsIntegrations, err := cli.LwApi.Integrations.ListAwsCfg()
cli.StopProgress()
if err != nil {
return errors.Wrap(err, "unable to get aws compliance integrations")
}

return cliListAwsAccounts(&awsIntegrations)
},
}
Expand Down Expand Up @@ -311,6 +314,10 @@ Then navigate to Settings > Integrations > Cloud Accounts.
}

for _, i := range awsIntegrations.Data {
if containsDuplicateAccountID(awsAccounts, i.Data.AwsAccountID) {
cli.Log.Warnw("duplicate aws account", "integration_guid", i.IntgGuid, "account", i.Data.AwsAccountID)
continue
}
awsAccounts = append(awsAccounts, awsAccount{
AccountID: i.Data.AwsAccountID,
Status: i.Status(),
Expand All @@ -330,3 +337,12 @@ Then navigate to Settings > Integrations > Cloud Accounts.
cli.OutputHuman(renderSimpleTable([]string{"AWS Account", "Status"}, rows))
return nil
}

func containsDuplicateAccountID(awsAccount []awsAccount, accountID string) bool {
for _, value := range awsAccount {
if accountID == value.AccountID {
return true
}
}
return false
}
138 changes: 100 additions & 38 deletions cli/cmd/compliance_azure.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ package cmd

import (
"fmt"
"sort"
"strings"
"time"

Expand Down Expand Up @@ -78,51 +79,19 @@ Use the following command to list all Azure Tenants configured in your account:
// complianceAzureListTenantsCmd represents the list-tenants sub-command inside the azure command
complianceAzureListTenantsCmd = &cobra.Command{
Use: "list-tenants",
Aliases: []string{"list"},
Short: "List all Azure Tenants configured",
Long: `List all Azure Tenants configured in your account.`,
Aliases: []string{"list", "ls"},
Short: "List Azure tenants and subscriptions",
Long: `List all Azure tenants and subscriptions configured in your account.`,
Args: cobra.NoArgs,
RunE: func(_ *cobra.Command, _ []string) error {
cli.StartProgress(" Fetching compliance information...")
azureIntegrations, err := cli.LwApi.Integrations.ListAzureCfg()
cli.StopProgress()
if err != nil {
return errors.Wrap(err, "unable to get azure integrations")
}
if len(azureIntegrations.Data) == 0 {
msg := `There are no Azure Tenants configured in your account.

Get started by integrating your Azure Tenants to analyze configuration compliance using the command:
lacework integration create
If you prefer to configure the integration via the WebUI, log in to your account at:
https://%s.lacework.net
Then navigate to Settings > Integrations > Cloud Accounts.
`
cli.OutputHuman(fmt.Sprintf(msg, cli.Account))
return nil
}

azureTenants := make([]string, 0)
for _, i := range azureIntegrations.Data {
azureTenants = append(azureTenants, i.Data.TenantID)
}

if cli.JSONOutput() {
jsonOut := struct {
Tenants []string `json:"azure_tenants"`
}{Tenants: azureTenants}
return cli.OutputJSON(jsonOut)
}

var rows [][]string
for _, tenant := range azureTenants {
rows = append(rows, []string{tenant})
}

cli.OutputHuman(renderSimpleTable([]string{"Azure Tenants"}, rows))
return nil
return cliListTenantsAndSubscriptions(&azureIntegrations)
},
}

Expand Down Expand Up @@ -378,3 +347,96 @@ type cliComplianceAzureInfo struct {
Tenant cliComplianceIDAlias `json:"tenant"`
Subscriptions []cliComplianceIDAlias `json:"subscriptions"`
}

func cliListTenantsAndSubscriptions(azureIntegrations *api.AzureIntegrationsResponse) error {
jsonOut := struct {
Subscriptions []azureSubscription `json:"azure_subscriptions"`
}{Subscriptions: make([]azureSubscription, 0)}

if azureIntegrations == nil || len(azureIntegrations.Data) == 0 {
if cli.JSONOutput() {
return cli.OutputJSON(jsonOut)
}

msg := `There are no Azure Tenants configured in your account.
Get started by integrating your Azure Tenants to analyze configuration compliance using the command:
lacework integration create
If you prefer to configure the integration via the WebUI, log in to your account at:
https://%s.lacework.net
Then navigate to Settings > Integrations > Cloud Accounts.
`
cli.OutputHuman(fmt.Sprintf(msg, cli.Account))
return nil
}

if cli.JSONOutput() {
jsonOut.Subscriptions = extractAzureSubscriptions(azureIntegrations)
return cli.OutputJSON(jsonOut)
}

var rows [][]string
for _, az := range extractAzureSubscriptions(azureIntegrations) {
rows = append(rows, []string{az.TenantID, az.SubscriptionID, az.Status})
}

cli.OutputHuman(renderSimpleTable([]string{"Azure Tenant", "Azure Subscription", "Status"}, rows))
return nil
}

type azureSubscription struct {
TenantID string `json:"tenant_id"`
SubscriptionID string `json:"subscription_id"`
Status string `json:"status"`
}

func extractAzureSubscriptions(response *api.AzureIntegrationsResponse) []azureSubscription {
var azureSubscriptions []azureSubscription

if response == nil {
return azureSubscriptions
}

for _, gcp := range response.Data {
// fetch the subscription ids from tenant id
azureSubscriptions = append(azureSubscriptions, getAzureSubscriptions(gcp.Data.TenantID, gcp.Status())...)
}

sort.Slice(azureSubscriptions, func(i, j int) bool {
switch strings.Compare(azureSubscriptions[i].TenantID, azureSubscriptions[j].TenantID) {
case -1:
return true
case 1:
return false
}
return azureSubscriptions[i].SubscriptionID < azureSubscriptions[j].SubscriptionID
})

return azureSubscriptions
}

func getAzureSubscriptions(tenantID, status string) []azureSubscription {
var subs []azureSubscription
cli.StartProgress(fmt.Sprintf("Fetching compliance information about %s tenant...", tenantID))
subsResponse, err := cli.LwApi.Compliance.ListAzureSubscriptions(tenantID)
cli.StopProgress()
if err != nil {
cli.Log.Warn("unable to list azure subscriptions", "tenant_id", tenantID, "error", err.Error())
return subs
}
for _, subsRes := range subsResponse.Data {
for _, subRes := range subsRes.Subscriptions {
subscriptionID, _ := splitIDAndAlias(subRes)
subs = append(subs, azureSubscription{
TenantID: tenantID,
SubscriptionID: subscriptionID,
Status: status,
})
}
}
return subs
}
81 changes: 81 additions & 0 deletions cli/cmd/compliance_azure_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,84 @@ func TestSplitAzureSubscriptionsApiResponse(t *testing.T) {
})
}
}

func TestCliListAzureTenantsAndSubscriptionsWithoutData(t *testing.T) {
cliOutput := captureOutput(func() {
assert.Nil(t, cliListTenantsAndSubscriptions(new(api.AzureIntegrationsResponse)))
})
assert.Contains(t, cliOutput, "There are no Azure Tenants configured in your account.")

t.Run("test JSON output", func(t *testing.T) {
cli.EnableJSONOutput()
defer cli.EnableHumanOutput()
cliJSONOutput := captureOutput(func() {
assert.Nil(t, cliListTenantsAndSubscriptions(new(api.AzureIntegrationsResponse)))
})
expectedJSON := `{
"azure_subscriptions": []
}
`
assert.Equal(t, expectedJSON, cliJSONOutput)
})
}

/*
func TestCliListAzureTenantsAndSubscriptionsWithDataEnabled(t *testing.T) {
cliOutput := captureOutput(func() {
assert.Nil(t, cliListTenantsAndSubscriptions(mockAzureIntegrationsResponse(1)))
})
// NOTE (@afiune): We purposly leave trailing spaces in this table, we need them!
expectedTable := `
AZURE TENANT AZURE SUBSCRIPTION STATUS
---------------------------------------+--------------------------------------+----------
abc123xy-1234-abcd-a1b2-09876zxy1234 ABC123XX-1234-ABCD-1234-ABCD1234XYZZ Enabled
`
assert.Equal(t, strings.TrimPrefix(expectedTable, "\n"), cliOutput)
}
func TestCliListAzureTenantsAndSubscriptionsWithDataDisabled(t *testing.T) {
cliOutput := captureOutput(func() {
assert.Nil(t, cliListTenantsAndSubscriptions(mockAzureIntegrationsResponse(0)))
})
// NOTE (@afiune): We purposly leave trailing spaces in this table, we need them!
expectedTable := `
AZURE TENANT AZURE SUBSCRIPTION STATUS
---------------------------------------+--------------------------------------+----------
abc123xy-1234-abcd-a1b2-09876zxy1234 ABC123XX-1234-ABCD-1234-ABCD1234XYZZ Disabled
`
assert.Equal(t, strings.TrimPrefix(expectedTable, "\n"), cliOutput)
}
func mockAzureIntegrationsResponse(enabled int) *api.AzureIntegrationsResponse {
response := &api.AzureIntegrationsResponse{}
err := json.Unmarshal([]byte(`{
"data": [
{
"CREATED_OR_UPDATED_BY": "[email protected]",
"CREATED_OR_UPDATED_TIME": "2021-08-02T17:53:24.116Z",
"DATA": {
"TENANT_ID": "abc123xy-1234-abcd-a1b2-09876zxy1234"
},
"ENABLED": `+strconv.Itoa(enabled)+`,
"INTG_GUID": "MOCK_1234",
"IS_ORG": 0,
"NAME": "TF Config",
"STATE": {
"lastSuccessfulTime": "2021-Jun-04 09:40:39 UTC",
"lastUpdatedTime": "2022-Jan-31 11:49:09 UTC",
"ok": false
},
"TYPE": "AZURE_CFG",
"TYPE_NAME": "Azure Compliance"
}
],
"message": "SUCCESS",
"ok": true
}
`), response)
if err != nil {
log.Fatal(err)
}
return response
}
*/
12 changes: 10 additions & 2 deletions cli/cmd/compliance_gcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ var (
Short: "List gcp projects and organizations",
Long: `List all GCP projects and organization IDs.`,
RunE: func(_ *cobra.Command, args []string) error {
cli.StartProgress(" Fetching compliance information...")
response, err := cli.LwApi.Integrations.ListGcpCfg()
cli.StopProgress()
if err != nil {
return errors.Wrap(err, "unable to list gcp projects/organizations")
}
Expand Down Expand Up @@ -378,9 +380,12 @@ func splitIDAndAlias(text string) (id string, alias string) {

func getGcpAccounts(orgID, status string) []gcpProject {
var accounts []gcpProject

cli.StartProgress(fmt.Sprintf("Fetching compliance information about %s organization...", orgID))
projectsResponse, err := cli.LwApi.Compliance.ListGcpProjects(orgID)
cli.StopProgress()
if err != nil {
cli.Log.Warn("unable to list gcp projects", "org_id", orgID, "error", err.Error())
cli.Log.Warnw("unable to list gcp projects", "org_id", orgID, "error", err.Error())
return accounts
}
for _, projects := range projectsResponse.Data {
Expand Down Expand Up @@ -419,7 +424,10 @@ func extractGcpProjects(response *api.GcpIntegrationsResponse) []gcpProject {
// if organization account, fetch the project ids
if gcp.Data.IDType == "ORGANIZATION" {
gcpAccounts = append(gcpAccounts, getGcpAccounts(gcp.Data.ID, gcp.Status())...)
} else if !containsDuplicateProjectID(gcpAccounts, gcp.Data.ID) {
} else if containsDuplicateProjectID(gcpAccounts, gcp.Data.ID) {
cli.Log.Warnw("duplicate gcp project", "integration_guid", gcp.IntgGuid, "project", gcp.Data.ID)
continue
} else {
gcpIntegration := gcpProject{
OrganizationID: "n/a",
ProjectID: gcp.Data.ID,
Expand Down

0 comments on commit c479863

Please sign in to comment.