Intune Compliance Power BI Power Automate Microsoft Graph

Quản lý thiết bị Non-Compliance trên Intune: phân loại nguyên nhân, tự động xử lý và chặn truy cập

Quản lý thiết bị Non-Compliance trên Intune: phân loại nguyên nhân, tự động xử lý và chặn truy cập

Mở báo cáo Compliance trong Intune, mình thấy hàng trăm thiết bị Not compliant: laptop Windows, MacBook, điện thoại Android và iPhone. Nhìn qua thì tưởng đều là “máy có vấn đề bảo mật”. Nhưng khi đào sâu, phần lớn lại là máy đã thanh lý, máy nằm trong kho, máy enroll trùng serial, hoặc máy của nhân viên đã nghỉ việc. Chỉ một phần nhỏ thật sự là máy đang được dùng nhưng không tuân thủ policy. Với môi trường khoảng 2.000 máy tính cộng thêm thiết bị di động, xử lý từng máy bằng tay là không khả thi. Bài này chia sẻ cách mình phân loại tự động và xử lý theo từng nguyên nhân.

Tóm tắt nhanh: Power BI gộp dữ liệu từ Intune Data Warehouse, SentinelOne, Jira Asset và AD, rồi dùng DAX gán cho mỗi máy một Reason và Action Require. Power Automate lấy nhóm Delete / Backup&Delete / Wipe ra CSV để chạy Graph API, còn các nhóm còn lại được thêm vào một group bị chặn truy cập tài nguyên công ty, kèm email báo cho user.

Bối cảnh: vì sao không thể tin hoàn toàn vào Intune

Danh sách Non-Compliance của Intune có ba vấn đề:

  • Last check-in chưa chính xác. Có máy vẫn đang online nhưng Intune ghi nhận check-in từ nhiều tuần trước.
  • Không biết trạng thái thực tế của máy. Intune không biết máy đang ở kho, đang chuẩn bị bàn giao hay đã thanh lý.
  • Không biết trạng thái tài khoản. Primary user có thể đã bị disable trên AD từ lâu.

Vì vậy mình bổ sung thêm ba nguồn dữ liệu để có bức tranh đầy đủ.

Nguồn Dùng để làm gì
Intune Data Warehouse Danh sách thiết bị, trạng thái compliance, serial, primary user, last check-in
SentinelOne Last seen từ agent EDR, dùng để đối chiếu với last check-in của Intune
Jira Asset Trạng thái tài sản: In Use, In Stock, In Prepare, Liquidation, user đang được gán
AD Computer / User object Tài khoản user và máy đang Enabled hay Disabled

Kiến trúc tổng thể

Bước Thành phần Việc làm
1 Power BI Lấy dữ liệu từ 4 nguồn, dùng DAX tạo bảng NonCompliance
2 DAX Gán Reason và Action Require cho từng máy
3 Power Automate (Flow 1) Lấy máy có Action = Delete, Backup&Delete, Wipe, xuất CSV
4 Graph API Backup khóa (nếu cần), rồi delete hoặc wipe thiết bị
5 Power Automate (Flow 2) Lấy các Action còn lại, thêm thiết bị vào group chặn truy cập
6 Intune Group đã được assign sẵn cho app/policy thực thi chặn truy cập
7 Power Automate (Flow 3) Gửi email cho user, hướng dẫn cách khắc phục

Hình 1: luồng xử lý từ Power BI đến Intune.

Bảng phân loại Reason và Action Require

Đây là phần quan trọng nhất của cả quy trình. Mỗi máy chỉ nhận một Reason, theo thứ tự ưu tiên từ trên xuống: điều kiện nào khớp trước thì dừng lại.

Thứ tự Reason Action Require
1 Duplicate serial number (older check-in) Delete
2 Liquidation Delete
3 Device already in wipe state Delete
4 In Stock and Primary User has a new device Delete
5 In use and inactive for over 90 days Backup&Delete
6 In stock and inactive for over 30 days Wipe
7 No Serial Number Re-check
8 No Primary User Re-enroll
9 Primary user account disabled Re-enroll
10 In use and inactive for over 30 days Contact User
11 Other reasons Other

Vì sao thứ tự quan trọng? Một máy vừa trùng serial vừa inactive 90 ngày phải được xếp vào nhóm Delete (bản ghi cũ, an toàn để xóa), chứ không phải Backup&Delete. Tương tự, điều kiện “inactive 90 ngày” phải đứng trước “inactive 30 ngày”.

💡
Tip"Inactive" được tính từ last check-in gần nhất giữa Intune và SentinelOne, không chỉ từ Intune. Nhờ vậy, máy vẫn đang online nhưng Intune chưa cập nhật sẽ không bị wipe hay xóa nhầm.

Bước 1-2: Tạo bảng phân loại bằng DAX

Lấy last check-in gần nhất

Ý tưởng là so sánh hai mốc thời gian và lấy mốc mới hơn:

LastActivity =
VAR _Intune = Intune[lastSyncDateTime]
VAR _S1 =
    LOOKUPVALUE ( S1[Last Active Date], S1[Serial Number], Intune[serialNumber] )
RETURN
    IF ( ISBLANK ( _S1 ) || _Intune >= _S1, _Intune, _S1 )

Bảng NonCompliance hoàn chỉnh

Dưới đây là khung DAX mình dùng tạo bảng mơi trên Power BI. Tên bảng và cột là ví dụ, bạn đổi theo dataset thật:

Non-Compliance =  
VAR ExtendedIntune = 
    ADDCOLUMNS ( 
        FILTER ( 
            Intune, 
            Intune[isDeleted] = FALSE && 
            Intune[operatingSystem] = "Windows" && 
            LEFT(Intune[deviceName], 3) = "VN-" && 
            NOT ISBLANK(Intune[azureADRegistered]) && 
            (Intune[deviceRegistrationStateKey] = 5172 || Intune[deviceRegistrationStateKey] = 5226) 
        ), 
        "ComplianceStatus", LOOKUPVALUE ( 
            State[complianceStatus], 
            State[complianceStateKey], Intune[complianceStateKey] 
        ), 
        "JiraStatus", CALCULATE ( 
            MAX ( Jira[Status] ), 
            FILTER ( Jira, Jira[Serial Number] = Intune[serialNumber] ) 
        ), 
        "JiraStock", CALCULATE ( 
            MAX ( Jira[Stock] ), 
            FILTER ( Jira, Jira[Serial Number] = Intune[serialNumber] ) 
        ), 
        "OfficeLocation", CALCULATE ( 
            MAX ( UsersAD[physicalDeliveryOfficeName] ), 
            FILTER ( UsersAD, UsersAD[employeeNumber] = Intune[primaryUser] ) 
        ), 
        "UserAccountControl", CALCULATE ( 
            MAX ( UsersAD[userAccountControl] ), 
            FILTER ( UsersAD, UsersAD[employeeNumber] = Intune[primaryUser] ) 
        ), 
        "DeviceAccountControl", CALCULATE ( 
            MAX ( DevicesAD[userAccountControl] ), 
            FILTER ( DevicesAD, DevicesAD[serialNumber] = Intune[serialNumber] ) 
        ), 
        "LastActiveDate", CALCULATE ( 
            MAX ( Agents[lastActiveDt] ), 
            FILTER ( Agents, Agents[serialNumber] = Intune[serialNumber] ) 
        ), 
        "LastRebootDate", CALCULATE ( 
            MAX ( Agents[lastRebootDt] ), 
            FILTER ( Agents, Agents[serialNumber] = Intune[serialNumber] ) 
        ), 
        "WipeState", CALCULATE ( 
            MAX ( Wipe[managementStateName] ), 
            FILTER ( Wipe, Wipe[managementStateKey] = Intune[managementStateKey] ) 
        ), 
        "DuplicateSerialMaxCheckin", CALCULATE ( 
            MAX ( Intune[lastSyncDateTime] ), 
            ALLEXCEPT ( Intune, Intune[serialNumber] ) 
        ), 
        "DuplicateSerialCount", CALCULATE ( 
            COUNTROWS ( Intune ), 
            ALLEXCEPT ( Intune, Intune[serialNumber] ) 
        ), 
        "PrimaryUserDeviceCount", CALCULATE ( 
            COUNTROWS ( Intune ), 
            FILTER ( Intune, Intune[primaryUser] = EARLIER(Intune[primaryUser]) ) 
        ) 
    ) 
VAR FilteredIntune = 
    FILTER ( 
        ExtendedIntune, 
        [ComplianceStatus] = "Noncompliant" 
    ) 
VAR FinalIntune = 
    ADDCOLUMNS ( 
        FilteredIntune, 
        "Reason", SWITCH ( 
            TRUE(), 
            TRIM(Intune[serialNumber]) = "", "No Serial Number", 
            ISBLANK([JiraStatus]) || ISBLANK([JiraStock]), "Liquidation", 
            [DuplicateSerialCount] > 1 && Intune[lastSyncDateTime] < [DuplicateSerialMaxCheckin], "Duplicate serial number (older check-in)", 
            [WipeState] IN { "RetirePending", "WipeFailed", "WipePending", "RetireIssued", "WipeIssued" }, "Device already in wipe state", 
            [JiraStatus] = "IN STOCK" && 
            Intune[primaryUser] <> "00000000-0000-0000-0000-000000000000" && 
            Intune[lastSyncDateTime] < TODAY() - 30 && 
            (ISBLANK([LastActiveDate]) || [LastActiveDate] < TODAY() - 30) && 
            [PrimaryUserDeviceCount] > 1, "In Stock and Primary User has a new device", 
            [JiraStatus] = "IN STOCK" && 
            Intune[lastSyncDateTime] < TODAY() - 30 && 
            (ISBLANK([LastActiveDate]) || [LastActiveDate] < TODAY() - 30), "In stock and inactive for over 30 days", 
            [JiraStatus] = "IN USE" && 
            (Intune[primaryUser] = "00000000-0000-0000-0000-000000000000" || 
            CALCULATE ( 
                COUNTROWS ( 
                    FILTER ( 
                        User, 
                        User[userId] = SELECTEDVALUE(Intune[primaryUser]) && 
                        (User[isDeleted] = TRUE || ISBLANK(User[userEmail])) 
                    ) 
                ) 
            ) > 0), "No Primary User", 
            [JiraStatus] = "IN USE" && 
            Intune[lastSyncDateTime] < TODAY() - 30 && 
            Intune[primaryUser] <> "00000000-0000-0000-0000-000000000000" && 
            (ISBLANK([LastActiveDate]) || [LastActiveDate] < TODAY() - 30), "In use and inactive for over 30 days", 
            CALCULATE ( 
                COUNTROWS ( 
                    FILTER ( 
                        User, 
                        User[userId] = SELECTEDVALUE(Intune[primaryUser]) && 
                        (User[intuneLicensed] = FALSE) 
                    ) 
                ) 
            ) > 0, "Primary User account Disabled", 
            "Other reasons" 
        ), 
        "ActionRequire", SWITCH ( 
            TRUE(), 
            TRIM(Intune[serialNumber]) = "", "Re-check", 
            ISBLANK([JiraStatus]) || ISBLANK([JiraStock]), "Delete", 
            [DuplicateSerialCount] > 1 && Intune[lastSyncDateTime] < [DuplicateSerialMaxCheckin], "Delete", 
            [WipeState] IN { "RetirePending", "WipeFailed", "WipePending", "RetireIssued", "WipeIssued" }, "Delete", 
            [JiraStatus] = "IN STOCK" && 
            Intune[primaryUser] <> "00000000-0000-0000-0000-000000000000" && 
            Intune[lastSyncDateTime] < TODAY() - 30 && 
            (ISBLANK([LastActiveDate]) || [LastActiveDate] < TODAY() - 30) && 
            [PrimaryUserDeviceCount] > 1, "Delete", 
            [JiraStatus] = "IN STOCK" && 
            Intune[lastSyncDateTime] < TODAY() - 30 && 
            (ISBLANK([LastActiveDate]) || [LastActiveDate] < TODAY() - 30), "Wipe", 
            [JiraStatus] = "IN USE" && 
            (Intune[primaryUser] = "00000000-0000-0000-0000-000000000000" || 
            CALCULATE ( 
                COUNTROWS ( 
                    FILTER ( 
                        User, 
                        User[userId] = SELECTEDVALUE(Intune[primaryUser]) && 
                        (User[isDeleted] = TRUE || ISBLANK(User[userEmail])) 
                    ) 
                ) 
            ) > 0), "Re-enroll", 
            [JiraStatus] = "IN USE" && 
            Intune[primaryUser] <> "00000000-0000-0000-0000-000000000000" && 
            Intune[lastSyncDateTime] < TODAY() - 30 && 
            (ISBLANK([LastActiveDate]) || [LastActiveDate] < TODAY() - 30), "Contact User", 
            CALCULATE ( 
                COUNTROWS ( 
                    FILTER ( 
                        User, 
                        User[userId] = SELECTEDVALUE(Intune[primaryUser]) && 
                        (User[intuneLicensed] = FALSE) 
                    ) 
                ) 
            ) > 0, "Re-enroll", 
            "Other" 
        ) 
    ) 
RETURN 
SELECTCOLUMNS ( 
    FinalIntune, 
    "Reason", [Reason], 
    "Action Require", [ActionRequire], 
    "Device ID", Intune[deviceId], 
    "Device Name", Intune[deviceName], 
    "Last Check-in", Intune[lastSyncDateTime], 
    "Last Reboot", [LastRebootDate], 
    "AD State", [DeviceAccountControl], 
    "Device State", [ComplianceStatus], 
    "Serial Number", Intune[serialNumber], 
    "Primary User UPN", Intune[primaryUser], 
    "Office Location", [OfficeLocation], 
    "Jira Status", [JiraStatus], 
    "Jira Stock", [JiraStock] 
) 

Khi viết DAX cho bài toán này, chú ý:

  • LOOKUPVALUE sẽ báo lỗi nếu khóa tra cứu bị trùng (ví dụ một serial có hai dòng trong Jira). Hãy lọc trùng ở Power Query trước, hoặc dùng MAXX(FILTER(...)).
  • Ghi chú thêm cột Reason dạng chuỗi rõ ràng để Power Automate lọc và để License/Helpdesk team đọc hiểu ngay.
  • Với máy bị loại khỏi kiểm tra (ví dụ máy không thuộc phạm vi), vẫn giữ trong bảng nhưng đặt Action = Other kèm lý do, để báo cáo không bị “mất” thiết bị.

Bước 3-4: Xóa và wipe thiết bị bằng Power Automate và Graph API

Flow 1: xuất danh sách ra CSV

  1. Recurrence chạy theo lịch, ví dụ mỗi tuần.
  2. Run a query against a dataset lấy các dòng có Action là Delete, Backup&Delete hoặc Wipe:
EVALUATE
FILTER (
    NonCompliance,
    NonCompliance[Action Require] IN { "Delete", "Backup&Delete", "Wipe" }
)
  1. Create CSV table rồi Create file lưu vào SharePoint, đặt tên kèm ngày chạy để làm bằng chứng.

Gọi Graph API

Một PowerShell script (chạy trên server hoặc Azure Automation bằng App Registration có quyền DeviceManagementManagedDevices.ReadWrite.All và DeviceManagementManagedDevices.PrivilegedOperations.All) đọc CSV và xử lý theo Action:

Action Graph API
Delete DELETE /deviceManagement/managedDevices/{id}
Wipe POST /deviceManagement/managedDevices/{id}/wipe
Backup&Delete Backup BitLocker key (/informationProtection/bitlocker/recoveryKeys) và LAPS (/directory/deviceLocalCredentials), lưu lại, rồi mới DELETE
# Ví dụ khung xử lý - đọc CSV và gọi Graph theo Action

#Delete devices

$GetDevices = Import-csv "C:\Temp\DevicesToRemove.csv" #Intune ID
$serverUrl = "https://graph.microsoft.com"
$userToken = Get-Content -Path 'C:\Temp\IntuneToken.txt'
$apiRequest = '/beta/deviceManagement/managedDevices/executeAction'   ## Change to Desired Get Method API Call


Start-Transcript "C:\Temp\IntuneCleanup.txt" -Force
foreach($Device in $GetDevices){
    $BodyJSON         = @{
        "actionName"            = "delete"
        "deviceIds"             = @(
            "$($Device."DeviceID")"
        )
    } | ConvertTo-JSON -Depth 20
    Write-Host "Deleting Tired devices($($Device."Device Name") - User: $($Device."Primary user UPN"))" -ForegroundColor Yellow
    $Params = @{ Uri = ($serverUrl + $apiRequest)
        Headers = @{'Authorization' = "Bearer $($userToken)"}
        Method = 'POST'
        BODY  = $BodyJSON
        ContentType = "application/json" }
    Invoke-RestMethod @Params
}
Stop-Transcript

#Backup LAPS, Bitlocker trước
# ============ INPUT ============
$CsvInput   = "C:\Temp\DevicesToRemove.csv" #Entra ID
$TokenFile  = "C:\Temp\IntuneToken.txt" #Get from Intune Web
$CsvOutput  = "C:\Temp\LAPS_BitLocker_$(Get-Date -Format 'yyyyMMdd_HHmm').csv"
# ============ GRAPH ============
$GraphBeta = "https://graph.microsoft.com/beta"
$GraphV1   = "https://graph.microsoft.com/v1.0"
# ============ AUTH ============
$Token = (Get-Content -Path $TokenFile -Raw) -replace '\s',''
$Headers = @{
    Authorization  = "Bearer $Token"
    "Content-Type" = "application/json"
}
# ============ HELPERS ============
function Invoke-Graph {
    param($Uri)
    return Invoke-RestMethod -Uri $Uri -Headers $Headers -Method GET -ErrorAction Stop
}
function Invoke-GraphRetry {
    param($Uri)
    for ($i=1; $i -le 3; $i++) {
        try {
            return Invoke-Graph $Uri
        }
        catch {
            Start-Sleep -Seconds (2 * $i)
        }
    }
    throw
}
# ============ MAIN ============
$Devices = Import-Csv $CsvInput
$Report  = @()
$i = 0
$total = $Devices.Count
foreach ($row in $Devices) {
    $i++
    $DeviceId = ($row.DeviceID -replace '\s','')
    Write-Host "`n[$i/$total] Processing $DeviceId" -ForegroundColor Cyan
    # ================= DEVICE NAME =================
    $DeviceName = ""
    $AzureAdDeviceId = ""
    try {
        $dev = Invoke-GraphRetry "$GraphV1/devices/$DeviceId`?`$select=id,deviceId,displayName"
        $DeviceName = $dev.displayName
        $AzureAdDeviceId = $dev.deviceId
        Write-Host "   🖥️ $DeviceName" -ForegroundColor Green
    }
    catch {
        Write-Host "   ⚠️ Device lookup failed" -ForegroundColor Yellow
    }
    # ================= LAPS =================
    $AccountName=""
    $Password=""
    $BackupDateTime=""
    $LAPSStatus=""
    try {
        $lapsUri = "$GraphBeta/directory/deviceLocalCredentials/$DeviceId`?`$select=credentials"
        $res = Invoke-GraphRetry $lapsUri
        if ($res.credentials) {
            $latest = $res.credentials |
                Sort-Object {[datetime]$_.backupDateTime} -Descending |
                Select-Object -First 1
            $Password = [Text.Encoding]::UTF8.GetString(
                [Convert]::FromBase64String($latest.passwordBase64)
            )
            $AccountName = $latest.accountName
            $BackupDateTime = ([datetime]$latest.backupDateTime).ToString("yyyy-MM-dd HH:mm:ss")
            $LAPSStatus = "OK"
            Write-Host "   ✅ LAPS OK" -ForegroundColor Green
        }
        else {
            $LAPSStatus = "No LAPS"
        }
    }
    catch {
        $LAPSStatus = "ERROR"
    }
    Start-Sleep -Milliseconds 200
    # ================= BITLOCKER =================
    $BitLockerKeyId=""
    $BitLockerKey=""
    $BitLockerDate=""
    $BitLockerStatus=""
    $candidateIds = @($DeviceId)
    if ($AzureAdDeviceId) { $candidateIds += $AzureAdDeviceId }
    foreach ($cid in $candidateIds) {
        try {
            Write-Host "   🔎 Try BitLocker deviceId: $cid" -ForegroundColor DarkCyan
            $filter = [uri]::EscapeDataString("deviceId eq '$cid'")
            $uri = "$GraphV1/informationProtection/bitlocker/recoveryKeys?`$filter=$filter"
            $list = Invoke-GraphRetry $uri
            if (-not $list.value) { continue }
            $latest = $list.value |
                Sort-Object {[datetime]$_.createdDateTime} -Descending |
                Select-Object -First 1
            $keyUri = "$GraphV1/informationProtection/bitlocker/recoveryKeys/$($latest.id)`?`$select=key"
            $keyRes = Invoke-GraphRetry $keyUri
            $BitLockerKeyId = $latest.id
            $BitLockerKey   = $keyRes.key
            $BitLockerDate  = ([datetime]$latest.createdDateTime).ToString("yyyy-MM-dd HH:mm:ss")
            $BitLockerStatus = "OK"
            Write-Host "   ✅ BitLocker OK" -ForegroundColor Green
            break
        }
        catch {
            continue
        }
    }
    if (-not $BitLockerStatus) {
        $BitLockerStatus = "No BitLocker"
        Write-Host "   ⚠️ No BitLocker" -ForegroundColor Yellow
    }
    # ================= OUTPUT =================
    $Report += [PSCustomObject]@{
        DeviceId                 = $DeviceId
        DeviceName               = $DeviceName
        AccountName              = $AccountName
        Password                 = $Password
        BackupDateTime           = $BackupDateTime
        BitLockerKeyId           = $BitLockerKeyId
        BitLockerRecoveryKey     = $BitLockerKey
        BitLockerCreatedDateTime = $BitLockerDate
        LAPSStatus               = $LAPSStatus
        BitLockerStatus          = $BitLockerStatus
    }
    Start-Sleep -Milliseconds 200
}
# ============ EXPORT ============
$Report | Export-Csv $CsvOutput -NoTypeInformation -Encoding UTF8
Write-Host "`n✅ DONE → $CsvOutput" -ForegroundColor Green
⛔
Nguy hiểmDelete và Wipe không thể hoàn tác. Trước khi gọi API, script phải kiểm tra lại từng máy: Intune ID còn tồn tại, serial khớp với CSV, last activity vẫn đúng như lúc xuất file. Với Backup&Delete, chỉ xóa khi đã lưu thành công khóa BitLocker và LAPS. Nên chạy chế độ dry-run và gửi danh sách cho người duyệt trước mỗi lần chạy thật.

Bước 5-6: Chặn truy cập tài nguyên công ty bằng Group

Các máy thuộc nhóm Re-enroll, Re-check, Contact User, Other vẫn đang thuộc về user, nên không xóa mà chặn truy cập cho tới khi máy Compliance trở lại.

Flow 2: thêm thiết bị vào group

  1. Run a query against a dataset lấy các dòng có Action không thuộc nhóm Delete / Backup&Delete / Wipe.
  2. Với mỗi thiết bị, gọi Graph API thêm vào group đã được assign sẵn trong Intune:
POST https://graph.microsoft.com/v1.0/groups/{group-id}/members/$ref
Content-Type: application/json

{
  "@odata.id": "https://graph.microsoft.com/v1.0/directoryObjects/{entra-device-object-id}"
}

Lưu ý: {entra-device-object-id} là Object ID của device trong Entra ID, khác với azureADDeviceId lấy từ Intune. Cần tra trước bằng GET /devices?$filter=deviceId eq '{azureADDeviceId}'.

  1. Khi máy Compliance trở lại, flow chạy lần sau sẽ không còn thấy máy đó trong bảng. Thêm một bước so sánh thành viên group với bảng hiện tại để gỡ máy ra khỏi group (DELETE /groups/{group-id}/members/{id}/$ref).

Group được dùng để làm gì

Group này được assign sẵn cho một Intune app/policy thực thi việc chặn truy cập, ví dụ gỡ chứng chỉ dùng cho Wi-Fi/VPN nội bộ, nên máy không kết nối được vào mạng công ty. Khi máy ra khỏi group, chứng chỉ được cấp lại.

# Block_Access.ps1 - Intune app assigned to Non-Compliance group

⚠️
Lưu ýChặn truy cập sẽ ảnh hưởng trực tiếp tới công việc của user. Hãy gửi email báo trước một khoảng thời gian, loại trừ các nhóm thiết bị đặc biệt (máy của lãnh đạo, máy dùng cho hệ thống quan trọng), và có quy trình mở chặn nhanh qua Helpdesk.

Bước 7: Gửi email cho user

Flow 3 gom theo Primary User, mỗi user chỉ nhận một email liệt kê các thiết bị và việc họ cần làm. Nội dung email thay đổi theo Action:

Action User cần làm
Contact User Bật máy, kết nối mạng và mở Company Portal để đồng bộ
Re-enroll Liên hệ Helpdesk để enroll lại thiết bị
Re-check Liên hệ Helpdesk để kiểm tra lại thông tin thiết bị
Other Đọc lý do Non-Compliance trong Company Portal và khắc phục
Subject: [IT Notice] Thiết bị {Device Name} không tuân thủ chính sách bảo mật

Chào anh/chị {User Name},

Thiết bị {Device Name} (Serial: {Serial Number}) đang ở trạng thái
Non-Compliance. Lý do: {Reason}.

Để tiếp tục truy cập tài nguyên công ty, vui lòng:
{Hướng dẫn theo Action}

Nếu không khắc phục trước ngày {Deadline}, thiết bị sẽ bị chặn truy cập
mạng nội bộ cho đến khi tuân thủ chính sách.

Trân trọng,
IT Team

Các lệnh kiểm tra trên thiết bị

Windows

# Ép đồng bộ với Intune
Get-ScheduledTask | Where-Object TaskName -eq 'PushLaunch' | Start-ScheduledTask

# Trạng thái join Entra ID / MDM
dsregcmd /status | Select-String 'AzureAdJoined|DomainJoined|DeviceId|MdmUrl'

# Log của Intune Management Extension
Get-Content 'C:\ProgramData\Microsoft\IntuneManagementExtension\Logs\IntuneManagementExtension.log' -Tail 50

# Serial number
(Get-CimInstance Win32_BIOS).SerialNumber

macOS

# Profile MDM đã cài
sudo profiles show -type enrollment
profiles list -all | grep -i -E "mdm|intune"

# Serial number
system_profiler SPHardwareDataType | grep Serial

Kiểm tra từ Intune bằng Graph

# Danh sách máy Non-Compliance kèm last sync
Get-MgDeviceManagementManagedDevice -Filter "complianceState eq 'noncompliant'" -All |
  Select-Object DeviceName, SerialNumber, OperatingSystem, UserPrincipalName, LastSyncDateTime

Những lỗi mình đã gặp

Lỗi Triệu chứng Nguyên nhân gốc Cách xử lý
Xếp nhầm máy đang dùng vào nhóm Wipe Máy vẫn online nhưng bị đánh dấu inactive Chỉ dùng last check-in của Intune Lấy mốc gần nhất giữa Intune và SentinelOne
LOOKUPVALUE báo lỗi Bảng DAX không load được Serial bị trùng trong Jira hoặc S1 Lọc trùng trước, hoặc dùng MAXX(FILTER(...))
Một máy rơi vào nhiều nhóm Báo cáo đếm sai Thứ tự điều kiện trong SWITCH không đúng Sắp xếp theo mức ưu tiên: Delete trước, inactive 90 trước 30
Thêm device vào group thất bại Graph trả lỗi 404 Dùng azureADDeviceId thay cho Object ID Tra Object ID bằng /devices?$filter=deviceId eq '...'
Bảng thiếu máy Jira In Stock không có user Máy không hiện trong danh sách cần xử lý Lọc ISBLANK loại luôn các dòng đó Giữ lại, gán Reason No Assigned User hoặc Other
Flow Power Automate chạy quá lâu Timeout hoặc throttling Một flow lớn xử lý tất cả Tách thành nhiều flow nhỏ theo Action, thêm delay giữa các request Graph

Kết luận

Danh sách Non-Compliance trong Intune chỉ là điểm bắt đầu. Khi gộp thêm SentinelOne, Jira Asset và AD, bạn biết được máy nào cần xóa, máy nào cần wipe, và máy nào chỉ cần nhắc user. Hãy bắt đầu bằng bảng phân loại trong Power BI và để nó chạy vài tuần ở chế độ chỉ báo cáo, trước khi bật Delete, Wipe và chặn truy cập.

Bạn đang xử lý thiết bị Non-Compliance theo cách nào: chặn bằng Conditional Access, gỡ chứng chỉ, hay dọn dẹp định kỳ bằng tay?

TN

Tam Nguyen

Senior System Administrator tại TP.HCM. Viết về Microsoft 365, Intune, Windows Server, macOS và bảo mật endpoint từ môi trường doanh nghiệp thật.

Bình luận