disable-eNAuditorperUserMFA – jak pozbyć się per-user MFA

„per-user MFA” czyli najstarsza z metod MFA, jest coraz bardziej odstawiana na bok. nie ma póki co mowy o tym, żeby była zupełnie wyłączona, ale od 3o Października na pewno będzie jeszcze mniej przydatna, ponieważ pierwotne polisy SSPR oraz MFA zostają wycofane.

po wykonaniu migracji per-user MFA jest oczywiście cały czas dostępny, jednak metody MFA zarządzane są już (w końcu!) z jednego miejsca:

z SSPR nadal jest pewien babol, związany z hasłami i mailem… ale to oddzielny temat.

czy należy się bać migracji do nowego MFA?

raczej nie. jeśli po prostu zaakceptuje się kolejne ekrany procesu, włączone zostaną wszystkie metody MFA, cały proces pozostanie przeźroczysty dla użytkownika końcowego.

ALE…

jest to IMHO świetny moment, aby zrobić trochę więcej niż tylko przeklikać się przez kilka stron wizarda na standardowych ustawieniach. warto faktycznie opracować polityki metod MFA, pozbyć się wszystkich per-user MFA oraz wymusić rejestrację. „kampania rejestracji” jest mniej znaną funkcją, z obserwacji większość firm zostawia te polityki nieskonfigurowane, a to ma ciekawe zastosowanie. i konsekwencje.

wyłączanie per-user MFA

jednym z dość częstych błędów, które widzę, to mieszanie per-user MFA z Conditional Access, wynikający głównie z braku zrozumienia że są różne metody MFA (do niedawna było ich więcej, pow0li zaczyna się unifikować). oczywiście aby wyłączyć per-user MFA zupełnie, należy w pierwszej kolejności upewnić się, że mamy odpowiedni poziom licencjonowania. choć wystarczy pojedyncza licencja aby móc zacząć korzystać z Conditional Access, każde konto powinno mieć licencję. jeśli ktoś korzysta z Security Defaults, to tworzona polityka CA obejmuje wyłącznie konta posiadające odpowiednią licencję, a w takim przypadku model mieszany jest niezbędny.

w dużej ogólności należy pamiętać, iż

per-user MFA ma priorytet i nawet dla osób z licencją i objętych Conditional Access, jeśli skonfigurowane jest per-user MFA, będzie wygrywało

co prowadzi do dziwnych zachowań, może wydawać się, że polityki nie działają.

w module eNAuditor jest funkcja, która pozwala wyłączyć per-user MFA dla pojedynczego obiektu lub dla całego tenanta – wygodne przy przejściu na model CA, np. po zakupie większej ilości licencji:

#disable for a single user with <userID>
disable-eNAuditorperUserMFA -userId <userID> -forceReconnect

#disable for a whole tenant
disable-eNAuditorperUserMFA

flaga 'forceReconnect’ przydaje się do wyczyszczenia kontekstu lub uzupełnienia go o odpowiednie uprawenienia.

kampania rejestracji

fajna funkcja pozwalająca na uzupełnienie CA. możemy pozwolić użytkownikom na korzystanie z różnych metod – np. aplikacja oraz SMS, ale chcemy aby każdy miał aplikację skonfgurowaną obowiązkowo oraz jako podstawową metodę. wtedy możemy uruchomić kampanię rejestracyjną oraz wymuszenie:

standardowe ustawienie to „Microsoft Managed” czyli „nie znasz dnia ani godziny”… osobiście nie lubię takich ustawień. włączenie kampanii wymusi wizarda MFA na wszystkich użytkownikach – nawet tych, którzy mają odpowiednią metodę skonfigurowaną. po prostu będą musieli przejść „pustego” wizarda.

należy jednak przy tym ustawieniu uważać, ponieważ może doprowadzić do pętli na kontach, które wymagają nietypowego ustawienia. np: mamy konta administracyjne czy breakglass, konta usług etc – takie, które są do użytku kilku osób, a zatem mają nie aplikację, tylko kod z password managera (konto breakglass nie powinno mieć MFA w ogóle, dłuuuuugie, złożone hasło). w takim przypadku można wpaść w pętlę. long story short – nie zapomnij zrobić odpowiednich wyjątków od rejestracji, żeby nie zrobić sobie krzywdy.

ogólnie jest to ustawienie bardzo przydatne i pozwala na wymuszenie minimum, ale bądź ostrożny! przypomnę jeszcze, że jeśli korzystasz z EAM, to masz problem.

eN.

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.

raportowanie kont hybrydowych

Raportowanie kont (hybrydowych)

raporty dotyczące kont i skrzynek są niezbędne podczas wielu różnych sytuacji – czy to codzienna administracja, porządki czy przygotowania do migracji. problem polega na tym, że sposób w jaki prezentowane są dane przez portale jest nieznośny – informacje rozbite pomiędzy usługami, ograniczone filtry i widoki, niektóre informacje ukryte gdzieś i niedostępne przez GUI. dla tego skrypty raportujące pojawiały się tutaj nie raz i teraz pracuję nad kolejnym 'mini-projektem’ który ma proces usprawnić. ponieważ dojrzał już do jakiejś sensownie działającej wersji, trochę informacji i ciekawostek.

scenariuszy jest zaiste wiele:

  • aktywność i nieużywane konta
  • informacje na temat licencji użytkowników
  • status MFA
  • informacje o skrzykach exchange – zajętość, status, aktywność
  • przygotowanie do synchronizacji
  • wszelkiego rodzaju migracje – pomiędzy tenantami, onprem-to-Cloud etc.

o ile zadanie dotyczy pojedynczego systemu, można sobie poradzić standardowymi narzędziami… zazwyczaj. ale kiedy trzeba popatrzeć holistycznie na 'konto’ jako całość – zaczyna się robić trudniej.

Skrypty raportujące

same skrypty dostępne są na github ale warto wiedzieć jaka jest ogólna logika.

najwygodniejszym formatem do obróbki danych jest CSV z dwóch powodów.. czy też raczej sumy dwóch powodów – PowerShell świetnie sobie z tym formatem radzi oraz to, że Microsoft Excel jest najbardziej rozpowszechnionym narzędziem do prezentowania danych tabelarycznych – a w końcu o to nam chodzi, żeby zbudować zestawienie danych, czyli tablę. dość złożoną, ale nadal tylko tabelę.

o ile przygotowanie eksportu z poszczególnych systemów jest stosunkowo proste, o tyle największym problemem jest ich połączenie. to, czego PowerShell nie posiada, to zmienna typu 'tabela’. oh, nie chodzi o zwykłą tablicę, hashtable czy podobną zmienną, chodzi mi bardziej o coś bliższego tabeli w rozumieniu bazy danych – tak, żeby można było wyszukać informację/rekord, zrobić aktualizację, powiązać ze sobą rekordy. pisałem już kilkukrotnie różne synchronizatory i to, ile problemów jest z takim systemem wie tylko ten, kto kiedyś spróbował coś takiego napisać – wyjątki, duplikaty, łączenie rekordów… o ile połączenie dwóch źródeł jest trywialne – przy trzech i więcej zaczyna się prawdziwe wyzwanie. dla tego wzorem dla mnie była lekcja z MIIS (Microsoft Identity Integration Server) na którym zbudowany został AD Sync. nie żeby moje skrypty były choćby blisko, ale zrozumienie metaverse (tego z MIIS, nie w tym dzisiejszym rozumieniu) jest kluczowe do napisania sensownej synchronizacji.

logika skryptów jest w założeniu prosta:

  • zrobić zrzut danych z EntraID do csv
  • zrobić zrzut danych z Active Directory do csv
  • zrobić zrzut danych z Exchange Online do csv
  • połączyć dane i skonwertować do xlsx gdzie już ręcznie można zacząć bawić się wynikami

jak już wspomniałem, to ten ostatni krok jest największym wyzwaniem – żeby pisać kod tak, aby mógł potem być użyty przy innych podobnych zadaniach, gdzie jest potrzeba połączenia danych z kilku źródeł. stąd w skrypcie join-eNReportHybridUsersInfo pojawiają się trzy funkcje:

  • search-MetaverseData
  • add-MetaverseData
  • update-MetaverseData

kiedy mamy już zrzut z poszczególnych systemów w postaci CSV (nie muszą być wszystkie trzy, wystarczą dowolne dwa – EID/EXO/AD), 'join’ sprawdza czy da się dane jakoś powiązać.

ostatnio mam sporo sytuacji gdzie nie ma wdrożonej synchronizacji (Cloud Sync) albo jeszcze gorzej – jest częściowa. środowiska w których część kont jest synchronizowana, część z jakiegoś powodu nie, ale mają … lub nie mają swojego bliźniaka w EID. niestety ale z mojego doświadczenia to bałagan jest normą a porządek wyjątkiem – więc znalezienie par nie jest trywialne i często wymaga poszukiwania na podstawie różnych atrybutów – czasem to, co jest w stanie powiązać dane to UPN, email, a czasem displayname. nie można liczyć na żaden konkretny atrybut? skrypt pozwala spróbować powiązać na dowolnym istniejącym. i chociaż funkcja nadal jest dość prymitywna to działa świetnie i pomaga mi szybko odpowiedzieć na bardzo wiele pytań… :

  • wyłapać konta, które dawno powinny być zablokowane
  • dziwne skrzynki pocztowe o których nawet diabeł zapomniał
  • konta bez skonfigurowanego MFA, żeby nie zabić firmy po konfiguracji Conditional Access
  • wszystkie panie, które pomiędzy kontem AD a EID zdążyły zmienić nazwiska
  • … i masę innych dziwnych sytuacji, literówek i wszelakiego innego bałaganu, z którego da się wyłowić obraz, jeśli się na niego spojrzy z odpowiedniej perspektywy

co do samych operacji na CSV, funkcje do ich obsługi to najczęściej wykorzystywane funkcje z biblioteki eNLib:

load-CSV

  • automatyczne wykrywanie znaku – ratuje jak się pracuje w środowiskach z różnymi ustawieniami regionalnymi
  • możliwość wymuszenia nagłówka w skrypcie, żeby nie zassać jakiegoś przypadkowego śmietnika
  • możliwość dodawania prefiksów, suffiksów czy podania tabeli transformacji nazw kolumn podczas importu

convert-CSV2XLS

  • konwersja do xlsx
  • utworzenie tabeli
  • automatyczne uruchomienie

..i kilka innych przydatnych zabiegów, które czynią pracę z csv prostą i przyjemną.

eN.

 

 

 

KB5034441 0x80070643 fix

Microsoft zapowiedział, że nie wyda poprawki do poprawki KB5034441, która w większości wypadków wysypuje się błędem 0x80070643. w takim razie pół-automatyczne rozwiązanie do zassania tutaj, a poniżej informacje o co chodzi i jak to działa.

czemu to się sypie

niby opis naprawy można znaleźć na stronie Microsoft. ale po pierwsze, nie każdy jest na tyle odważny, żeby grzebać na żywych partycjach. po drugie w całym tym opisie brakuje bardzo ważnej informacji: potrzebny jest plik w winRE. teoretycznie wykonanie komendy 'reagentc /disable’ powinno skopiować plik winRE, ale tego nie robi. a więc po zmianie wielkości partycji, próbujemy włączyć… i nie działa.

w związku z tym ogólne kroki do wykonania to:

  1. przygotowanie pliku winRM.wim i umieszczenie go w katalogu 'recovery’.
  2. wyłaczenie winRM
  3. znalezienie partycji recovery
  4. zmniejszenie partycji systemowej, żeby zrobić miejsce na winRM
  5. zwiększenie partycji recovery
  6. właczenie winRM

Skrypt

na github

skrypt wymaga uruchomienia jako administrator. co robi:

  • pobiera plik winRM.wim do katalogu 'recovery’
  • próbuje znaleźć partycję recovery i jak mu się uda:
  • przygotowuje plik wsadowy dla diskpart
  • wypluwa instrukcje do robić dalej

jeśli skrypt się nie wysypał – to prawie na pewno wszystko jest ok. jeśli się wysypał przed końcem – cóż, może masz pecha i Twoja konfiguracja jest niestandardowa.

nie działa w 1oo% oczywiście – dla tego nie jest w pełni automatyczny. są różne dziwne scenariusze, ale w większości, na komputerach firmowych sytuacja jest dość prosta. skrypt odpaliłem na wielu serwerach i stacjach roboczych i skuteczność jest na poziomie 95%, oszczędzając mojemu zespołowi godzin pracy i dziargania w diskpart.

Przypadki, które sprawiały problemy to:

  • kilka przypadków z bitlocker, dziwnym układem partycji
  • trudne do wyjaśnienia przypadki w których winRE nie było włączone a patch i tak chciał się instalować. partycja recovery była w 'dziwnym’ stanie [chyba ktoś przy niej wcześniej grzebał (; ]
  • w kilku przypadkach, kiedy zabrało miejsca na dysku systemowym, chociaż było go dużo – pomogła defragmentacja.

w sumie nie dziwię się, że Microsoft nie wydaje automatu, bo nie chce potem odpowiadać za utracone dane. i ja również dodam: USE ON YOUR OWN RISK.

powodzenia

eN.

nieaktywni użytkownicy EntraID

w AD sprawa była prosta… no może nie tak bardzo prosta, bo historia atrybutów lastLogon i lastLogonTimeStamp też ma swoje drugie dno, ale od wielu lat wiadomo jak aktywność użytkownika zbadać… i pojawiła się Chmura i hybryda, która całość skomplikowała…

przez wiele lat nie było prostego sposobu na zapytanie 'wyszukaj nieużywane konta’, ponieważ nie było w EntraID odpowiednika lastLogonTimeStamp i trzeba było każdy system badać na dziwne sposoby. wedle Microsoft, należało odpytać logu audytu, który miał standardowo 3o dni.. wielce przydatne. nie drążąc dalej historii, od kwietnia 2o2o GraphAPI produkcyjnie obsługuje ’signInActivity’ – w końcu atrybut/zasób, który jest automatycznie aktualizowany ostatnią datą uwierzytelnienia.

uwaga. atrybut może mieć do 6h opóźnienia w aktualizacji

UWAGA – to nie jest atrybut 'ostatniego UDANEGO uwierzytelnienia’ a 'ostatniej PRÓBY uwierzytelnienia

jak odpytać

kilka przykładów jak przygotować sobie raport, korzystając z commandletów Microsoft.Graph

wyświetlenie wszystkich użytkowników, wraz z ich mailem oraz datą ostatniej aktywności:

Get-MgUser -Property displayname,mail,signInActivity -all|
    select displayname,mail,@{L='signin';E={$_.SignInActivity.LastSignInDateTime}}

korzystanie z commandletów graphowych nie jest przyjemne i wymaga cierpliwości. większość operacji, ze względu na optymalizację, nie wyświetla prawie żadnych atrybutów – dla tego trzeba wyraźnie zaznaczyć w 'property’ czego będzie się potrzebowało.

trochę bardziej złożony raport, wyrzucany na csv, zawierający dodatkowo informacje o licencjach:

Get-MgUser -Property id,displayname,givenname,surname,accountenabled,userprincipalname,mail,signInActivity,userType -all|
    select id,displayname,givenname,surname,accountenabled,userprincipalname,userType,mail,`
    @{L='signin';E={$_.SignInActivity.LastSignInDateTime}},`
    @{L='licenses';E={(Get-MgUserLicenseDetail -UserId $_.id).SkuPartNumber -join ','}}|
        export-csv -nti c:\temp\EntraUsers.csv -Encoding utf8

co mnie denerwuje w tych commandletach, to że nie są przyjmują w prosty sposób obiektów, np to nie zadziała:

get-mgUser -all|get-mgUserLicenseDetail

to było abecadło PS, ale wzięli się za to developerzy webowi i taki efekt… trzeba też zawsze pamiętać o dodaniu 'all’ bo standardowo jest paging na 1oo obiektów.

i jeszcze drobny przykład na użycie filtra. nie jest co prawda widoczne w helpie od get-mgUser ale to jest filtr OData – z którego ogólnie korzysta cały Graph. tutaj prosty filtr, żeby wyświetlić tylko konta typu guest. nie podjąłem się filtrowania po dacie w OData bo to by kosztowało mnie za dużo czasu na rozkminkę – operacje na datach nie są oczywiste, a PowerShell sobie świetnie z tym radzi:

Get-MgUser -Filter "userType eq 'guest'" -Property displayname,usertype,signinactivity|
    ?{$_.SignInActivity.LastSignInDateTime -lt (get-date).AddMonths(-2)}|
    select displayname,usertype,@{L='activity';E={($_|select -ExpandProperty SignInActivity).LastSignInDateTime}}

connect

oczywiście żeby korzystać z commandletów graph trzeba się połaczyć z odpowiednim zakresem (scope). jeśli to jest dla ciebie nadal problemem to koniecznie wrzuć do bookmarków ten link, który opisuje jak użyć Find-MgGraphCommand żeby sprawdzić niezbędne zakresy.

jak Microsoft potrafi strzelić klientom w kolano

ciekawostką jest, że żeby być w stanie odczytać ten atrybut, trzeba mieć minimum EntraID P1 – inaczej wyskoczy błąd:

Get-MgUser_List: Neither tenant is B2C or tenant doesn't have premium license

Status: 403 (Forbidden)
ErrorCode: Authentication_RequestFromNonPremiumTenantOrB2CTenant

…dlaczego? nie chodzi mi o techniczne wyjaśnienie, ale to krzyk w stronę MS – dla czego tak podstawowa funkcjonalność, jak wyszukiwanie nieaktywnych kont, wymaga dodatkowej licencji?

eN.

wyszukiwanie pustych subksrypcji

Scenariusz

porządek najlepiej zaczynać od redukcji. niby trywializm, a jednak często widzę, że osoby próbują najpierw 'porządkować’ a zapominają o tym pierwszym kroq – a przecież łatwiej jest uporządkować np. 1oo elementów, niż 2oo. w przypadq subskrypcji nie jest inaczej – wiele pozostaje bez właściciela (co było poprzednio) ale też wiele pozostaje pustych. w ramach pojedynczego tenanta subskrypcje pochodzą z różnych offeringów i największy bałagan jest z developerskimi wynalazkami z MSDN, MSDN Dev/Test, Trial czy Visual Studio, które mogą sobie powoływać na żądanie.

aby wyłączyć tą możliwość należy założyć ticket do wsparcia. nie ma samodzielnej metody kontroli. jedyne, co można zrobić samemu, to zdefiniować domyślną Management Group do której (wszystkie) subskrypcje będą wpadać. dzięki temu 'nasze’, z głównej umowy typu Enterprise czy MCA zakładamy tam, gdzie mają być, a pozostałe, z niewiadomych źródeł, można zautomatyzować – czy to polisami czy jakimś triggerem.

Czysty PowerShell

napisanie prostego skrypciq w PS, który przeliczy ilość zasobów i ładnie wyświetli listę, nie jest skomplikowane:

$subscriptions = Get-AzSubscription | ? state -ne 'disabled'
$counts = @()
foreach($sub in $subscriptions){
    write-host "processing $($sub.name)..." -ForegroundColor grey
    try {
        Set-AzContext -SubscriptionObject $sub | Out-Null
    } catch {
        write-host "error accessing $($sub.Name)" -ForegroundColor Red
        $counts += [pscustomobject]@{
            subscriptionName = $sub.Name
            subscriptionId = $sub.Id
            resourceCount = -1
        }
        continue
    }
    $res = Get-AzResource
    $counts += [pscustomobject]@{
            subscriptionName = $sub.Name
            subscriptionId = $sub.Id
            resourceCount = $res.count
        }
}
$counts | sort resourceCount | export-csv -nti c:\temp\ResourcePerSubscription.csv -Encoding utf8
&(convert-CSV2XLS C:\temp\ResourcePerSubscription.csv)

nie lubię one-linerów, lubię wiedzieć co się dzieje, i mieć kontrolę – dla tego skrypt może wydawać się niepotrzebnie rozbudowany… ale mi zajmuje ok 1o min żeby taki napisać, wszystko widzę, jak coś wywali to wiem gdzie, mogę to debugować – i koniec-końców czas zainwestowany w trochę więcej więcej kodu i jakąś podstawową obsługę błędu – zwraca mi się dość szybko. zwłaszcza, że pojedynczy przebieg takiego kodu może trwać całkiem sporo – w moim przypadq to ok. 10 minut….

Przyspieszamy

… i właśnie ten czas jest denerwujący. oczywiście – niby takie rzeczy robi się bardzo rzadko, więc optymalizacja jest trochę sztuką-dla-sztuki. dla mnie jest sztuką-dla-nauki. języki operujące na zbiorach danych – jak SQL czy KQL – nie są dla mnie codziennością. warto się więc pogimnastykować, bo to z kolei zwróci się w przyszłości. tak było w przypadq samego PS – przedstawiony wcześniej skrypt, kilka lat temu, pewnie zająłby mi kilka razy więcej czasu. dziś z automatu wiem gdzie wrzucić obsługę błędu, a gdzie to jest strata czasu, kiedy warto zainwestować więcej, a kiedy walnąć one-liner’a.

spróbujmy więc uzyskać podobny efekt korzystając z KQL. największym problem dla myślących proceduralnie czy sekwencyjnie, jest przestawienie głowy na pracę na zbiorach. dobrze jest sobie zrobić powtórkę z matematyki, w ramach gimnastyki zwojów.

na początq szkielet, którego używam jako starter, i zmieniam sobie tylko kwerkę:

$searchParams=@{
    first = 1000
    query = $QUERY
}    

$allResults = @()
do {
    $results = Search-AzGraph @searchParams
    $results|%{$allResults+=$_}
    $searchParams.ContainsKey('skip') ? ($searchParams.skip+=1000) : $searchParams.add('skip',1000)
} while($results.skipToken)
$allResults
tak rozbudowany zapewnia, że jeśli przekroczy się limit 1.ooo odpowiedzi, zostanie ładnie obsłużone. warto sobie jakiś-taki szablon trzymać. teraz będę rozbudowywał swoje $QUERY, żeby uzyskać pożądany efekt.

#1

najpierw prosty test:

$QUERY = "resources
    | summarize nrOfResources=count() by subscriptionId"

ten przykład zwraca ładnie wyniki – ilość subskrypcji wraz z ilością zasobów. to czego jednak NIE zwraca, to nazwy subskrypcji – a nam, humanoidom, jednak łatwiej posługiwać się literkami niż GUIDami. po drugie nie ma subskrypcji pustych. wynika to z faktu, że zapytanie jest do 'zbiór zasobów’ a potem grupuje je po atrybucie 'subscriptionId’. więc jeśli subskrypcja nie ma zasobów to nie ma zasobów do zaraportowania subskrypcji…

#2

ponieważ wynik ma być częścią wspólną zbioru subskrypcji oraz zbioru zasobów, najpierw napiszmy obie strony zapytania…

jako ciekawostka – jest pewna niespójność  dotycząca zasobu/resource. chodzi o subskrypcje i ResourceGroupy, które zasadniczo, z definicji, nie są zasobami, a kontenerami zasobów. przez to mają swoją oddzielną domenę nazewniczą 'resourceContainers’. czemu niespójność? bo przy niektórych operacjach i commandletach traktuje się je jako zasoby. spojrzyj na samo zapytanie: ResourceContainers z microsoft.resources/subscriptions ?

…a że prawą stronę praktycznie napisaliśmy wcześniej, bo to po prostu zapytanie o zasoby, zajmijmy się lewą stroną:

$QUERY = "resourceContainers
    | where type =~ 'microsoft.resources/subscriptions'
    | project subscriptionId, subName=name"
w zbiorze subskrypcji, elementy mają już pełne informacje, czyli np. atrybut 'name’ – w ten sposób można wyciągnąć nazwę subskrypcji.
porównanie niewrażliwe na wielkość znaków '=~’ jest bezpieczniejszym sposobem niż '==’. należy na to bardzo uważać, ponieważ KQL jest wrażliwy na wielkość znaków w atrybutach czy nazwach, zapisywanych camelBack’iem a inne narzędzia pokazują je CamelCapsem. jeśli więc nie dostajesz wyników, zacznij od sprawdzenia wielkości znaqw, albo użyj [bardziej kosztownego] '=~’ .
mamy więc oba potrzebne zbiory – wszystkie subskrypcje oraz wszystkie zasoby. trzeba teraz wyciągnąć odpowiednio skonstruowaną część wspólną…

#3

do różnych operacji łączenia zbiorów służy, co dość oczywiste, 'join’. problem w tym, że jest wiele różnych joinów, a przypadq Kusto jest ich wyjątkowo dużo.

KQL – to Kusto Query Language. czasem łatwiej coś wyszukać korzystając z tego słowa kluczowego… przynajmniej póki search nie jest podpięty pod GPT (;

co więcej, domyślnym operatorem, jest [nomen-omen] unikalny dla KQL innerunique. to może być dość istotna różnica, więc może warto dodawać zawsze odmianę? tu na leniucha:

$QUERY = "resourceContainers
    | where type =~ 'microsoft.resources/subscriptions'
    | project subscriptionId, subName=name
    | join resources on subscriptionId
    | summarize nrOfResources=count() by subscriptionId,subName
    | sort by nrOfResources asc"
jeśli odrobiłe(a)ś lekcję ze zbiorów, to powinno być łatwe: wyszuqjemy wszystkie subskrypcje i wyciągamy z niego ID i nazwę [project] następnie bierzemy wszystkie zasoby i wyciągamy część wspólną, korzystając z kolumny subscriptionId. następnie liczymy ilość zasobów [summarize] i sortujemy po ich ilości wzrastająco. brzmi jak bajka…
ale niestety nie zwraca pustych rekordów. znów wiąże się z operacjami na pustych zbiorach i wynika z prostej matematyki 'A ∩  B’. a jak się kończy mnożenie przez zero, to chyba nie trzeba mówić. jeśli nie wierzyła(e)ś, że powtórka z matematyki się przyda, to chyba powoli przestajesz mieć wątpliwości?

#4

pobawmy się zatem typem [flavor] joina:
$QUERY = "resourceContainers
    | where type =~ 'microsoft.resources/subscriptions'
    | project subscriptionId, subName=name
    | join kind=leftouter resources on subscriptionId
    | summarize nrOfResources=count() by subscriptionId,subName
    | sort by nrOfResources asc"
smaczek 'leftouter’, jak wynika z definicji, oznacza:
Returns all the records from the left side and only matching records from the right side.
co powinno dobrze zadziałać, bo po lewej mamy WSZYSTKIE subskrypcje , nawet te które nie mają części wspólnej z zasobami…
..no i generalnie to działa. ale jest glitch. mianowicie puste subskrypcje pokazują '1′ zamiast '0′.  a, że lubię drążyć… drążę dalej…

#5

oto finalne rozwiązanie:

$QUERY =  "resourceContainers
    | where type =~ 'microsoft.resources/subscriptions'
    | project subscriptionId, subName=name
    | join kind=leftouter
    (resources
        | summarize nrOfResources = count() by subscriptionId
    ) on subscriptionId
    | extend nrOfResources = coalesce(nrOfResources, 0)
    | sort by nrOfResources asc"
magiczną funkcją tutaj jest 'coalesce’, która jest ekwiwalentem 'isNullOrEmpty’, oraz zamiana sekwencji wyliczania sumy – przed wykonaniem join a nie po nim. czy to rozwiązuje wszystkie problemy? no nie. nie ma np. ResourceGroup – jeśli będą Subscription bez zasobów, ale zawierające ResourceGroup – wynik będzie nadal '0′.
jak wszystko w tej dziedzinie – można wymyślić kilka innych wersji dających taki, lub nawet lepszy wynik, ale celem całego wpisu ma być pomoc tym, którzy z KQLem przygodę zaczynają, i podobnie jak ja – bazy danych/zbiory danych nie są ich chlebem powszednim. i jeśli jeszcze nie jesteś przekonana(y) – powtórka z działań na zbiorach będzie na prawdę pomocna, bo pozwala na zmianę sposobu myślenia z sekwencyjnego przetwarzania, na bardziej przestrzenne.
eN.

start-process powershell.exe

niby proste, a wpisów na forach dużo.. a jednak zajęło mi sporo czasu. wiele rzeczy uważane za proste zajmuje mi czas, żeby dogłębnie zrozumieć i dopiero mogę je wykorzystać – it’s not a bug, it’s a feature (;

nie pamiętam już nawet wszystkich zapisów, których próbowałem, żeby to odpalić… więc tylko szorcik, z samym rozwiązaniem:

Start-Process "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" -ArgumentList @("-noExit","-file c:\scripts\somescript.ps1")
kilka uwag:
  • w dużej ogólności widać, że ArgumentList jest tablicą argumentów (nie ciągiem znaków, nie hashtable)
  • pomimo że 'file’ oraz ścieżka są de facto dwoma elementami – jednak w tablicy argumentów podaje się je razem, jako jeden „argument-z-wartością”. to nie jest oczywiste, a wręcz trochę dziwne…
  • uruchomienie skryptu z przełącznikiem 'file’ i bez niego zachowuje się nieco inaczej
  • druga bardzo ważna uwaga, którą długo pomijałem (czytaj doqmentację DOKŁADNIE!), to że jeśli używa się 'file’ to musi to być ostatni parametr. można również uruchomić skrypt ’powershell.exe c:\scripts\somescript.ps1’ i wtedy musi to być pierwszy argument.

eN.

PowerShell dla początkujących

jakiś czas temu przygotowałem wraz z Microsoft szkolenie na MVA – Microsoft Virtual Academy. MVA został zlikwidowany i long-story-short – szkolenia przepadły. moje wypłakiwanie się przyniosło sqtek i jeden z czytelników odezwał się, iż ma backup. dziękuję ci Piotrze w imieniu swoim oraz przyszłych oglądających (:

udało mi się również skontaktować z Microsoft i otrzymać pozwolenie na publikację! dziękuję!

tu znajdziesz PowerShell dla początkujących

nie jestem wprawnym youtuberem, ale mam nadzieję, że opublikowałem prawidłowo. na razie pierwsza część, jak wszystko będzie ok – są jeszcze dwie (:

eN.