Skip to content
Open
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
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
81 changes: 81 additions & 0 deletions client/intune_devices.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// 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) query.GraphParams {
if params.Top == 0 {
params.Top = 999
}
return params
}

// 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)
)

params = 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] {
if deviceId == "" {
out := make(chan AzureResult[intune.ComplianceState])
go func() {
defer close(out)
out <- AzureResult[intune.ComplianceState]{Error: fmt.Errorf("deviceId cannot be empty")}
}()
return out
}

var (
out = make(chan AzureResult[intune.ComplianceState])
path = fmt.Sprintf("/%s/deviceManagement/managedDevices/%s/deviceCompliancePolicyStates", constants.GraphApiVersion, deviceId)
)

params = 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] {
if deviceId == "" {
out := make(chan AzureResult[intune.ConfigurationState])
go func() {
defer close(out)
out <- AzureResult[intune.ConfigurationState]{Error: fmt.Errorf("deviceId cannot be empty")}
}()
return out
}

var (
out = make(chan AzureResult[intune.ConfigurationState])
path = fmt.Sprintf("/%s/deviceManagement/managedDevices/%s/deviceConfigurationStates", constants.GraphApiVersion, deviceId)
)

params = setDefaultParams(params)

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

package cmd

import (
"context"
"fmt"
"os"
"os/signal"
"strings"
"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: fmt.Sprintf("%s-%s-%d", device.Id, suffix, time.Now().UnixNano()),
DeviceId: device.Id,
DeviceName: device.DeviceName,
State: device.ComplianceState,
Version: 1,
}
}

var (
complianceState string
includeDetails bool
)

func init() {
listRootCmd.AddCommand(listIntuneComplianceCmd)

listIntuneComplianceCmd.Flags().StringVar(&complianceState, "state", "", "Filter by compliance state: compliant, noncompliant, conflict, error, unknown, inGracePeriod")
listIntuneComplianceCmd.Flags().BoolVar(&includeDetails, "details", false, "Include detailed compliance settings")

// Add validation for compliance state
listIntuneComplianceCmd.PreRunE = func(cmd *cobra.Command, args []string) error {
if complianceState != "" {
validStates := []string{"compliant", "noncompliant", "conflict", "error", "unknown", "inGracePeriod"}
for _, state := range validStates {
if complianceState == state {
return nil
}
}
return fmt.Errorf("invalid compliance state: %s. Valid values: %s", complianceState, strings.Join(validStates, ", "))
}
return nil
}
}

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 != "" {
// Validate complianceState to prevent injection
validStates := map[string]bool{
"compliant": true, "noncompliant": true, "conflict": true,
"error": true, "unknown": true, "inGracePeriod": true,
}
if !validStates[complianceState] {
log.Error(fmt.Errorf("invalid compliance state"), "invalid state provided", "state", complianceState)
close(out)
return out
}

if params.Filter != "" {
params.Filter += " and "
}
params.Filter += fmt.Sprintf("complianceState eq '%s'", complianceState)
}

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
hasError := false

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)
hasError = true
break // Stop processing this device on first error
}

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
}
}

// Handle fallback after the loop
if hasError && count == 0 {
basicCompliance := createBasicComplianceState(device, "fallback")
select {
case out <- NewAzureWrapper(enums.KindAZIntuneCompliance, basicCompliance):
case <-ctx.Done():
return
}
}

if count > 0 {
log.V(1).Info("finished detailed compliance collection", "device", device.DeviceName, "policies", count)
}
}
Loading
Loading