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

Add azuread_conditional_access_named_location table #191

Merged
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
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),
TheRealHouseMouse marked this conversation as resolved.
Show resolved Hide resolved
"azuread_conditional_access_policy": tableAzureAdConditionalAccessPolicy(ctx),
"azuread_device": tableAzureAdDevice(ctx),
"azuread_directory_audit_report": tableAzureAdDirectoryAuditReport(ctx),
Expand Down
261 changes: 261 additions & 0 deletions azuread/table_azuread_conditional_access_named_location.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
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"}),
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we treat the Invalid object identifier error as a "not found" error? It appears that this error occurs when the input provided is incorrect. In my opinion, it would be better to notify the user to provide the correct input instead.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It is the same error as in:
table_azuread_conditional_access_policy
table_azuread_user
table_azuread_group
And other tables in the plugin...
I can change them all to "Request_ResourceInvalidIdentifier" but I think it is better to keep it that way and not change the other tables.

},
KeyColumns: plugin.SingleColumn("id"),
},
List: &plugin.ListConfig{
Hydrate: listAdConditionalAccessNamedLocations,
IgnoreConfig: &plugin.IgnoreConfig{
ShouldIgnoreErrorFunc: isIgnorableErrorPredicate([]string{"Request_UnsupportedQuery"}),
Copy link
Contributor

Choose a reason for hiding this comment

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

I believe we are constructing the query parameter using the key qualifiers provided as query parameters. Are there any specific query parameter combinations where we encounter the Request_UnsupportedQuery error?

Additionally, I noticed that we only build the query parameter when the display_name is included in the WHERE clause.

Instead of adding this to the ignore configuration, it would be better if we could handle it directly in our table code by building the query parameter based on the API's behavior.

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'm sorry I don't think I fully understand you. I copied this part from the other tables in the plugin. I changed the type column to location_type because type is a reserved in SQL. I can't think about a specific query parameter combinations where we encounter the Request_UnsupportedQuery.
Added support for id, location_type to be handled by our table code. Is it alright now?

},
KeyColumns: []*plugin.KeyColumn{
{Name: "display_name", Require: plugin.Optional},
TheRealHouseMouse marked this conversation as resolved.
Show resolved Hide resolved
{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")},
TheRealHouseMouse marked this conversation as resolved.
Show resolved Hide resolved
{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 "Unkown"
}
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';
```

Loading