Skip to content
Open
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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -232,3 +232,7 @@ tags

# Built Visual Studio Code Extensions
*.vsix
.github/workflows/cla.yml
.github/workflows/vuln-scan.yml
/.github
get_token.ps1
7 changes: 7 additions & 0 deletions client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import (
"github.com/bloodhoundad/azurehound/v2/models/azure"
"github.com/bloodhoundad/azurehound/v2/panicrecovery"
"github.com/bloodhoundad/azurehound/v2/pipeline"
"github.com/bloodhoundad/azurehound/v2/models/intune"
)

func NewClient(config config.Config) (AzureClient, error) {
Expand Down Expand Up @@ -221,6 +222,12 @@ type AzureClient interface {

TenantInfo() azure.Tenant
CloseIdleConnections()

// Add Intune methods
ListIntuneManagedDevices(ctx context.Context, params query.GraphParams) <-chan AzureResult[intune.ManagedDevice]
GetIntuneDeviceCompliance(ctx context.Context, deviceId string, params query.GraphParams) <-chan AzureResult[intune.ComplianceState]
GetIntuneDeviceConfiguration(ctx context.Context, deviceId string, params query.GraphParams) <-chan AzureResult[intune.ConfigurationState]

}

func (s azureClient) TenantInfo() azure.Tenant {
Expand Down
62 changes: 62 additions & 0 deletions client/intune_devices.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// File: client/intune_devices.go
// Copyright (C) 2022 SpecterOps
// Implementation of Intune device management API calls

package client

import (
"context"
"fmt"

"github.com/bloodhoundad/azurehound/v2/client/query"
"github.com/bloodhoundad/azurehound/v2/constants"
"github.com/bloodhoundad/azurehound/v2/models/intune"
)

func setDefaultParams(params *query.GraphParams) {
if params.Top == 0 {
params.Top = 999
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

// ListIntuneManagedDevices retrieves all managed devices from Intune
// GET /deviceManagement/managedDevices
func (s *azureClient) ListIntuneManagedDevices(ctx context.Context, params query.GraphParams) <-chan AzureResult[intune.ManagedDevice] {
var (
out = make(chan AzureResult[intune.ManagedDevice])
path = fmt.Sprintf("/%s/deviceManagement/managedDevices", constants.GraphApiVersion)
)

setDefaultParams(&params)

go getAzureObjectList[intune.ManagedDevice](s.msgraph, ctx, path, params, out)
return out
}

// GetIntuneDeviceCompliance retrieves compliance information for a specific device
// GET /deviceManagement/managedDevices/{id}/deviceCompliancePolicyStates
func (s *azureClient) GetIntuneDeviceCompliance(ctx context.Context, deviceId string, params query.GraphParams) <-chan AzureResult[intune.ComplianceState] {
var (
out = make(chan AzureResult[intune.ComplianceState])
path = fmt.Sprintf("/%s/deviceManagement/managedDevices/%s/deviceCompliancePolicyStates", constants.GraphApiVersion, deviceId)
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

setDefaultParams(&params)

go getAzureObjectList[intune.ComplianceState](s.msgraph, ctx, path, params, out)
return out
}

// GetIntuneDeviceConfiguration retrieves configuration information for a specific device
// GET /deviceManagement/managedDevices/{id}/deviceConfigurationStates
func (s *azureClient) GetIntuneDeviceConfiguration(ctx context.Context, deviceId string, params query.GraphParams) <-chan AzureResult[intune.ConfigurationState] {
var (
out = make(chan AzureResult[intune.ConfigurationState])
path = fmt.Sprintf("/%s/deviceManagement/managedDevices/%s/deviceConfigurationStates", constants.GraphApiVersion, deviceId)
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

setDefaultParams(&params)

go getAzureObjectList[intune.ConfigurationState](s.msgraph, ctx, path, params, out)
return out
}
204 changes: 204 additions & 0 deletions cmd/list-intune-compliance.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
// File: cmd/list-intune-compliance.go
// Command for listing Intune device compliance information

package cmd

import (
"context"
"fmt"
"os"
"os/signal"
"sync"
"time"

"github.com/bloodhoundad/azurehound/v2/client"
"github.com/bloodhoundad/azurehound/v2/client/query"
"github.com/bloodhoundad/azurehound/v2/config"
"github.com/bloodhoundad/azurehound/v2/enums"
"github.com/bloodhoundad/azurehound/v2/models/intune"
"github.com/bloodhoundad/azurehound/v2/panicrecovery"
"github.com/bloodhoundad/azurehound/v2/pipeline"
"github.com/spf13/cobra"
)

func createBasicComplianceState(device intune.ManagedDevice, suffix string) intune.ComplianceState {
return intune.ComplianceState{
Id: device.Id + suffix,
DeviceId: device.Id,
DeviceName: device.DeviceName,
State: device.ComplianceState,
Version: 1,
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

var (
complianceState string
includeDetails bool
)

func init() {
listRootCmd.AddCommand(listIntuneComplianceCmd)

listIntuneComplianceCmd.Flags().StringVar(&complianceState, "state", "", "Filter by compliance state: compliant, noncompliant, conflict, error, unknown")
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
listIntuneComplianceCmd.Flags().BoolVar(&includeDetails, "details", false, "Include detailed compliance settings")
}

var listIntuneComplianceCmd = &cobra.Command{
Use: "intune-compliance",
Short: "List Intune device compliance information",
Long: `List compliance information for Intune managed devices.

Examples:
# List all device compliance
azurehound list intune-compliance --jwt $JWT

# List only non-compliant devices
azurehound list intune-compliance --state noncompliant --jwt $JWT

# Include detailed compliance settings
azurehound list intune-compliance --details --jwt $JWT`,
Run: listIntuneComplianceCmdImpl,
SilenceUsage: true,
}

func listIntuneComplianceCmdImpl(cmd *cobra.Command, args []string) {
ctx, stop := signal.NotifyContext(cmd.Context(), os.Interrupt, os.Kill)
defer gracefulShutdown(stop)

log.V(1).Info("testing connections")
azClient := connectAndCreateClient()
log.Info("collecting intune device compliance...")
start := time.Now()
stream := listIntuneCompliance(ctx, azClient)
panicrecovery.HandleBubbledPanic(ctx, stop, log)
outputStream(ctx, stream)
duration := time.Since(start)
log.Info("collection completed", "duration", duration.String())
}

func listIntuneCompliance(ctx context.Context, client client.AzureClient) <-chan interface{} {
var (
out = make(chan interface{})
)

go func() {
defer panicrecovery.PanicRecovery()
defer close(out)

// First get all managed devices
devices := getComplianceTargetDevices(ctx, client)

// Then collect compliance data for each device
collectDeviceCompliance(ctx, client, devices, out)
}()

return out
}

func getComplianceTargetDevices(ctx context.Context, client client.AzureClient) <-chan intune.ManagedDevice {
var (
out = make(chan intune.ManagedDevice)
params = query.GraphParams{
Filter: "operatingSystem eq 'Windows'",
}
)

// Apply compliance state filter if specified
if complianceState != "" {
if params.Filter != "" {
params.Filter += " and "
}
params.Filter += fmt.Sprintf("complianceState eq '%s'", complianceState)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

go func() {
defer panicrecovery.PanicRecovery()
defer close(out)

count := 0
for item := range client.ListIntuneManagedDevices(ctx, params) {
if item.Error != nil {
log.Error(item.Error, "unable to continue processing devices")
} else {
log.V(2).Info("found device for compliance check", "device", item.Ok.DeviceName)
count++
select {
case out <- item.Ok:
case <-ctx.Done():
return
}
}
}
log.V(1).Info("finished collecting target devices", "count", count)
}()

return out
}

func collectDeviceCompliance(ctx context.Context, client client.AzureClient, devices <-chan intune.ManagedDevice, out chan<- interface{}) {
var (
streams = pipeline.Demux(ctx.Done(), devices, config.ColStreamCount.Value().(int))
wg sync.WaitGroup
)

wg.Add(len(streams))
for i := range streams {
stream := streams[i]
go func() {
defer panicrecovery.PanicRecovery()
defer wg.Done()

for device := range stream {
if includeDetails {
collectDetailedCompliance(ctx, client, device, out)
} else {
basicCompliance := createBasicComplianceState(device, "-basic")
select {
case out <- NewAzureWrapper(enums.KindAZIntuneCompliance, basicCompliance):
case <-ctx.Done():
return
}
}
}
}()
}
wg.Wait()
}

func collectDetailedCompliance(ctx context.Context, client client.AzureClient, device intune.ManagedDevice, out chan<- interface{}) {
log.V(2).Info("collecting detailed compliance", "device", device.DeviceName)

params := query.GraphParams{}
count := 0

for complianceResult := range client.GetIntuneDeviceCompliance(ctx, device.Id, params) {
if complianceResult.Error != nil {
log.Error(complianceResult.Error, "failed to get detailed compliance", "device", device.DeviceName)

// Fall back to basic compliance info using helper
basicCompliance := createBasicComplianceState(device, "-fallback")
select {
case out <- NewAzureWrapper(enums.KindAZIntuneCompliance, basicCompliance):
case <-ctx.Done():
return
}
continue
}

log.V(2).Info("found detailed compliance state",
"device", device.DeviceName,
"state", complianceResult.Ok.State,
"settingsCount", len(complianceResult.Ok.SettingStates))

count++
select {
case out <- NewAzureWrapper(enums.KindAZIntuneCompliance, complianceResult.Ok):
case <-ctx.Done():
return
}
}

if count > 0 {
log.V(1).Info("finished detailed compliance collection", "device", device.DeviceName, "policies", count)
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
76 changes: 76 additions & 0 deletions cmd/list-intune-devices.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// File: cmd/list-intune-devices.go
// Copyright (C) 2022 SpecterOps
// Command implementation for listing Intune managed devices

package cmd

import (
"context"
"os"
"os/signal"
"time"

"github.com/bloodhoundad/azurehound/v2/client"
"github.com/bloodhoundad/azurehound/v2/client/query"
"github.com/bloodhoundad/azurehound/v2/enums"
"github.com/bloodhoundad/azurehound/v2/panicrecovery"
"github.com/spf13/cobra"
)

func init() {
listRootCmd.AddCommand(listIntuneDevicesCmd)
}

var listIntuneDevicesCmd = &cobra.Command{
Use: "intune-devices",
Long: "Lists Intune Managed Devices",
Run: listIntuneDevicesCmdImpl,
SilenceUsage: true,
}

func listIntuneDevicesCmdImpl(cmd *cobra.Command, args []string) {
ctx, stop := signal.NotifyContext(cmd.Context(), os.Interrupt, os.Kill)
defer gracefulShutdown(stop)

log.V(1).Info("testing connections")
azClient := connectAndCreateClient()
log.Info("collecting intune managed devices...")
start := time.Now()
stream := listIntuneDevices(ctx, azClient)
panicrecovery.HandleBubbledPanic(ctx, stop, log)
outputStream(ctx, stream)
duration := time.Since(start)
log.Info("collection completed", "duration", duration.String())
}

func listIntuneDevices(ctx context.Context, client client.AzureClient) <-chan interface{} {
var (
out = make(chan interface{})
params = query.GraphParams{
Filter: "operatingSystem eq 'Windows'", // Focus on Windows devices for BloodHound
}
)

go func() {
defer panicrecovery.PanicRecovery()
defer close(out)

count := 0
for item := range client.ListIntuneManagedDevices(ctx, params) {
if item.Error != nil {
log.Error(item.Error, "unable to continue processing intune devices")
} else {
log.V(2).Info("found intune device", "device", item.Ok)
count++
select {
case out <- NewAzureWrapper(enums.KindAZIntuneDevice, item.Ok):
case <-ctx.Done():
return
}
}
}
log.V(1).Info("finished listing intune devices", "count", count)
}()

return out
}
Loading
Loading