This stops IT from having to go into multiple different security provider sites/applications and collate all their metrics.
We'll leverage one PowerBI dataset/model which consolidates all these sources
- MS Graph
- Security
- Licenses
- Users
- Alerts
- Groups
- Good tutorial, with a minor error
Error: This tutorial's example API call of
/me
won't work if you set up the API in the way explained (delegated), as it does not run as an impersonated user, and the/me
command returns information about the (impersonated) user!! I wasted a few minutes trying to figure this one out You do want delegated perms, so follow the tute, just use a diff API call. - Lower quality tute, but still good reading
-
Set up an App registration for your application - name it something link "PowerBI to Graph" Links:
-
Give that app API permissions, allowing it to call specific API functions Links:
-
For Exposure scores, they're not under Microsoft Graph, so you need to add the WindowsDefenderATP API:
- Make query to MS login API, asking for an access token.
makeToken = (#"API endpoint URL" as any, #"Azure Tenant ID" as any, #"Azure Application ID" as any, #"Azure Application Client Secret" as any) => let loginURL = "https://login.microsoftonline.com/", TokenUri = #"Azure Tenant ID" & "/oauth2/token", // which domain is this a token for ResourceId = #"API endpoint URL", // where is this token for TokenResponse = Json.Document( Web.Contents(loginURL , [RelativePath = TokenUri , Content = Text.ToBinary(Uri.BuildQueryString([client_id = #"Azure Application ID", resource = ResourceId, grant_type = "client_credentials", client_secret = #"Azure Application Client Secret"])) , Headers = [Accept = "application/json"], ManualStatusHandling = {400} ] ) // end web contents ), // end json generatedToken = TokenResponse[access_token] // assign token value in generatedToken,
- Get token from that API return.
NB: Depending on your API endpoint, you will need to change the token's URL - ideally do this by passing different parameters (for each end-point) into the argument
API endpoint URL
.myToken = makeToken( #"Azure Graph API Url" , #"Azure Tenant ID" , #"Azure Application ID" , #"Azure Application Client Secret" ) // end makeToken
- Use this token in HTTP requests to MS graph
// Ping API for json Source = Json.Document( Web.Contents( #"Azure Graph API Url" , [ RelativePath = "beta/security/secureScores" , Headers = [ Authorization = "Bearer " & myToken ] ] ) ), value = Source[value],
Some queries exceed the amount of information an API return shoulod contain. These need to be "paginated", aka repeated, asking for the next bit of info.
MS uses odata
style for it's pagination, a fairly simple method, that just requires you to ask for the next odata-link.
- Here we define a function that will use a paginated query to fetch all users.
We reference the
@odata.nextLink
element of the returnedSource
object for the try-catch in this recursive function.
// Define a Function Called GetUserDetail that takes a single parameter Path; the MS Graph API Endpoint
GetUserDetail = (queryPath)=>
let
// call out to the web endpoint and convert the result to a JSON
// Document object which is saved in the Source variable
Source = Json.Document(
Web.Contents(
#"Azure Graph API Url"
, [RelativePath = queryPath, Headers = [Authorization = "Bearer " & myToken]]
)
),
// Assign the resultant List object (API return) to NextList variable
NextList= @Source[value],
nextLink = Text.AfterDelimiter(Source[#"@odata.nextLink"], #"Azure Graph API Url"),
// recursively call GetUserDetail function using the value of the @odata.nextLink as the path
// Since @odata.nextLink is not guaranteed to always exist in the case where the result is less than 100 we need to catch this and deal with it
// using a try otherwise basically means if you error out because no
// @odata.nextLink exisit then just return the resulting list of values you did get
// there is an API returned value called @odata.nextLink - so we can directly reference that
result = try @NextList & @GetUserDetail(nextLink) otherwise @NextList
in
// Return the result variable as the function return
result,
// Use the function GetUserDetail with the initial path to the object.
// The function will run recursively until it gets all the record and then
// loads the full result to UserDetail Variable
UserDetail = GetUserDetail("beta/users?$"),