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 15 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 @@ -34,6 +34,7 @@ func Plugin(ctx context.Context) *plugin.Plugin {
"azuread_application_app_role_assigned_to": tableAzureAdApplicationAppRoleAssignment(ctx),
"azuread_authorization_policy": tableAzureAdAuthorizationPolicy(ctx),
"azuread_conditional_access_policy": tableAzureAdConditionalAccessPolicy(ctx),
"azuread_conditional_access_named_location": tableAzureAdConditionalAccessNamedLocation(ctx),
TheRealHouseMouse marked this conversation as resolved.
Show resolved Hide resolved
"azuread_device": tableAzureAdDevice(ctx),
"azuread_directory_audit_report": tableAzureAdDirectoryAuditReport(ctx),
"azuread_directory_role": tableAzureAdDirectoryRole(ctx),
Expand Down
163 changes: 163 additions & 0 deletions azuread/table_azuread_conditional_access_named_location.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
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
},
},

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_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")},
TheRealHouseMouse marked this conversation as resolved.
Show resolved Hide resolved
{Name: "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
}),
}
}

//// 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("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("listAdConditionalAccessNamedLocations", "create_iterator_instance_error", err)
TheRealHouseMouse marked this conversation as resolved.
Show resolved Hide resolved
return nil, err
}

err = pageIterator.Iterate(ctx, func(pageItem models.NamedLocationable) bool {
switch t := pageItem.(type) {
case *models.IpNamedLocation:
d.StreamListItem(ctx, &ADIpNamedLocationInfo{t})
case *models.CountryNamedLocation:
d.StreamListItem(ctx, &ADCountryNamedLocationInfo{t})
Copy link
Contributor

Choose a reason for hiding this comment

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

It would be ideal if we could use StreamList to structure the items in a generic structure rather than basing StreamList results on the response type.

This approach would be beneficial if this table is used as a parent for other tables in the future when retrieving parent table data.

For reference:

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 managed to change it, but tool me a lot of time and the results isn't elegant at all. I'm not sure at all this is better than before. If you have a suggestion how to make it better please tell me. I think it might be better before this change.

}

// 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("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("getAdConditionalAccessNamedLocation", "get_conditional_access_location_error", errObj)
return nil, errObj
}
return &ADLocationInfo{location}, nil
}

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

filterQuals := map[string]string{
"display_name": "string",
"state": "string",
TheRealHouseMouse marked this conversation as resolved.
Show resolved Hide resolved
}

for qual, qualType := range filterQuals {
switch qualType {
case "string":
if equalQuals[qual] != nil {
filters = append(filters, fmt.Sprintf("%s eq '%s'", strcase.ToCamel(qual), equalQuals[qual].GetStringValue()))
TheRealHouseMouse marked this conversation as resolved.
Show resolved Hide resolved
}
}
}

return filters
}
74 changes: 72 additions & 2 deletions azuread/transforms.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,18 @@ type ADIdentityProviderInfo struct {
ClientSecret interface{}
}

type ADLocationInfo struct {
models.NamedLocationable
}

type ADIpNamedLocationInfo struct {
models.IpNamedLocationable
}

type ADCountryNamedLocationInfo struct {
models.CountryNamedLocationable
}

type ADSecurityDefaultsPolicyInfo struct {
models.IdentitySecurityDefaultsEnforcementPolicyable
}
Expand Down Expand Up @@ -437,13 +449,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 Expand Up @@ -556,6 +568,64 @@ func (device *ADDeviceInfo) DeviceMemberOf() []map[string]interface{} {
return members
}

func (ipLocationInfo *ADIpNamedLocationInfo) GetLocationInfo() 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["type"] = "IP"
TheRealHouseMouse marked this conversation as resolved.
Show resolved Hide resolved
locationInfoJSON["IPv4Cidr"] = IPv4CidrArr
locationInfoJSON["IPv4Range"] = IPv4RangeArr
locationInfoJSON["IPv6Cidr"] = IPv6CidrArr
locationInfoJSON["IPv6Range"] = IPv6RangeArr
locationInfoJSON["IsTrusted"] = ipLocationInfo.GetIsTrusted()
return locationInfoJSON
}

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

func (ipLocationInfo *ADIpNamedLocationInfo) GetType() string {
return "IP"
}

func (countryLocationInfo *ADCountryNamedLocationInfo) GetType() string {
return "Country"
}

func (directoryAuditReport *ADDirectoryAuditReportInfo) DirectoryAuditAdditionalDetails() []map[string]interface{} {
if directoryAuditReport.GetAdditionalDetails() == nil {
return nil
Expand Down
62 changes: 62 additions & 0 deletions docs/tables/azuread_conditional_access_named_location.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
---
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,
type,
created_date_time,
modified_date_time
from
azuread_conditional_access_named_location;
```

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

### Detailed information about the Namedl 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,
type,
location_info
from
azuread_conditional_access_named_location;
```

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