Skip to content

Commit

Permalink
Add azuread_conditional_access_named_location table Closes #191 (#199)
Browse files Browse the repository at this point in the history

Co-authored-by: TheRealHouseMouse <[email protected]>
Co-authored-by: anon4mouse <[email protected]>
  • Loading branch information
3 people authored Sep 20, 2024
1 parent 6d6a1b3 commit 2c31393
Show file tree
Hide file tree
Showing 4 changed files with 358 additions and 2 deletions.
1 change: 1 addition & 0 deletions azuread/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ func Plugin(ctx context.Context) *plugin.Plugin {
"azuread_application": tableAzureAdApplication(ctx),
"azuread_application_app_role_assigned_to": tableAzureAdApplicationAppRoleAssignment(ctx),
"azuread_authorization_policy": tableAzureAdAuthorizationPolicy(ctx),
"azuread_conditional_access_named_location": tableAzureAdConditionalAccessNamedLocation(ctx),
"azuread_conditional_access_policy": tableAzureAdConditionalAccessPolicy(ctx),
"azuread_device": tableAzureAdDevice(ctx),
"azuread_directory_audit_report": tableAzureAdDirectoryAuditReport(ctx),
Expand Down
258 changes: 258 additions & 0 deletions azuread/table_azuread_conditional_access_named_location.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
package azuread

import (
"context"
"fmt"
"strings"

"github.com/iancoleman/strcase"
"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"

msgraphcore "github.com/microsoftgraph/msgraph-sdk-go-core"
"github.com/microsoftgraph/msgraph-sdk-go/identity"
"github.com/microsoftgraph/msgraph-sdk-go/models"
)

//// TABLE DEFINITION

func tableAzureAdConditionalAccessNamedLocation(_ context.Context) *plugin.Table {
return &plugin.Table{
Name: "azuread_conditional_access_named_location",
Description: "Represents an Azure Active Directory (Azure AD) Conditional Access Named Location.",
Get: &plugin.GetConfig{
Hydrate: getAdConditionalAccessNamedLocation,
IgnoreConfig: &plugin.IgnoreConfig{
ShouldIgnoreErrorFunc: isIgnorableErrorPredicate([]string{"Request_ResourceNotFound", "Invalid object identifier"}),
},
KeyColumns: plugin.SingleColumn("id"),
},
List: &plugin.ListConfig{
Hydrate: listAdConditionalAccessNamedLocations,
IgnoreConfig: &plugin.IgnoreConfig{
ShouldIgnoreErrorFunc: isIgnorableErrorPredicate([]string{"Request_UnsupportedQuery"}),
},
KeyColumns: []*plugin.KeyColumn{
{Name: "display_name", Require: plugin.Optional},
{Name: "id", Require: plugin.Optional},
{Name: "location_type", Require: plugin.Optional},
},
},

Columns: commonColumns([]*plugin.Column{
{Name: "id", Type: proto.ColumnType_STRING, Description: "Specifies the identifier of a Named Location object.", Transform: transform.FromMethod("GetId")},
{Name: "display_name", Type: proto.ColumnType_STRING, Description: "Specifies a display name for the Named Location object.", Transform: transform.FromMethod("GetDisplayName")},
{Name: "location_type", Type: proto.ColumnType_STRING, Description: "Specifies the type of the Named Location object: IP or Country.", Transform: transform.FromMethod("GetType")},
{Name: "created_date_time", Type: proto.ColumnType_TIMESTAMP, Description: "The create date of the Named Location object.", Transform: transform.FromMethod("GetCreatedDateTime")},
{Name: "modified_date_time", Type: proto.ColumnType_TIMESTAMP, Description: "The modification date of Named Location object.", Transform: transform.FromMethod("GetModifiedDateTime")},
{Name: "location_info", Type: proto.ColumnType_JSON, Description: "Specifies some location information for the Named Location object. Now supported: IP (v4/6 and CIDR/Range), odata_type, IsTrusted (for IP named locations only). Country (and regions, if exist), lookup method, UnkownCountriesAndRegions (for country named locations only).", Transform: transform.FromMethod("GetLocationInfo")},

// Standard columns
{Name: "title", Type: proto.ColumnType_STRING, Description: ColumnDescriptionTitle, Transform: transform.FromMethod("GetDisplayName")},
}),
}
}

//// LIST FUNCTION

func listAdConditionalAccessNamedLocations(ctx context.Context, d *plugin.QueryData, _ *plugin.HydrateData) (interface{}, error) {
// Create client
client, adapter, err := GetGraphClient(ctx, d)
if err != nil {
plugin.Logger(ctx).Error("azuread_conditional_access_named_location.listAdConditionalAccessNamedLocations", "connection_error", err)
return nil, err
}

// List operations
input := &identity.ConditionalAccessNamedLocationsRequestBuilderGetQueryParameters{
Top: Int32(1000),
}

limit := d.QueryContext.Limit
if limit != nil {
if *limit > 0 && *limit < 1000 {
l := int32(*limit)
input.Top = Int32(l)
}
}

equalQuals := d.EqualsQuals
filter := buildConditionalAccessNamedLocationQueryFilter(equalQuals)

if len(filter) > 0 {
joinStr := strings.Join(filter, " and ")
input.Filter = &joinStr
}

options := &identity.ConditionalAccessNamedLocationsRequestBuilderGetRequestConfiguration{
QueryParameters: input,
}

result, err := client.Identity().ConditionalAccess().NamedLocations().Get(ctx, options)
if err != nil {
errObj := getErrorObject(err)
plugin.Logger(ctx).Error("azuread_conditional_access_named_location.listAdConditionalAccessNamedLocations", "list_conditional_access_named_location_error", errObj)
return nil, errObj
}

pageIterator, err := msgraphcore.NewPageIterator[models.NamedLocationable](result, adapter, models.CreateNamedLocationCollectionResponseFromDiscriminatorValue)
if err != nil {
plugin.Logger(ctx).Error("azuread_conditional_access_named_location.listAdConditionalAccessNamedLocations", "create_iterator_instance_error", err)
return nil, err
}

err = pageIterator.Iterate(ctx, func(pageItem models.NamedLocationable) bool {
d.StreamListItem(ctx, ADNamedLocationInfo{
NamedLocationable: pageItem,
NamedLocation: getNamedLocationDetails(pageItem),
})

// Context can be cancelled due to manual cancellation or the limit has been hit
return d.RowsRemaining(ctx) != 0
})
if err != nil {
plugin.Logger(ctx).Error("azuread_conditional_access_named_location.listAdConditionalAccessNamedLocations", "paging_error", err)
return nil, err
}

return nil, nil
}

//// HYDRATE FUNCTIONS

func getAdConditionalAccessNamedLocation(ctx context.Context, d *plugin.QueryData, h *plugin.HydrateData) (interface{}, error) {

conditionalAccessNamedLocationId := d.EqualsQuals["id"].GetStringValue()
if conditionalAccessNamedLocationId == "" {
return nil, nil
}

// Create client
client, _, err := GetGraphClient(ctx, d)
if err != nil {
plugin.Logger(ctx).Error("azuread_conditional_access_named_location.getAdConditionalAccessNamedLocation", "connection_error", err)
return nil, err
}

location, err := client.Identity().ConditionalAccess().NamedLocations().ByNamedLocationId(conditionalAccessNamedLocationId).Get(ctx, nil)
if err != nil {
errObj := getErrorObject(err)
plugin.Logger(ctx).Error("azuread_conditional_access_named_location.getAdConditionalAccessNamedLocation", "get_conditional_access_location_error", errObj)
return nil, errObj
}

return &ADNamedLocationInfo{
NamedLocationable: location,
NamedLocation: getNamedLocationDetails(location),
}, nil
}

func buildConditionalAccessNamedLocationQueryFilter(equalQuals plugin.KeyColumnEqualsQualMap) []string {
filters := []string{}

filterQuals := map[string]string{
"display_name": "string",
"id": "string",
}

for qual, qualType := range filterQuals {
switch qualType {
case "string":
if equalQuals[qual] != nil {
if qual == "location_type" {
filters = append(filters, fmt.Sprintf("type eq '%s'", equalQuals[qual].GetStringValue()))
} else {
filters = append(filters, fmt.Sprintf("%s eq '%s'", strcase.ToLowerCamel(qual), equalQuals[qual].GetStringValue()))
}
}
}
}

return filters
}

/// UTILITY FUNCTION

func getNamedLocationDetails(i interface{}) models.NamedLocationable {

switch t := i.(type) {
case *models.IpNamedLocation:
return ADIpNamedLocationInfo{t}
case *models.CountryNamedLocation:
return ADCountryNamedLocationInfo{t}
}

return nil
}

//// TRANSFORM FUNCTIONS

func IpGetLocationInfo(ipLocationInfo *ADIpNamedLocationInfo) map[string]interface{} {
ipRangesArray := ipLocationInfo.GetIpRanges()
locationInfoJSON := map[string]interface{}{}

IPv4CidrArr := []map[string]interface{}{}
IPv4RangeArr := []map[string]interface{}{}
IPv6CidrArr := []map[string]interface{}{}
IPv6RangeArr := []map[string]interface{}{}

for i := 0; i < len(ipRangesArray); i++ {
switch t := ipRangesArray[i].(type) {
case *models.IPv4CidrRange:
IPv4CidrPair := map[string]interface{}{}
IPv4CidrPair["Address"] = *t.GetCidrAddress()
IPv4CidrArr = append(IPv4CidrArr, IPv4CidrPair)
case *models.IPv4Range:
IPv4AddressPair := map[string]interface{}{}
IPv4AddressPair["Lower"] = *t.GetLowerAddress()
IPv4AddressPair["Upper"] = *t.GetUpperAddress()
IPv4RangeArr = append(IPv4RangeArr, IPv4AddressPair)
case *models.IPv6CidrRange:
IPv6CidrPair := map[string]interface{}{}
IPv6CidrPair["Address"] = *t.GetCidrAddress()
IPv6CidrArr = append(IPv6CidrArr, IPv6CidrPair)
case *models.IPv6Range:
IPv6AddressPair := map[string]interface{}{}
IPv6AddressPair["Lower"] = *t.GetLowerAddress()
IPv6AddressPair["Upper"] = *t.GetUpperAddress()
IPv6RangeArr = append(IPv6RangeArr, IPv6AddressPair)
}
}

locationInfoJSON["IPv4Cidr"] = IPv4CidrArr
locationInfoJSON["IPv4Range"] = IPv4RangeArr
locationInfoJSON["IPv6Cidr"] = IPv6CidrArr
locationInfoJSON["IPv6Range"] = IPv6RangeArr
locationInfoJSON["IsTrusted"] = ipLocationInfo.GetIsTrusted()
return locationInfoJSON
}

func CountryGetLocationInfo(countryLocationInfo *ADCountryNamedLocationInfo) map[string]interface{} {
locationInfoJSON := map[string]interface{}{}
locationInfoJSON["Countries_and_Regions"] = countryLocationInfo.GetCountriesAndRegions()
locationInfoJSON["Get_Unknown_Countries_and_Regions"] = countryLocationInfo.GetIncludeUnknownCountriesAndRegions()
locationInfoJSON["Lookup_Method"] = countryLocationInfo.GetCountryLookupMethod().String()
return locationInfoJSON
}

func (locationInfo *ADNamedLocationInfo) GetLocationInfo() map[string]interface{} {
switch t := locationInfo.NamedLocation.(type) {
case ADIpNamedLocationInfo:
return IpGetLocationInfo(&ADIpNamedLocationInfo{t})
case ADCountryNamedLocationInfo:
return CountryGetLocationInfo(&ADCountryNamedLocationInfo{t})
}
return nil
}

func (locationInfo *ADNamedLocationInfo) GetType() string {
switch locationInfo.NamedLocation.(type) {
case ADIpNamedLocationInfo:
return "IP"
case ADCountryNamedLocationInfo:
return "Country"
}
return "Unknown"
}
17 changes: 15 additions & 2 deletions azuread/transforms.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,19 @@ type ADIdentityProviderInfo struct {
ClientSecret interface{}
}

type ADNamedLocationInfo struct {
models.NamedLocationable
NamedLocation models.NamedLocationable
}

type ADIpNamedLocationInfo struct {
models.IpNamedLocationable
}

type ADCountryNamedLocationInfo struct {
models.CountryNamedLocationable
}

type ADSecurityDefaultsPolicyInfo struct {
models.IdentitySecurityDefaultsEnforcementPolicyable
}
Expand Down Expand Up @@ -437,13 +450,13 @@ func (conditionalAccessPolicy *ADConditionalAccessPolicyInfo) ConditionalAccessP
return conditionalAccessPolicy.GetGrantControls().GetBuiltInControls()
}

func (conditionalAccessPolicy *ADConditionalAccessPolicyInfo) ConditionalAccessPolicyGrantAuthenticationStrength() []models.AuthenticationMethodModes {
func (conditionalAccessPolicy *ADConditionalAccessPolicyInfo) ConditionalAccessPolicyGrantAuthenticationStrength() []models.AuthenticationMethodModes {
if conditionalAccessPolicy.GetGrantControls() == nil {
return nil
}
if conditionalAccessPolicy.GetGrantControls().GetAuthenticationStrength() == nil {
return nil
}
}
return conditionalAccessPolicy.GetGrantControls().GetAuthenticationStrength().GetAllowedCombinations()
}

Expand Down
84 changes: 84 additions & 0 deletions docs/tables/azuread_conditional_access_named_location.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
---
title: "Steampipe Table: azuread_conditional_access_named_location - Query Microsoft Entra Named Locations using SQL"
description: "Allows users to query Microsoft Entra Named Locations, providing information about custom definitions of Named Locations"
---

# Table: azuread_conditional_access_named_location - Query Microsoft Entra Named Locations using SQL

Microsoft Entra Named Locations is a feature in Azure Active Directory (Microsoft Entra) that allows administrators to define custom Named Locations. These Custom named locations can be included in Conditional Access Policies and restrict user access to this specific locations. There are two types of Named Locations - IP based Named locations and Country based Named Locations, the table supports both types.

## Table Usage Guide

The `azuread_conditional_access_named_location` table provides insights into Named Locations within Azure Active Directory (Microsoft Entra). As a security administrator, you can understand policies based on Named Locations better through this table, including display name, type, and detailed location information. Utilize it to uncover information about custom Named Locations, understand Conditional Access policies better, and maintain security and compliance within your organization.

## Examples

### Basic info
Analyze the settings to understand the status and creation date of the Named Locations in your Microsoft Entra Named Locations. This can help you assess the locations elements within your Conditional Access Policy and make necessary adjustments.

```sql+postgres
select
id,
display_name,
location_type,
created_date_time,
modified_date_time
from
azuread_conditional_access_named_location;
```

```sql+sqlite
select
id,
display_name,
location_type,
created_date_time,
modified_date_time
from
azuread_conditional_access_named_location;
```

### Detailed information about the Named Location definitions
Analyze detailed information about the definition of Named Locations in your Microsoft Entra Named Locations. This can help you understand the locations elements within your Conditional Access Policy and assure the definitions are compliance within your organization policies.

```sql+postgres
select
id,
display_name,
location_type,
location_info
from
azuread_conditional_access_named_location;
```

```sql+sqlite
select
id,
display_name,
location_type,
location_info
from
azuread_conditional_access_named_location;
```

### Detailed information about IP based named location
Retrieve IP based Named Locations in your Microsoft Entra Named Locations. This can help you understand the locations elements within your Conditional Access Policy distringuishes between different types of named locations (Options: [IP, Country]).

```sql+postgres
select
id,
display_name,
location_info
from
azuread_conditional_access_named_location where location_type = 'IP';
```

```sql+sqlite
select
id,
display_name,
location_info
from
azuread_conditional_access_named_location where location_type = 'IP';
```

0 comments on commit 2c31393

Please sign in to comment.