mordęga z MgGraph (get-eNAuditorBasicSecurityInfo, show-eNAuditorScopes)

dziś dwie malutkie funkcje, które dodałem ostatnio, żeby pomóc sobie w wykonywanym zadaniu oraz w pracy z GraphAPI. o ile działanie samych skryptów jest średnio ciekawe i robią stosunkowo niewiele, posłużą do komentarza na temat pracy z Graph.

funkcja 'get-eNAuditorBasicSecurityInfo’ miała pomóc mi zebranie informacji z ok. 3o tenantów, dotyczących tego jak są w tej chwili zabezpieczane, poprzez sprawdzenie czy jest licencja EID P1/P2 oraz czy włączone są Security Defaults. tylko tyle, ale z punktu widzenia zapytania 'aż tyle’ – ze względu na to ile dwa proste commandlety wymagają zakresów (scopes) podczas autoryzacji. ilość jest absurdalna, ale o tym za chwilę. skrypt po prostu zwraca np coś takiego:

C:\temp :))o- get-eNAuditorBasicSecurityInfo
Tenant name: W-Files
deault domain: W-Files.pl
EID Plan: AAD_PREMIUM_P2, AAD_PREMIUM
Get-MgPolicyIdentitySecurityDefaultEnforcementPolicy_Get: You cannot perform the requested operation, required scopes are missing in the token. Status: 403 (Forbidden)
ErrorCode: AccessDenied Date: 2025-05-19T17:26:18 Headers: Cache-Control : no-cache Vary
: Accept-Encoding Strict-Transport-Security : max-age=31536000 request-id :
23232323-2155-40bb-8b55-73dd81b18a15 client-request-id : 23232323-0f82-457a-b671-ce55695f552f
x-ms-ags-diagnostic : {"ServerInfo":{"DataCenter":"Canada
Central","Slice":"E","Ring":"3","ScaleUnit":"000","RoleInstance":"TO1PEPF00009BE7"}} Date :
Mon, 19 May 2025 17:26:17 GMT

Recommendation: See service error codes: https://learn.microsoft.com/graph/errors
Security defaults are DISABLED
done.

skąd ten błąd? ano dla tego, że w obecnym kontekście nie mam odpowiedniego zakresu. a skąd mam wiedzieć, przed połączeniem się do tenanta, o jakie zakresy mam prosić?

scopes

zakresy to prawdziwe piekło. ciekawą propozycję odpowiedzi można znaleźć w artykule Get started with the Microsoft Graph PowerShell SDK gdzie wykorzystuje się Find-MgGraphCommand. Każdy commadlet Graph potrafi pokazać jakie uprawnienia są mu potrzebne… teoretycznie. tutaj będę korzystał ze swojej funkcji:

show-eNAuditorScopes -FunctionName Get-MgSubscribedSku

Name IsAdmin Description FullDescription
---- ------- ----------- ---------------
Organization.Read.All False Read organization information Allows the app to read the organization and…
Organization.ReadWrite.All False Read and write organization information Allows the app to read and write the organi…
Directory.ReadWrite.All False Read and write directory data Allows the app to read and write data in yo…
Directory.Read.All False Read directory data Allows the app to read data in your organiz…

super. tylko:

  • Read jest podzbiorem ReadWrite. a więc czy mam zażądać obu czy wystarczy RW?
  • dlaczego w poleceniu Get, które nic nie zapisuje, wymagane jest RW?

po sprawdzeniu w praktyce:

C:\_ScriptZ :))o-  connect-mggraph -scopes "Organization.Read.All" -NoWelcome

C:\_ScriptZ :))o- Get-MgSubscribedSku -all

Id AccountId AccountName
-- --------- --------
23232323-4563-43cf-ad1e-232323232323_1e615a51-59db-4807-9957-232323232323 8fc43c60-4563-43cf-ad1e-232323232323 w-files
[...]

działa. czyli stawiam tymczasową tezę:

  • wcale nie jest potrzebny Write, wystarczy Read
  • nie potrzebne są wszystkie – z wymienionych wystarczył jeden zakresy

…ale to jeszcze nie koniec. w odkrywaniu od strony praktycznej jest jeszcze kilak innych utrudnień, zniekształcających obraz – cache oraz zgody (consents) wydane w przeszłości. zróbmy taki test, zaraz po wydaniu tego polecenia:

C:\temp :))o- (get-mgcontext).Scopes
AuditLog.Read.All
Directory.AccessAsUser.All
Directory.Read.All
Directory.ReadWrite.All
Domain.Read.All
email
Group.ReadWrite.All
openid
Organization.Read.All
Policy.ReadWrite.AuthenticationMethod
profile
RoleManagement.Read.Directory
User.Read.All
User.ReadWrite.All
UserAuthenticationMethod.Read.All

hmmm.. co tu się zadziało? poprosiłem o jednego Read a dostaję cały kubeł uprawnień, również ReadWrite? skąd to? wedle artykułu na Practical365 podczas zapytania, automatyczne są dodawane uprawnienia, na które daliśmy consent w przeszłości. ile jeszcze jest takich króczków i niuansów – nie wiem, ale jest to potwornie trudne w ustalaniu.

drugi sposób ustalania wymaganych scope jaki stosuję, to podejrzenie jakie są wysyłane w zapytaniu, i utrwalam to w skrypcie. dla tego funkcja 'show-eNAuditorScopes’ posiada parametr 'url’, żeby szybko dokonać ekstrakcji zakresów z zapytania URL wywoływanego podczas wykonania polecenia (copy-paste z paska URL wyszukiwarki):

show-eNAuditorScopes -URL "https://login.microsoftonline.com/common/oauth2/v2.0/authorize?scope=User.Read.All+UserAuthenticationMethod.Read.All+Directory.Read.All+Policy.Read.All+Policy.ReadWrite.ConditionalAccess+AuditLog.Read.All+Domain.Read.All+RoleManagement.Read.Directory+openid+profile+offline_access&response_type=code&client_id=23232323-204b-4c2f-b7e8-232323232323&redirect_uri=http%3A%2F%2Flocalhost%3A51987&client-request-id=32323232-54e5-4393-a5a5-323232323232&x-client-SKU=MSAL.NetCore&x-client-Ver=4.61.3.0&x-client-OS=Microsoft+Windows+10.0.26100&prompt=select_account&code_challenge=8wSisaiudhfjrL6PnMTM09kEAfe8Ae4Khywl63cwBlQ&code_challenge_method=S256&state=23232323-2aaa-4d27-912a-45a92b9fa45bbf42bcaf-7727-48e5-b528-232323232323&client_info=1"

User.Read.All
UserAuthenticationMethod.Read.All
Directory.Read.All
Policy.Read.All
Policy.ReadWrite.ConditionalAccess
AuditLog.Read.All
Domain.Read.All
RoleManagement.Read.Directory
openid
profile
offline_access

fajnie, tylko temu też nie można wierzyć chyba, że wyczyści się najpierw lokalny cache i wszystkie przeszłe zgody dla aplikacji Grpah w tenancie… i nie wiadomo czy nie ma jeszcze jakiś innych, automagicznych usprawnień – np. ciasteczka w przeglądarce. a to jeszcze nie koniec!

artykuł zacząłem od pokazania skryptu, który pokazał błąd 'Access Denied’ – wynikający właśnie z braku jakiegoś zakresu. i tutaj kolejna karuzela z zachowaniem, zależna od nieokreślonej ilości zmiennych takich jak cache, historia poprzednich działań i licho wie co jeszcze. obserwowane zachowania to:

  • skrypt wysypie się z Acc Denied
  • jeśli brakuje jakiegoś zakresu, automatycznie pokaże się okno przeglądarki pozwalające potwierdzić uprawnienie
  • w przeźroczysty sposób odświeżony zostanie token i uzupełniony o brakujące uprawnienie
  • …i jeszcze jedno zachowanie, do którego nie znalazłem dokumentacji więc nie mogę potwierdzić – ale jeśli poprosimy podczas połączenia o wiele zakresów, to będziemy mieli dwa ekrany zgód…

to ostatnie stwierdzenie to trochę strzał na ślepo. dokumentacja mówi o tym, że zapytanie może zostać podzielone na token OID i OAuth, albo że jeśli żąda się uprawnień zarówno delegowanych i aplikacyjnych – wtedy będą oddzielne żądania zgód. porobiłem trochę testów i to się nie spina –  czasem to prawda, a czasem nie. mam nadzieję udaje mi się przekonać, że ilość zmiennych jest spora i nie do końca wiadomo czego się jeszcze nie wie ze względu na ilość elementów biorących udział w procesie i to, w jaki sposób 'pomagają’.

..dodajmy (a może raczej pomnóżmy) jeszcze fakt tego jak zmienna jest Chmura – jutro może wyjść nowa wersja MgGraph i wszystko będzie inaczej.

jak obejść problem?

w dużym skrócie można stwierdzić, że temat jest nie-do-ogarnięcia z poziomu skryptów. trzeba się pogodzić z tym, że czasem będzie kilka żądań (consent request). albo skorzystać z Service Principal.

wykorzystanie aplikacji omija wszystkie wymienione problemy:

  • wszelkie niezbędne zgody wydane są 'z góry’, przed użyciem skryptu
  • potem można łączyć się np. za pomocą Secret

przykładem jest moduł PnP.PowerShell, którego twórcy postanowili przestać walczyć z wiatrakami i uprościć sobie życie, wymagając zarejestrowanej aplikacji.

czemu zatem ja tak nie robię w skryptach? ano ze względu na pewien paradygmat tego do jakiego scenariusza skrypt jest robiony. konkretnie podstawowa różnica pomiędzy 'administracją’ a 'pojedyncze zadanie’. jeśli tenantem zarządzamy (administrujemy) i potrzebujemy uruchomić skrypt regularnie – metoda z aplikacją jest jedyną sensowną. gorzej, jeśli logujemy się po wielu tentach (że zacytuję siebie samego: „zebranie informacji z ok. 3o tenantów„) – wtedy jest to dodatkowe przygotowanie a potem posprzątanie, co mocno redukuje ideę 'skryptu’ jako szybkiego wsparcia.

na zakończenie

słowo komentarza na temat zakresów – bo widać wyraźnie dwa typy zakresów – jedne określane są krótkim słowem (np. email), inne są złożone (np. Something.ReadWrite.All). to właśnie złożenie dwóch tokenów – OID, który zawiera informacje

  • openid, profile oraz email to elementy tekenu OID
  • offline_access choć również jest zakresem OID, występuje niejawnie – jeśli zażądasz go, to dostaniesz błąd. dodawany jest automatycznie.

This permission currently appears on all consent pages, even for flows that don’t provide a refresh token (such as the implicit flow). This setup addresses scenarios where a client can begin within the implicit flow and then move to the code flow where a refresh token is expected.

Scopes and permissions in the Microsoft identity platform

do samej pracy/zabawy z tokenami można skorzystać z modułu MSAL.PS, co pozwala trochę lepiej kontrolować proces.

…kiedyś życie skryptera było prostsze /:

eN.

get-eNAuditorMFAReport : raport MFA

być może przygotowujesz się do przejścia na nowy model MFA i chcesz upewnić się, czy konta są odpowiednio skonfigurowane. a może po prostu chcesz zweryfikować stan MFA w ramach tenanta?

(między innymi) tego rodzaju raporty muszę dostarczać często, dla tego potrzebuję dobrego narzędzia. takim jest funkcja get-eNAuditorMFAReport z modułu eNAuditor. postaram się opisać kilka funkcji wraz ciekawostkami… bo dawno nie było o PowerShell.

instalacja to po prostu wywołanie „install-module enauditor”. funkcja pozwala na sprawdzenie stanu MFA dla pojedynczego konta lub (default) dla całego tenanta. można też od razu wygenerować plik Excel (-xlsxReport):

C:\temp :))o- get-eNAuditorMFAReport -userId catest@w-files.pl -extendedMFAInformation -xlsxReport
VERBOSE: connected as nexor@w-files.pl.
VERBOSE: 1 member users found. gathering MFA status...

MFAstatus : enabled
softwareAuth : False
authApp : True
authDevice : [SM-S921W]
phoneAuth : False
authPhoneNr :
fido : False
fidoDetails :
helloForBusiness : False
helloForBusinessDetails :
emailAuth : False
SSPREmail :
tempPass : False
tempPassDetails :
passwordLess : False
passwordLessDetails :
UserDisplayName : Test
UserPrincipalName : catest@w-files.pl
Id : 23232323-a33b-45a5-8484-232323232323
AccountEnabled : True
IsAdmin : False
IsMfaCapable : True
IsMfaRegistered : True
IsPasswordlessCapable : True
IsSsprCapable : False
IsSsprEnabled : False
IsSsprRegistered : False
LastUpdatedDateTime : 2025-05-05 8:23:06 PM
MethodsRegistered : macOsSecureEnclaveKey,microsoftAuthenticatorPush,softwareOneTimePasscod
e
IsSystemPreferredAuthenticationMethodEnabled : True
SystemPreferredAuthenticationMethods : PhoneAppNotification
UserPreferredMethodForSecondaryAuthentication : push
AdditionalProperties : 0

VERBOSE: results saved as eNMFAReport-w-files.pl-250514-203901.csv.
08:39:04> INFO: creating C:\temp\eNMFAReport-w-files.pl-250514-203901.xlsx excel file...
08:39:04> INFO: adding eNMFAReport-w-files.pl-250514-203901.csv data as worksheet...
08:39:04> INFO: ',' detected as delimiter.
08:39:05> convertion done, saved as C:\temp\eNMFAReport-w-files.pl.250514-203901.xlsx
08:39:05> OK: done and cleared.
done.

userId może być GUIDem lub UPNem natomiast do konwersji CSV to XLSX wykorzystywana jest funkcja convert-CSVtoXLS z biblioteki eNLib.

extendedMFAInformation

ciekawosta związana z MFA jest taka, że są dwie funkcje Graph, które pozwalają na odpytanie się o MFA:

pierwsza jest bardziej uniwersalna i łatwiej znaleźć przykłady Internecie, jednak ta druga dostarcza bardzo fajnych, szczegółowych informacji… ale. musi być jakieś 'ale’. choć nie jest to opisane w dokumentacji, Get-MgReportAuthenticationMethodUserRegistrationDetail wymaga:

  • aby konto było aktywne – nie zadziała na wyłączonym koncie
  • aby miało licencję. jestem jeszcze w trakcie sprawdzania czy jakąkolwiek licencję czy EID P1/P2.
  • na pewno nie zadziała, jeśli nie ma P1 w ramach tenanta

tak, że parametru 'extendedMFAInformation’ można użyć, ale niekoniecznie uda się uzyskać dodatkowe informacje.

outro

na zakończenie dodam, że moduł jest na GitHub jeśli ktoś chce sobie dokładnie przejrzeć działanie.

w ramach modułu jest na tą chwilę kilka innych funkcji, które też będę po krótce opisywał. na dziś są to:

  • disable-eNAuditorperUserMFA
  • get-eNAuditorADPrivilegedUsers
  • get-eNAuditorBasicSecurityInfo
  • get-eNAuditorEntraIDPrivilegedUsers
  • get-eNAuditorMFAReport
  • get-eNAuditorReportADObjects
  • get-eNAuditorReportEntraUsers
  • get-eNAuditorReportMailboxes
  • join-eNAuditorReportHybridUsersInfo

jak większość tego rodzaju projektów – moduł rośnie w trakcie kiedy mam jakiś projekt z tym związany. w poczekali leży funkcja to weryfikacji uprawnień aplikacji EID, ale nie jest skończona. przy najbliższym audycie na pewno ją dodam.

ze względu na funkcję get-eNAuditorADPrivilegedUsers musiałem ustawić kompatybilność modułu na PS5, chociaż to jedyna funkcja dla tej wersji, jednak wszystkie funkcje dla Chmury wymagają PS7. niestety dodanie #requires dla chociaż jednej funkcji powoduje, że podczas ładowania modułu w ramach PS5 moduł się wysypie – a funckję dla AD przyodziło mi odpalać niedawno na w2k12 (bez R2 nawet!).

taki urok modułów, taki urok środowisk on-premises.

eN.

LockedShields 2o25

w tym roku po raz kolejny stanęliśmy na podium LockedShields, organizowanego przez CCDCOE. POL-FRA drużyna stanęła na wysokości zadania, minimalnie ustępując drużynie z Niemiec.

to ważne wydarzenie dla mnie co roku – zupełnie inne spojrzenie na bezpieczeństwo i pracę w stanie wysokiego stresu. przez 3 dni miałem dostarczane jedzenie na biurko, a wypad na siku raportowany resztom osób, bo przez 2 min nie jestem w stanie wspierać reszty teamu. świetny trening, który przypomina mi, jak bardzo różni się dzień powszedni od sytuacji kryzysowej. trzeba wykrzesać z siebie więcej, pracować inaczej, a 'książkowe zasady’, które stosuje się na co dzień do zabezpieczania systemów nagle stają się farsą i trzeba rzeczy adresować zupełnie inaczej.

pomimo wysokiej lokaty ten rok wyjątkowo oberwaliśmy [jako team AD] – szacun dla Red Teamu, tym razem nas wyprzedzili. ale to dobrze – chodzi o to, żeby się rozwijać, nie spocząć na laurach i co rok odtwarzać te same procedury. co z takich ćwiczeń się wynosi to tzw 'drill’:

  • praca w nietypowych warunkach wymagająca podejmowania natychmiastowych decyzji
  • hierarchia i wykonywanie rozkazów – wbrew pozorom, bardzo trudne dla cywilnych IT. mamy swoją dumę, wielu z nas w wojsku nie było i przyzwyczajeni do brylowania w środowiskach, w których pracujemy, wcale nie jest łatwo utrzymać hierarchię, słuchać rozkazów. starcie charakterów, umiejętność 'zamknięcia się’ i po prostu zrobienia czegoś, nawet jak się z tym nie zgadzamy czy przekazane jest zbyt dosadnie… komunikacja i zarządzanie kryzysowe 'w praktyce’ to co innego niż narysowanie drzewka na papierze.
  • przestrzeganie procedur ale i umiejętność improwizacji, kiedy zawodzą

do tego oczywiście duża dawka wiedzy od reszty zespołów, co też ma swoje znaczenie, jednak IMHO 'drill’ w takich sytuacjach jest najcenniejszym doświadczeniem.

wiele food for though na przyszły rok po otrzymaniu wyników – mam szczerą nadzieję, iż będę miał zaszczyt i okazję poprawić ochronę naszej części środowiska i udowodnić, że się rozwijamy – technicznie i i w zakresie zarządzania kryzysowego.

dziękuję gorąco całemu zespołowi LS25 i wielkie kudos dla organizatorów, bo impreza jest na na prawdę wysokim poziomie!

PS. dla wszystkich, którzy chcieliby wesprzeć kraj jako cywile – Dowództwo Komponentu Wojsk Obrony Cyberprzestrzeni uruchomiło program CyberLEGION.

eN.