Vadims 的个人资料Vadims Podans's former b...照片日志列表更多 工具 帮助

日志


2008/11/9

Закрытие блога

По независящим от меня (а может и зависящим) причинам я сегодня объявляю о закрытии свего блога на spaces.live.com. Безусловно, блоггинг-движок здесь один из самых продвинутых, которые я видел, но, тем не менее, обладает некоторыми недостатками, которые мне кажутся важными. Это:

  • очень слабые возможности управления комментариями в постах;
  • отсутствие возможности присоединения поста к нескольким категориям;
  • качество работы в последнее время оставляет желать лучшего.

Конечно же, постов тут не так и много, но невозможность удобного ведения категорий со временем превратит блог в кучу неотсортированных сообщений. Создание для каждого поста новой категории, как мне кажется, порядка не добавит.

Но я не прекращаю свою деятельность в качестве блоггера. Прежде чем закрыться мне нужно было найти альтернативу spaces и результатом поисков стал мой новый блог на отдельном хостинге. Я очень надеюсь, что мои постоянные читатели не сильно обременятся переключением на новый RSS feed.

Что касается текущего блога, то я закрываю любые новые комментарии, отсоединяю RSS ленту (смысла в ней уже нету) и закрываю гостевую книгу. Весь контент сохраняется как есть и оставляю возможность связываться со мной через личные сообщения на spaces.

Ну и самое главное - мой новый блог:

>> www.sysadmins.lv <<

2008/11/2

Книга "Введение в Windows PowerShell"

 

Сегодня закончил читать книгу "Введение в Windows PowerShell", которую купил на ozon.ru. По сути данная книга - ужатый вариант симбиоза PowerShell In Action и Windows PowerShell Cookbook, который не содержит пространных рассуждений (коих в PowerShell In Action немало) и опущено достаточно много серьёзного (который применяется лишь в специфичных задачах) материала. Оно, собственно говоря, и верно, т.к. из названия видно, что это введение и в ней рассказываются только те вещи, которые необходимы для тех, кто только начинает изучение PowerShell. Но вместе с этим книга, как мне кажется, потеряла некоторую живость, т.е. текст выглядит достаточно сухо и идея лёгкости и простоты языка так же теряется. Я читал книгу как типичный хэндбук и руководство к пользованию. К технической части материала никаких претензий нету совершенно, т.к. тех.материал подобран автором удачно. Но это не учебник по высшей математике, а вводная книга по технологии, которая должна не только рассказать об этой технологии, но и заинтересовать ею читателя (книга "PowerShell In Action" образцово решила как техническую, так и общелитературную и агитационную составляющую). Если бы я не был до этого знаком с PowerShell и не читал других книг, то я вряд ли заинтересовался "очередной глючной поделкой от мелкософта" (цэ) луноходы этой технологией. Как мне кажется, причина тому:

Андрей Владимирович Попов


Кандидат физико-математических наук,  доцент кафедры технологий программирования 
Мордовского госуниверситета  им. Н.П.Огарева

http://popov.math.mrsu.ru/ - сайт со страничкой автора книги.

Редко встречаются доценты и кандидаты наук, которые могут излагать свои мысли просто и с азартом (во всяком случае мне такие в РТУ не попадались). Но, тем не менее, видно, что автор старался писать наиболее понятным языком. Хотя, несколько слов (а точнее их значения в русском языке) мне так и остались непонятны. В принципе, все недостатки книги легко компенсируются технической составляющей. На редкость в книге разбираются не абстрактные примеры, а вполне реальные и живые примеры, которыми просто напичкана книга Windows PowerShell Cookbook. Вобщем, автору удалось выбрать самое лучшее из имеющихся англоязычных книг, хоть и общая концепция немного была утеряна и главы стали несколько оторваны друг от друга. Но в общем и целом впечатление от книги осталось очень и очень хорошее, т.к. данная книга позволяет сразу вникать в суть PowerShell, его работы и возможностей, позволяя за очень быстрое время начать писать собственные скрипты на новом (во всяком случае, ему ещё и 2 года не стукнуло) скриптовом языке.

  • Я рекомендовую данную книгу для тех, кто только собирается начать изучение (или только начал изучать и хочет пойти дальше, кроме как умение запускать отдельные командлеты) PowerShell и достаточно быстро научиться писать собственные скрипты.
  • Далее, если читатель захочет расширить свои познания в PowerShell я рекомендую прочесть PowerShell In Action. Данная книга позволит не только ближе ознакомиться с языком, но и проникнуться его философией.
  • После можно будет серьёзно браться за PowerShell Cookbook (которую при первом приближении стоит рассматривать, как коллекцию скриптов), в которой уже описывается сам подход к решению задач в скриптах и с ней учиться разбирать готовые и писать собственные скрипты уже высокой сложности и эффективности.

Список актуальных и полезных книг по PowerShell прикреплены на главной странице блога в разделе "Книжная полка".

 

материал далее никоим образом не относится к содержанию и автору книги, поэтому при прочтении можно опустить, т.к. он выражает моё личное мнение к исполнению книги.


К типографии у меня 2 претензии:

  1. как можно печатать книги на туалетной бумаге? Серовато-полупрозрачную бумагу, которая чуть толще газетной бумаги я иначе назвать не могу. Если для художественных произведений это ещё куда не шло (хотя, тоже не самый удачный выбор), то для тех.документации это недопустимо. Художественную литературу прочитал и положил на полку. Здесь же читать книгу и возвращаться к ней придётся не один раз. Плюс, в процессе изучения книга будет (во всяком случае предполагается такое) активно использоваться и, соответсвенно, изнашиваться будет не меньше. После 10 прочтений книга явно придёт в не совсем потребный вид. А ведь уже 2008 год на дворе, если что.
  2. Шрифт. В последнее время читаю литературу только на английском языке (издательства MS Press и др.). Скажем, взять ту же Windows Vista Resource Kit или Windows Server 2008 Pki and Certificate Security. Открываешь книгу и она уже приятна для зрительного восприятия, её удобно читать. Здесь же шрифт подобран "совок классический" на бумаге "бумага совковая, классическая". Я, конечно же, понимаю, что администраторы должны уметь читать мануалы на любой бумаге с любым шрифтом и кроме материала книг ничего лишнего воспринимать не должны. Но, я думаю, на западе же не зря тратятся солидные средства только на оформление и дизайн печати, чтобы читателю было приятно открыть книгу, прочитать и получить от этого как эстетическое, так и интеллектуальное удовольствие. Здесь же книгу открыл, прочитал и появляется желание с ней сходить в туалет.

Вобщем, как говорит Артемий Лебедев: так верстают только мудаки. (цэ)

2008/10/29

Saruna par PowerShell by default

 

Citāts

PowerShell by default

The next versions of Windows will have PowerShell installed by default (except server core where its an option) -  http://blogs.msdn.com/powershell/archive/2008/10/28/powershell-will-be-installed-by-default-on-windows-server-08-r2-ws08r2-and-windows-7-w7.aspx

This is going to make remote admin a dream. When you add the new features in PowerShell v2 and the additional Windows functionality based on PowerShell - http://blogs.msdn.com/powershell/archive/2008/10/29/ny-times-delcares-powershell-to-be-30-of-the-value-of-windows-7.aspx

it really is going to be a case of you have to learn PowerShell to do your job.  I'm currently working through the study guide for one of the Exchange 2007 exams and I'm amazed by how much of it is PowerShell based. Takes me back to the SQL Server 6.5 admin exam that was all based on Stored Procedures (yes I know I'm showing my age :-) ).

Command lines rule.

 

Share this post :

 

Technorati Tags:

-------------------------------------------------------------------------------------------------------------------------------------------

честно украдено с блога Ричарда Сиддэвэя (Richard Siddaway). Суть простая: во всех последующих версиях Windows (включая Windows Server 2008 R2, Windows 7, etc) консоль PowerShell будет установлена по умолчанию. Исключение составят серверные инсталляции в режиме Server Core, где PowerShell будет доступен в качестве опционального компонента (во всяком случае разработчики активно работают в этом направлении, дабы превратить этот факт в реальность). Это не может не радовать. В этом есть несколько плюсов:
  • простота языка, функционал и семантика (которая на порядки лучше, чем в CMD/VBS/JS) ускоряет разработку скриптов и сценариев на PowerShell;
  • следовательно, снижаются трудовые и финансовые затраты на администрирование сетей;
  • наконец-то луноходы могут заткнуться с фразами, что в Windows нету консоли! (это по сути был единственный аргумент луноходов, против которого пользователям Windows сказать было по сути нечего).

И ещё несколько слов:

Wow - the NY Times is Johnny-on-the-spot with their analysis of the 10 best features of Windows 7 for IT Pros HERE.  It turns out that PowerShell is responsible for 3 of them!

Это с одной стороны и приятно и с другой стороны вполне ожидаемо. Всё-таки меньше чем за 2 года Windows PowerShell наверстал упущенные возможности развития командной оболочки Windows. Да, раньше с этим было значимо хуже, зато за этот период (со времени релиза PowerShell) мы имеем то, что в альтернативных ОС разрабатывалось десятилетиями. Но это уже детали, т.к. мы живём здесь и сейчас. Опенсорс зажимают со всех сторон. Но эт уже не проблемы Windows :)
2008/10/26

Преобразование типов доступа в ACL PowerShell

Сегодня на форуме TechNet задали вопрос о том, как преобразовать числовое значение типа прав доступа к объекту в его текстовое значение (см. тут). В предыдущих постах, посвящённых управленю ACL из PowerShell я использовал этот приём, но не акцентировал на этом внимание. Поэтому я подумал, что пора поставить на этом вопросе жирную точку.

Итак, как я не раз писал ранее, для управления списком доступа к объектам (ACL) используются различные классы .NET, например:

Если у нас есть числовое значение права доступа (как у автора топика на форуме), то преобразовать его в текстовый вид очень просто:

[System.Security.AccessControl.FileSystemRights]1179817

где 1179817 - числовое значение, которое описывает тип доступа. В данном случае это число соответствует праву ReadAndExecute и Synchronize. Если ввести другое число, например 721343:

[vPodans] [System.Security.AccessControl.FileSystemRights]721343
Modify, TakeOwnership

то мы получим текстовое значение прав, а именно - Modify и TakeOwnership. Бывают случаи, когда не допускается указания прав в текстовом виде и требуется указание только в числовом виде. Обратное преобразование выполняется при помощи свойства Value__ :

[vPodans] [System.Security.AccessControl.FileSystemRights]721343 | gm


   TypeName: System.Security.AccessControl.FileSystemRights

Name        MemberType Definition
----        ---------- ----------
CompareTo   Method     System.Int32 CompareTo(Object target)
Equals      Method     System.Boolean Equals(Object obj)
GetHashCode Method     System.Int32 GetHashCode()
GetType     Method     System.Type GetType()
GetTypeCode Method     System.TypeCode GetTypeCode()
ToString    Method     System.String ToString(), System.String ToString(String format, IFormatProvi..
value__     Property   System.Int32 value__ {get;set;}

Это единственное свойство, которое хранится в данном объекте. Посмотрим, как это работает на практике:

[vPodans] ([System.Security.AccessControl.FileSystemRights]"FullControl").value__
2032127

Вот так мы получили числовое значение права FullControl. По этой аналогии можно преобразовать типы доступа и к другим объектам, как реестр:

[vPodans] ([System.Security.AccessControl.RegistryRights]"FullControl").value__
983103
[vPodans] [System.Security.AccessControl.RegistryRights]2
SetValue
[vPodans]

Enjoy!

2008/10/14

Собираем базовые сведения о железе системы с помощью PowerShell

Ричард Сиддэвей (Richard Siddaway) в своём блоге ведёт на мой взгляд интересный цикл постов Windows 2000 Scripting Guide (W2KSG) с применением PowerShell и WMI. В них рассказываются достаточно интересные и полезные возможности классов WMI для сбора различных сведений как программной части системы, так и аппаратной. Так же недавно на форуме TechNet была поднята (да, на форумах всегда найдутся археологи, которые выкопают темы полу- и годичной давности, а то и ещё старше :) ) тема про скрипт, который бы собрал данные об аппаратной составляющей компьютеров. Подобные темы периодически всплывают на различных форумах. Я подумал, что неплохо было бы решить данный вопрос с помощью PowerShell.

Итак, отправной точкой для меня послужила ссылка на Computer System Hardware Classes, где я посмотрел какие классы можно применить. Изучив весь список я отобрал лишь самые необходимые для решения задачи классы, а именно:

Достаточно сходить по ссылкам и можно посмотреть множество свойств каждого класса, которые детально описывают себя. Но при прочтении очень важно следить за поддерживаемыми свойствами в ОС, которые были выпущены до Windows Vista/2008. Я старался эти моменты учитывать, чтобы получить оптимальную совместимость как с предыдущими ОС, так и с текущими.

Ничего сверхсложного в этом нету, сперва я определил требуемые классы WMI в переменные и определил набор необходимых свойств каждого класса следующим образом:

$OS = gwmi  Win32_OperatingSystem | Select Caption, OSArchitecture,
OtherTypeDescription, ServicePackMajorVersion, CSName, TotalVisibleMemorySize
$CPU = gwmi  Win32_Processor | Select Architecture, DeviceID, Name
$RAM = gwmi  Win32_MemoryDevice | Select DeviceID,
StartingAddress, EndingAddress
...

После определения всех классов WMI и переменных я начал писать секцию вывода. Например, вывод имени компьютера и ОС, под которой он управляется:

"Computer Name: `n`t" + $OS.CSName + "`n"
"Operating System: `n`t" + $OS.Caption + " " + $OS.OtherTypeDescription + $OS.OSArchitecture + "`n"

Здесь можно не обращать на знаки регулярных выражений, т.к. они несут только одну функцию, а именно - удобное для воспроиятия форматирование вывода. Т.е. первой строкой пойдёт название поля Computer Name, после чего будет переход на новую строку (`n). Чтобы все строчки не сливались я значения полей отделил табулятором (`t). И в конце так же добавил знак возврата каретки (`n), чтобы отделить между собой поля. Вывод в данном случае будет таким:

Computer Name:
        THOR


Operating System:
        MicrosoftR Windows VistaT Business  32-bit


Service Pack:
        Service Pack 1 installed


[vPodans]

Т.е. поля идут с левого края и между собой отделены двумя пустыми строками. А значения полей отделены табулятором относительно того же левого края. Мне кажется, что такой выход весьма читабелен и данные не сливаются в кучу. Когда у поля только одно значение - всё просто. Но когда значений возможно несколько, то задача форматирования выхода таких данных стала для меня небольшой проблемой. Почему небольшой - потому что ответ посмотрел в блоге у Ричарда, а именно в посте W2KSG: Free Disk Space. Суть заключается в очень простом: берётся переменная с массивом данных и по конвейеру при помощи команды Format-Table подготавливается табличный выход и с указанием основного элемента, по которому будет этот выход формироваться. Чтобы лучше понять этот процесс я покажу его на примере вывода сведений о процессоре. Сейчас многопроцессорные системы не редкость и скрипт должен поддерживать показ сведений о более чем одном процессоре:

[user name] $CPU = gwmi  Win32_Processor | Select Architecture, DeviceID, Name
[user name] "Processors:"
Processors:
[user name] $CPU | ft DeviceID, @{Label = "Architecture"; Expression = {switch ($_.Architecture) {
>> "0" {"x86"}; "1" {"MIPS"}; "2" {"Alpha"}; "3" {"PowerPC"}; "6" {"Intel Itanium"}; "9" {"x64"}}}},
>> @{Label = "Model"; Expression = {$_.name}} -AutoSize
>>

DeviceID Architecture Model
-------- ------------ -----
CPU0     x86          Intel(R) Pentium(R) 4 CPU 2.60GHz
CPU1     x86          Intel(R) Pentium(R) 4 CPU 2.60GHz


[user name]

К сожалению, ноутбук у меня однопроцессорный, поэтому эту часть скрипта я запустил на стационаре (в котором, кстати, тоже только 1 физический процессор. Но технология Hyper-Threading эмулирует именно настоящую мультипроцессорность, а не логические ядра, как это сделано в многоядерных процессорах). Итак, давайте разберём строку:

$CPU | ft DeviceID, @{Label = "Architecture"; Expression = {switch ($_.Architecture) {
"0" {"x86"}; "1" {"MIPS"}; "2" {"Alpha"}; "3" {"PowerPC"}; "6" {"Intel Itanium"}; "9" {"x64"}}}},
@{Label = "Model"; Expression = {$_.name}} -AutoSize

Переменная $CPU содержит сведения о всех установленных процессорах в системе. Я эту переменную передал по конвейеру сразу на команду форматирования - ft (сокращённый алиас от Format-Table) и указал по какому свойству форматировать (DeviceID, который перечисляет ID номера всех процессоров начиная с 0). А дальше я использовал несколько хэш-таблиц для отображения дополнительных свойств каждого объекта. Параметр Label задаёт название новой графе, а Expression указывается значение параметра (как Architecture и Model). Если посмотреть справку по классу Win32_Processor, то можно увидеть, что свойство Architecture содержит лишь числовое значение (от 0 до 9) и в справке приведена расшифровка этих значений. Числовые значения в данном случае, согласитесь, не самый читабельный вариант. Поэтому в Expression я вместил конструкцию Switch, которая автоматически будет числовым значениям сопоставлять понятные текстовые значения. И если свойство Architecture вернёт числовое значение 0, то Switch в нашем случае сопоставит ему более понятное значение x86. И в последней строке я добавил ещё одну хэш-таблицу, которая добавляет ещё одну графу - Model, которая будет содержать свойство Name класса Win32_Processor - название процессора.

Такое форматирование выхода я делал для каждого поля, которое может содержать несколько значений. Например, объём каждого установленного модуля памяти:

$RAM = gwmi  Win32_MemoryDevice | Select DeviceID, StartingAddress, EndingAddress

DeviceID будет содержать номер каждого установленного модуля памяти начиная с нулевого ряда (разбор понятий Ряд и Банк памяти выходит за рамки данного поста) или первого слота. StartingAddress и EndingAddress показывают адресное пространство, за которое отвечает каждый модуль начиная от первого байта. И простым вычитанием начального адреса из конечного мы получим ёмкость каждого модуля:

$RAM | ft DeviceID, @{Label = "Module Size(MB)"; Expression = {(($_.endingaddress - $_.startingaddress) / 1KB).ToString("F00")}} -AutoSize

Так же, как и с процессорами я содержимое переменной $RAM перенаправил по конвейеру на форматирование по столбцу DeviceID. И через запятую добавил ещё одну хэш-таблицу, которая будет показывать объём каждого модуля. Если просто произвести операцию вычитания, то мы получим объём памяти в килобайтах. Чтобы показать объём в мегабайтах я просто разделил полученную разность на килобайты - 1KB (Очень удобная штука :) ). Здесь важно было не ошибиться, т.к. по логике может показаться, что нужно делать на 1MB, чтобы получить размер в мегабайтах. Но, как я уже сказал выше, у нас разность будет уже в килобайтах. Поэтому, чтобы получить в мегабайты, то нам нужно разделить только на 1KB. И посмотрим, что мы будем иметь на выходе:

[vPodans] $RAM = gwmi  Win32_MemoryDevice | Select DeviceID, StartingAddress, EndingAddress
[vPodans] $RAM | ft DeviceID, @{Label = "Module Size(MB)"; Expression = {(($_.endingaddress - 
$_.startingaddress) / 1KB).ToString("F00")}} -AutoSize

DeviceID        Module Size(MB)
--------        ---------------
Memory Device 0 1024
Memory Device 1 1024

[vPodans]

Вот так мы получили номера модулей памяти и объём каждого из них. После деления у нас не получится целое число, поэтому я сконвертировал это число в строку (ToString) и указал количество знаков после запятой - 0 знаков (F00). В данном случае дробное число просто округляется до ближайшего целого числа. Количество знаков после запятой можно изменить, например указав F01, которое округлит число до ближайшего целого числа с точностью до 1 знака после запятой. Это всё не я придумал, а честно взял из поста W2KSG: Free Disk Space :)

Вот, в принципе, я рассказал о всех используемых в скрипте приёмах, которые доступны в PowerShell и при дальнейшей разработке скрипта я только выдёргивал нужные свойства каждого класса и формировал более-менее приличный и понятный вывод. Хотя в итоге я добавил ещё 2 вещи:

  1. Оформил скрипт в функцию Get-HwInfo и с возможностью передачи в неё имён компьютеров
  2. Каждый WMI класс позволяет собирать сведения не только с локальных комьютеров, но и с удалённых. Имя далённого компьютера указывается через параметр -ComputerName при вызове команды Get-WMIObject, например: $CPU = gwmi  Win32_Processor -ComputerName $computers | Select Architecture, DeviceID, Name

Таким образом извне можно подавать в функцию имена компьютеров (например, из текстового файла со списком имён компьютеров), что значительно расширяет функциональность скрипта. Ну и финальный скрипт:

########################################################
# Get-HwInfo.ps1
# Version 1.0
#
# Getting basic information about systems hardware
#
# Vadims Podans (c) 2008
#
http://vpodans.spaces.live.com/
########################################################

function Get-HwInfo ($computers = ".") {
$OS = gwmi  Win32_OperatingSystem -ComputerName $computers | Select Caption, OSArchitecture,
OtherTypeDescription, ServicePackMajorVersion, CSName, TotalVisibleMemorySize
$CPU = gwmi  Win32_Processor -ComputerName $computers | Select Architecture, DeviceID, Name
$RAM = gwmi  Win32_MemoryDevice -ComputerName $computers | Select DeviceID,
StartingAddress, EndingAddress
$MB = gwmi  Win32_BaseBoard -ComputerName $computers | Select Manufacturer, Product, Version
$VGA = gwmi  Win32_VideoController -ComputerName $computers | Select Name, AdapterRam
$HDD = gwmi  Win32_DiskDrive -ComputerName $computers | select Model, Size
$Volumes = gwmi  Win32_LogicalDisk -Filter "MediaType = 12" -ComputerName $computers | Select DeviceID,
Size, FreeSpace
$CD = gwmi Win32_CDROMDrive | Select Id, Name, MediaType
$NIC = gwmi Win32_NetworkAdapter -ComputerName $computers | ?{$_.NetConnectionID -ne $null}
"Computer Name: `n`t" + $OS.CSName + "`n"
"Operating System: `n`t" + $OS.Caption + " " + $OS.OtherTypeDescription + $OS.OSArchitecture + "`n"
"Service Pack: `n`t" + "Service Pack " + $OS.ServicePackMajorVersion + " installed`n"
"Processors:"
$CPU | ft DeviceID, @{Label = "Architecture"; Expression = {switch ($_.Architecture) {
"0" {"x86"}; "1" {"MIPS"}; "2" {"Alpha"}; "3" {"PowerPC"}; "6" {"Intel Itanium"}; "9" {"x64"}}}},
@{Label = "Model"; Expression = {$_.name}} -AutoSize
"Physical Memory: "
$RAM | ft DeviceID, @{Label = "Module Size(MB)"; Expression = {
(($_.endingaddress - $_.startingaddress) / 1KB).tostring("F00")}} -AutoSize
"Total Memory: `n`t" + ($OS.TotalVisibleMemorySize / 1KB).tostring("F00") + " MB`n"
"MotherBoard: "
"`tVendor: " + $MB.Manufacturer
"`tModel:  " + $MB.Product
"`tVersion: " + $MB.Version + "`n"
"Videocontroller:"
"`tModel: " + $VGA.Name
"`tVideo RAM: " + ($VGA.AdapterRam/1MB).tostring("F00") + " MB`n"
"HarddDisks:"
$HDD | ft Model, @{Label="Disk Size(GB)"; Expression = {($_.Size/1GB).tostring("F01")}} -AutoSize
"Disk Partitions:"
$Volumes | ft DeviceID, @{Label="TotalSize(GB)"; Expression={($_.Size/1GB).ToString("F01")}},
@{Label="FreeSize(GB)"; Expression={($_.FreeSpace/1GB).tostring("F01")}} -AutoSize
$CD | ft Id, @{Label = "Media Type"; Expression = {$_.MediaType}},
@{Label = "Model"; Expression = {$_.Name}} -AutoSize
"Netwok Adapters:"
$NIC | ft NetConnectionID, @{Label="Media Status"; Expression = {switch ($_.NetConnectionStatus) {
"0" {"Disconnected"}
"1" {"Connecting"}
"2" {"Connected"}
"3" {"Disconnecting"}
"4" {"Hardware not present"}
"5" {"Hardware disabled"}
"6" {"Hardware malfunction"}
"7" {"Media disconnected"}
"8" {"Authenticating"}
"9" {"Authentication succeeded"}
"10" {"Authentication failed"}
"11" {"Invalid address"}
"12" {"Credentials required"}
}}},
@{Label="NIC"; Expression={$_.name}}
}

Здесь видно, что в конце я снова применил конструкцию Switch, которая расшифровывает числовое значение статуса сетевого адаптера в его текстовое значение.

Согласен, что скрипт выглядит не очень опрятно, но в PowerGUI он выглядит вполне сносно. На основе данного скрипта каждый может его с лёгкостью расширить и изменить под свои нужды, я лишь старался показать образец решения задачи, а так же показал некоторые интересные приёмы в PowerShell. Кстати говоря, данный скрипт можно отправить в HTML формат командой ConvertTo-Html. Это будет удобно, когда потребуется собрать подобные сведения с нескольких компьютеров. Тогда HTML формат будет весьма полезен для последующего анализа. На сегодня всё, а теперь спать.

2008/10/2

Foreach, Foreach-Object и оптимизация циклов в PowerShell

Коллега, Вася Гусев в посте Непонятные штуки - $_ и % рассказал интересную и полезную тему про циклы. Однако я хочу немного дополнить этот рассказ. Я хочу показать некоторые технические нюансы использования цикла вида Foreach-Object и Foreach на практических примерах.

1) задача: произвести простое последовательное переприсвоение каждого элемента большого массива в другую переменную. Для решения этой задачи мы сгенерируем массив случайных чисел:

[vPodans] $a = 1..50000 | %{get-random}

И каждый элемент массива $a переприсвоим в переменную $X. А так же измерим время выполнения цикла. Для измерения времени работы цикла я буду использовать команду Measure-Command, которая показывает время исполнения команды с достаточно высокой точностью. Сперва переменную $a перенаправим по конвейеру в цикл Foreach-Object.

  • Пример 1

[vPodans] (Measure-Command {$a | Foreach-Object {$x = $_}}).TotalSeconds
4,7633872
[vPodans] (Measure-Command {$a | Foreach-Object {$x = $_}}).TotalSeconds
4,7561438
[vPodans] (Measure-Command {$a | Foreach-Object {$x = $_}}).TotalSeconds
4,7858102
[vPodans]

Я повторил команду 3 раза и мы получили среднее время обработки цикла равным примерно 4,7 секунд. Теперь повторим ту же операцию, но с использованием Foreach.

  • Пример 2

[vPodans] (Measure-Command {Foreach ($b in $a) {$x = $b}}).TotalSeconds
0,0465927
[vPodans] (Measure-Command {Foreach ($b in $a) {$x = $b}}).TotalSeconds
0,04518
[vPodans] (Measure-Command {Foreach ($b in $a) {$x = $b}}).TotalSeconds
0,0465084
[vPodans]

Здесь же мы видим, что цикл в среднем выполняет ту же операцию за 0,046 секунды! Или иными словами Foreach почти в 100(!) раз быстрее, чем Foreach-Object. При проведении простой обработке большого массива мы видим заметное преимущество Foreach. Однако, от типа данных и типа их обработки это число (100 раз) может изменяться как в сторону увеличения, так и уменьшения. Для этого обратимся к примеру 3 и 4.

2) Задача: выбрать все записи в журнале System (в моём случае в журнале находится 42855 записей) журнала событий и записать сообщения в текстовый файл. Данную задачу можно решить при помощи цикла Foreach-Object и Foreach. В отличии от предыдущего примера мы сталкиваемся с новым фактором влияния - запись на диск. И снова сперва будем использовать Foreach-Object.

  • Пример 3. Чтение журнала System и выдача тела эвентов по конвейеру в цикл:

Get-Process | ?{$_.processname -eq "powershell"}
(Measure-Command {Get-EventLog System | %{$_.message >> events.txt}}).TotalSeconds
Get-Process | ?{$_.processname -eq "powershell"}

И вот результаты выполнения данного скрипта:

[vPodans] Get-Process | ?{$_.processname -eq "powershell"}

Handles  NPM(K)    PM(K)      WS(K) VM(M)   CPU(s)     Id ProcessName
-------  ------    -----      ----- -----   ------     -- -----------
    219       7    37424      35568   178     0,98   4356 powershell


[vPodans] (Measure-Command {Get-EventLog System | %{$_.message >> events.txt}}).TotalSeconds
249,4354958
[vPodans] Get-Process | ?{$_.processname -eq "powershell"}

Handles  NPM(K)    PM(K)      WS(K) VM(M)   CPU(s)     Id ProcessName
-------  ------    -----      ----- -----   ------     -- -----------
    229       7    42648      42116   186   229,48   4356 powershell


[vPodans]

Здесь я добавил команду Get-Process, которая покажет нам некоторые моменты, которые в первых двух примерах были неактуальны. Итак, выгрузку тела сообщений журнала System в файл цикл Foreach-Object выполнил за 249 секунд, при этом показав незначительное увеличение потребляемой памяти. К слову говоря, размер файла составил 21,5МБ.  Хочу отметить, что цифры потребления памяти очень сильно расходятся с показаниями Task Manager, который во время инициализации консоли показывает 17,5МБ потребления памяти и 22МБ после завершения работы.

 

  • Пример 4. Чтение журнала System в перменную и обработка тела эвентов циклом Foreach:

Get-Process | ?{$_.processname -eq "powershell"}
(Measure-Command {$events = Get-EventLog System}).TotalSeconds
(Measure-Command {Foreach ($event in $events) {$event.message >>events.txt}}).TotalSeconds
Get-Process | ?{$_.processname -eq "powershell"}

Скрипт выполняет точно такую же операцию, только я добавил ещё один временной штамп, когда переменная $events будет заполнена и будет подана в foreach на разбор. И вот, что я получил:

[vPodans] Get-Process | ?{$_.processname -eq "powershell"}

Handles  NPM(K)    PM(K)      WS(K) VM(M)   CPU(s)     Id ProcessName
-------  ------    -----      ----- -----   ------     -- -----------
    218       7    37400      35560   178     0,98    512 powershell


[vPodans] (Measure-Command {$events = Get-EventLog System}).TotalSeconds
10,8001047
[vPodans] (Measure-Command {Foreach ($event in $events) {$event.message >>events.txt}}).TotalSeconds
340,2231286
[vPodans] Get-Process | ?{$_.processname -eq "powershell"}

Handles  NPM(K)    PM(K)      WS(K) VM(M)   CPU(s)     Id ProcessName
-------  ------    -----      ----- -----   ------     -- -----------
    237       7   116652     114432   270   331,39    512 powershell


[vPodans]

Итак, что мы здесь видим? Мы видим, что время считывания журнала System в переменную $Events составило чуть больше 10 секунд. А вот уже обработка тела эвентов (запись каждого тела в файл) заняла 340 секунд! Если сравнивать с примером использования Foreach-Object мы не то, чтобы получили уменьшение времени выполнения, но даже увеличение на 91 секунду или 1,5 минуты и это без учёта 10 секунд чтения журнала в переменную. При этом мы так же наблюдаем внушительный прирост потребления памяти почти в 4 раза! По сведениям Task Manager потребление памяти увеличилось с 17,5МБ до 95МБ (даже в 5 раз).

На показательных примерах я продемонстрировал особенности работы цикла Foreach-Object и Foreach при обработке различных типов данных и различных операциях с ними - только простое последовательное переприсвоение и запись достаточно большого объёма данных в файл.

Выводы:

  • Foreach-Object выгоден для сложной обработки большого объёма массивных данных и перенаправления их в небыстрые хранилища (например, в файл, где очень важным фактором становится невысокая скорость работы диска). Данный цикл работает достаточно быстро и характеризуется скромным потреблением памяти. Однако, когда мы обрабатываем данные несложными операциями (т.е. по сути важным фактором становится скорость работы центрального процессора и памяти), то Foreach-Object значительно уступает в скорости циклу Foreach и уступание может достигать колоссальных размеров (в примерах 1 и 2 он уступил в скорости в 100 раз).
  • Foreach выгоден при несложной обработке данных на лету без использования медленных посредников (как запись в файл), когда в основном используются только ресурсы процессора. За счёт увеличения скорости обработки данных в оперативной памяти увеличивается и потребление памяти. Но с увеличением сложности обработки (если взять пример 2 и сделать не переприсвоение, а простое математическое деление каждого элемента массива на число 3 и присвоение результата в переменную, то разница в скорости работы уже составит не 100 раз, а всего лишь 3 раза) скорость выполнения цикла заметно падает. Этот момент очень ярко выражается при использовании медленных посредников (жёсткий диск, сеть) может не только не давать прирост в скорости, но и даже снижать производительность по сравнению с Foreach-Object в схожих операциях. И в целом, Foreach характеризуется значительным увеличением потребления памяти. И чем сложнее обработка, тем более ощутимо увеличивается потребление памяти и снижается скорость работы, как это показано на примере 4.

Изучив особенности работы каждого цикла мы порою можем значительно оптимизировать работу скрипта с использованием циклов и выбирать тот или иной метод по таким критериям, как скорость работы и объём потребляемой памяти при работе скрипта, который выполняет реальные задачи на реальном сервере.

Удачи! © One

2008/9/28

Аудит входов на сервере терминалов с использованием PowerShell

Очень часто на форумах спрашивается скрипт или приложение, которое позволит в удобном виде предоставить администратору сведения о логоне пользователей на терминальный сервер. При этом есть задача показать только время входа, имя пользователя и IP адрес клиента, с которого терминально подключаются к сервер. Как известно эту информацию как правило берут из журнала Security журналов событий.

Событие успешного терминального логона имеет ID=528. Однако под этим ID регистрируются все входы, как интерактивные с консоли, так и терминальные. Для того, чтобы отличить типы входов (консольные и терминальные) в сообщении эвента используется поле Logon Type. Вот так выглядит типовой текст эвента с ID=528 в Windows XP/Windows Server 2003:

 

[user name] (Get-EventLog security | ?{$_.eventid -eq 528 })[0].message
Successful Logon:


        User Name:      user name


        Domain:         CAMELOT


        Logon ID:               (0x0,0x1F016C95)


        Logon Type:     10


        Logon Process:  User32


        Authentication Package: Negotiate


        Workstation Name:       CAMELOT


        Logon GUID:     -


        Caller User Name:       CAMELOT$


        Caller Domain:  WORKGROUP


        Caller Logon ID:        (0x0,0x3E7)


        Caller Process ID: 21848


        Transited Services: -


        Source Network Address: 81.198.xxx.xxx

        Source Port:    34834


[user name]

и мы видим строку: Logon Type: 10, где цифра 10 имеет самые частые типы входов:

  • 2 - интерактивный вход с консоли;
  • 3 - сетевой вход по SMB;
  • 10 - вход с использованием служб терминалов.

Поэтому при поиске событий, которые относятся только к успешным входам с использованием Termianl Services (или вход с консоли) мы должны кроме ID события включить дополнительный параметр поиска по типу входа. Можно сделать так:

Get-EventLog security | ?{$_.eventid -eq 528 -and $_.message -like "*Logon Type: 10*"}

Но вы получите нулевой результат, поскольку второе условие не выполнится. Спросите почему? А всё потому что между заголовком поля (Logon Type: ) и значением поля (10) нету пробелов, а используется табуляция! Скажу, что я убил несколько часов на то, чтобы понять, почему у меня ничего не ищется по пробелам. Когда понял, что там используется [Tab], то я быстро строку переделал в:

Get-EventLog security | ?{$_.eventid -eq 528 -and $_.message -like "*Logon Type:`t10*"}

[user name] (Get-EventLog security | ?{$_.eventid -eq 528 })[0].message | %{$_ -match "`t"}
True
[user name] Get-EventLog security | ?{$_.eventid -eq 528 -and $_.message -like "*Logon Type:`t10*"}

Index Time          Type Source                EventID Message
----- ----          ---- ------                ------- -------
 3068 Sep 28 14:32  Succ Security                  528 Successful Logon:...
 3061 Sep 27 00:35  Succ Security                  528 Successful Logon:...
 3052 Sep 26 18:49  Succ Security                  528 Successful Logon:...

обратите внимание на выделенный участок `t - таким образом в регулярных выражениях обозначается табулятор - [Tab]. Если так задать поиск, то команда найдёт все сообытия успешного входа с использованием терминальных служб. Чтобы с ними начать работать запишем все эти события в переменную:

$Events = Get-EventLog security | ?{$_.eventid -eq 528 -and $_.message -like "*Logon Type:`t10*"}

Прежде чем начинать разбор эвентов я решил создать форму шаблона для предоставления удобного вывода и чтобы отсечь лишнюю инофрмацию. Я решил выбирать дату и время логона, имя пользователя, который залогинился на сервере терминалов и исходящий IP адрес клиента, с которого было произведено подключение:

$Data = New-Object System.Management.Automation.PSObject
$Data | Add-Member NoteProperty Time ($null)
$Data | Add-Member NoteProperty UserName ($null)
$Data | Add-Member NoteProperty Address ($null)

В постах, посвящённых управлению сетевыми папками из PowerShell я показывал пример, как создаётся объект и обвязывается нами уже нужными для нас свойствами и методами. В данном случае я создал объект $Data, который будет иметь 3 свойства

  • время и дата входа;
  • имя пользователя;
  • IP адрес.

Итак, теперь пора приступить к разбору всего лога. Первая засада (а их будет несколько) нас ждёт вот в чём:

[user name] ((Get-EventLog security | ?{$_.eventid -eq 528 })[0].message).GetType().FullName
System.String

Этой командой я посмотрел тип свойства Message события в надежде, что это будет массив строк (которые мы увидели в отформатированном виде на первом листинге), но это оказалась одна целая строка с напичканными раздилителями внутри, которые форматируют текст для удобного восприятия. Значит нам придётся самостоятельно поделить строку на массив строк, чтобы мы смогли читать только нужные нам строки. Если поля и значения разделены табулятором, то отдельные секции внутри строки разделены символом возврата каретки, который в регулярном выражении имеет вид - `n. Убедимся, что это правда:

[user name] (Get-EventLog security | ?{$_.eventid -eq 528 })[0].message | %{$_ -match "`n"}
True

Команда вернула нам True, что позволяет нам разделить строку эвента на подстроки. Выше мы уже узнали, что кроме возврата каретки для форматирования используется так же и табулятор. С использованием этих разделителей мы сможем достаточно гибко разбивать строку Message на более мелкие составляющие. Для начала мы разделим эту строку на подстроки, а так же уберём в каждой подстроке разделители, которые расставлены с начала, с конца и посредине каждой строки. Для этого мы воспользуемся тремя методами, которые есть у объекта String:

  • Split - разделяет строку на отдельные составляющие по указанному разделителю;
  • TrimStart - удаляет любые символы вначале каждой строки по маске или все разделители, если маска не задана;
  • TrimEnd - делает то же самое, но только в конце каждой строки.

И получится у меня такая строка:

$Events | %{$Data.time = $_.TimeGenerated; $message = $_.message.split("`n") | %{$_.trimstart()} | %{$_.trimen()}

Я заранее собранные эвенты пропустил через через конвейер. Т.к. время не содержится в теле события (с которым мы и будем только работать), поэтому я его сразу перенаправил в свойство Time переменной $Data, которая будет содержать выходной отчёт. Далее я создал переменную $Message, которая будет хранить подстроки тела эвента. Попутно я его методом Split разделил на подстроки и методами TrimStart/TrimEnd я так же поудалял табуляторы с начала и концов строк.

Теперь нам осталось записать имя пользователя и IP адрес. Для этого нас будет интересовать только поле User Name и Source Network Address. Делается это очень просто:

$Data.UserName = $message | ?{$_ -like "User Name:*"}

я пропускаю тело эвента через фильтр, который начинается с поля User Name и имеет произвольное продолжение. Но если так и оставить, то мы получим примерно такой выход:

Time                              UserName                        Address                
----                                --------                             -------                
2008.09.28. 14:32:33      User Name:    user name
  

Как вы видите в $UserName попало не только имя пользователя, но и имя поля (на машине с 2003 сервером у меня используется логин user name и вышло вот такое совпадение :) ) Чтобы избавиться от заголовка поля нужно воспользоваться регулярным выражением, которое отделит заголовок поля от значения поля. Как я уже выше говорил, между полем заголовка и значением используется разделитель [Tab], поэтому пропустим строку через регулярное выражение, которое сделает за нас всю работу. Выражение будет таким: $_ -replace "^.+`t *" . Ну и подключим это выражение к нашей строке:

$Data.UserName = ($message | ?{$_ -like "User Name:*"} | %{$_ -replace "^.+`t *"})

Данное регулярное выражение говорит:

  • ^ - начинать с начала строки
  • . - обозначает любой символ в строке
  • + - обозначает один или более символов в строке
  • `t - указывает границу, до куда должно работать регулярное выражение.
  • * - ноль и более указанных символов включая сам символ (наш табулятор будет так же удалён)

После исполнения этой команды мы получим уже такой выход:

Time                              UserName                  Address                
----                                --------                       -------                
2008.09.28. 14:32:33      user name

И мы уже в колонке UserName мы будем получать только полезные значения полей, а именно - имена пользователей. Теперь повторим ту же процедуру и для адреса. На первом листинге видно, что IP адрес содержится в строке Source Network Address:

$Data.Address = ($message | ?{$_ -like "Source Network Address:*"} | %{$_ -replace "^.+`t *"})

Как видите, мы почти повторили команду, изменению подвергнулась лишь маска поиска нужной строки. Отфильтровав регулярным выражением мы так же отбросили заголовок поля и записали в $Data.Address только полезные данные - IP адрес.

Если вы приготовились ещё на пол часа утомительных разборов эвентлога, то можете расслабиться, т.к. поставленная задача решена. Осталось только вывести на экран переменную $Data и закрыть все нужные скобки. А выход у нас дополнился последним полем IP адреса:

Time                              UserName                 Address                
----                                --------                      -------                
2008.09.28. 14:32:33      user name                 81.198.xxx.xxx

В личных целях я стёр последние 2 октета своего IP, но в целом понятно, что там будет адрес. Даватйте теперь соберём всё это в единый скрипт:

########################################################
# TerminalServerLogonAudit.ps1
# Version 1.0
#
# Windows XP/Windows Server 2003 eventlog parser
#
# Vadims Podans (c) 2008
#
http://vpodans.spaces.live.com/
########################################################
$Events = Get-EventLog security | ?{$_.eventid -eq 528 -and $_.message -like "*Logon Type:`t10*"}
$Data = New-Object System.Management.Automation.PSObject
$Data | Add-Member NoteProperty Time ($null)
$Data | Add-Member NoteProperty UserName ($null)
$Data | Add-Member NoteProperty Address ($null)
$Events | %{
$Data.time = $_.TimeGenerated; $message = $_.message.split("`n") | %{$_.trimstart()} | %{$_.trimend()}
$Data.UserName = ($message | ?{$_ -like "User Name:*"} | %{$_ -replace "^.+`t *"})
$Data.Address = ($message | ?{$_ -like "Source Network Address:*"} | %{$_ -replace "^.+`t *"})
$Data
}

И напоследок покажу ещё раз выход данного скрипта:

Time                      UserName                  Address                 
----                      --------                  -------                
2008.09.28. 14:32:33      user name                 81.198.xxx.xxx         
2008.09.27. 0:35:09       user name                 81.198.xxx.xxx         
2008.09.26. 18:49:09      Administrator             127.0.0.1

Как видите, парсинг событий эвентлога - занятие далеко не самое приятное. Но тем не менее любую задачу парсера можно разбить на 3 этапа:

  • Разбор строки с телом эвента на более мелкие логические объекты с которыми можно индивидуально работать;
  • очистка мусора (удаление разделителей из строки и т.д.);
  • работа с конкретными логическими объектами эвента.

В принципе, я считаю, что данный скрипт можно взять за основу и незначительными изменениями скрипта можно значительно расширить или изменить функционал парсера. Может быть на его основе получится сделать парсер аудита доступа к объектам, т.к. чтение логов аудита - одно из самых скучных и утомительных занятий. Если есть что сказать - пишите в коментариях. И в завершении хочу сказать, что данный скрипт не подходит для систем Windows Vista/Windows Server 2008, поскольку по причинам, которые описаны здесь:

Странности Get-Eventlog

в новых системах значительно изменили работу с эвентлогами. Но об этом как-нибудь в другой раз.

2008/9/5

Windows Vista и Software Restriction Policies

Очень часто люди задают вопросы по поводу безопасности своих локальных машин, терминальных серверов и постоянно ищут средство защиты систем от поражения вирусами, троянами и просто контроля запуска узкого числа инсталлированных приложени. И каждый раз снова и снова приходится отвечать тремя словами на этот вопрос, а именно - Software Restriction Policies. Данная политика совмещает себе как простоту реализации, так и эффективность её работы. В критических случаях пользователю разрешено запускать только те приложения, которые явно разрешил запускать администратор. Данная технология не нова, поэтому подробно расписывать её здесь не буду, а лишь обозначу ключевые отличия реализации данной политики в Windows Vista/Windows Server 2008 от реализации в WindowsXP/Windows Server 2003.

Политика включается как обычно:

  • локальная - Start -> Run... -> secpol.msc -> Software Restriction Policies
  • доменная - Group Policy Object Editor -> Computer Configuration -> Windows Settings -> Security Settings -> Software Restriction Policies

Итак, какие изменения у нас появились по сравнению с предыдущими версиями:

  1. в Security Levels добавился новый уровень безопасности Basic User;
  2. в Additional Rules удалены 2 правила по умолчанию, которые разрешают запуск EXE файлов в папках Windows и Windows\System32;
  3. при использовании правил Hash Rule теперь вместо MD5 (как в предыдущих версиях) используется более стойкий алгоритм хэширования Sha256, но при этом сохранилась совместимость со старыми клиентами XP/2003 (будет храниться 2 хэша - MD5/Sha1 и Sha256);
  4. в Enforcement добавился Enforce/Ignore Certificate Rule;

Казалось бы, пустяк, но я считаю, что тут есть о чём поговорить. Итак, сначала поговорим о новом действии политики как Basic User. Данное действие политики было специально заделано под User Account Control (UAC) и которое совместно с UAC филтрует права пользователя на запуск. Если политики UAC распространяются на все приложения без исключения, то комбинирование действия по умолчанию UAC и при использовании Software Restriction Policies позволяет запретить повышение привелегий для некоторых приложений. Иными словами, даже если запустить приложение с повышенными привилегиями, они всё равно будут отфильтрованы.

Пример 1.

Рассмотрим поведение политики Basic User для установки действия Basic User для программы CMD.EXE:

Сперва при отключенной политике выполню команду, которая включает режим Hibernate для компьютера. Для этого нажимаю на ярлыке CMD правой кнопкой и выбираю Run As Administrator и получаю предупреждение UAC:

cmd1

соглашаюсь и получаю консоль командной строки, в которой набираю следующую команду и исполняю:

cmd2

 

данная команда включает режим Hibernate (возможно не самый удачный пример) и ошибок не вернула.

Действие общей политики здесь неважно, поэтому перейдём сразу к Additional Rules. Для этого я создал Hash Rule для CMD.EXE как показано на картинке:

 

cmd3

Теперь снова запускаю консоль CMD с повышенными привилегиями и повторяю процедуру:

cmd4

хоть я и просил повышенные привилегия для исполнения команды, но действие Basic User не дало этой возможности и отфильтровало мои права до обычного пользователя. Т.е. мы видим, что Basic User ни за что не позволяет выполнить приложение в привилигированном режиме!

Пример 2.

В предыдущем примере я добровольно пытался запустить приложение с повышенными привилегиями. В результате никакого повышения не получилось и приложение запустилось в обычном режиме. А теперь рассмотрим запуск приложения, которое требует повышенния привилегий через UAC. Для этого я буду использовать запуск консоли Computer Management с возможностью изменения настроек консоли. Я удалил правило для CMD.EXE и создал такое же правило для консоли compmgmt.msc:

comp1

И при нажатии правой кнопкой на Computer -> Manage получил запрос на повышение прав от UAC с чем я и согласился:

comp2

и снова действие Basic User отфильтровало мои повышенные права и выдало такое окошко. Как видно из скриншота политика блокировала запуск консоли. Ещё раз повторюсь, запуск данной консоли с возможностью изменения настроек без повышения прав через UAC невозможно, что мы и видим на рисунке.

Резюме: Действие политики SRP Basic User не позволяет ни в коем случае выполнить приложение с повышенными привилегиями. Если это доступно, то приложение запускается в обычном режиме, как показано на примере 1 или вообще запрещает запуск, если приложение для запуска требует повышенных прав, как это видно на примере 2.

Примечение: Когда первый раз я стал изучать политику SRP на своём нотебуке с Windows Vista был неприятно удивлён, когда при активации политики Disallowed у меня сохранялась возможность запуска .EXE файлов напрямую из проводника и .LNK из меню Start -> Run... . Я помню по XP/2003, что политика активировалась в момент при выборе действия политики. Здесь же я обнаружил что политика применилась лишь частично, игнорировав .EXE и .LNK файлы. Этим вопросом я озадачил Александра Станкевича, который позднее подсказал, что наблюдал такое же поведение на 2008 сервере до тех пор, пока не перезагрузил компьютер. Поэтому при инициализации политики для её полной активации нужно перезагрузить компьютер! Когда политика проинициализирована перезагрузка больше не требуется до полной отмены политики и все изменения применяются на лету. О чём, кстати сообщается в правом окне политики SRP, когда политика удалена или не создана (эхх, невнимательный какой).

Ну и ещё добавлю небольшую заметку по интеграции политик SRP с UAC. В Enforcement есть опция применения политики SRP For All Users except Administrators, т.е. применение политики ко всем пользователям, кроме администраторов. Данная опция работает только для приложений, которые запущены с повышенными привилегиями. Это вполне логично, т.к. даже локальный администратор работает в системе с правами обычного пользователя (ситуацию, когда UAC отключён совсем я не рассматриваю, ибо это не есть хорошо), поэтому в обычной работе политика в любом случае будет воздействовать на него. Это важно понимать, т.к. не совсем явная вещь и её легко упустить из виду.

Что касается Enforce/Ignore Certificate Rule, то тут наверное нету смысла объяснять, что это. По умолчанию правила сертификатов проверяются в первую очередь и если у вас они не используются, то включение обработки правил сертификатов может значительно снизить скорость запуска приложений.

Порядок (приоритет) применения правил политики SRP:

  1. Certificate Rules
  2. Hash Rules
  3. Path Rules
  4. Default Rule

поэтому отключение правил сертификатов, когда они не используются будет весьма кстати. Напомню, что Certificate Rule может использоваться не только для фильтрации подписанных приложений, но и для разрешения запуска подписанных доверенными сертификатами скриптов.

И на последок дам 3 .REG файла, которыми я пользуюсь в работе для управления политикой:

SRP_Enable - включает действие политики в Disallowed:

Windows Registry Editor Version 5.00

[HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\safer\codeidentifiers]
"DefaultLevel"=dword:00000000

SRP_Disable - включает действие политики в Unrestricted:

Windows Registry Editor Version 5.00

[HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\safer\codeidentifiers]
"DefaultLevel"=dword:00040000

SRP_Basic - включает действие политики в Basic User:

Windows Registry Editor Version 5.00

[HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\Safer\CodeIdentifiers]
"DefaultLevel"=dword:00020000

Я обычно кладу эти 3 файла (теперь 3, до этого использовались только SRP_Enable и SRP_Disable) в папку Windows и на рабочий стол администраторской учётной записи создаю ярлыки для них:

srp_buttons

эти ярлыки включены в исключения политики и указывают на соответствующие .REG файлы, которые так же находятся в исключениях. Однако хочу заметить, что действие этих ярлыков в среде рабочей группы может быть изменено после перезагрузки системы (после перезагрузки вернётся действие, которое указано в политике), а в доменной среде - либо после перезагрузки либо при последующем обновлении политик (по умолчанию - 1 раз в 90 минут). Поэтому данные ярылки дают лишь временный эффект, но как правило они и нужны на очень короткий срок.

Ну и хочу поделиться маленькой приятной особенностью, которая появилась сейчас. Администраторы, которые работали с политикой SRP в системах XP/2003 испытывали головную боль при обновлении серверов и рабочих станций. Проблема была в самом механизме работы инсталляторов апдейтов, которые в корне произвольного тома создавали папки с произвольными именами, в которые распаковывались апдейты и затем устанавливались. Такая схема не представляла возможным создание правил для SRP, чтобы без отключения политики установить апдейты. Поэтому во время апдейтов приходилось отключать политику и после включать её обратно. В Windows Vista и Windows Server 2008 это уже не так! Я сегодня же проверил возможность установки обновлений с Windows Update при включенной политике SRP Disallowed. И все 16 обновлений спокойно установились и политику отключать не пришлось. Это хороший плюс для Windows Vista!

Вроде осветил основные изменения политик Software Restriction Policies и ничего не забыл. Если забыл что-то, то пните ;)

упс, чуть не забыл, официальную ссылку на TechNet:

Using Software Restriction Policies to Protect Against Unauthorized Software

2008/8/12

param-пам-пам и немного про $args

Заметил, что при использовании PowerShell у некоторых администраторов появляются разные потребности по интерактивной работе скриптов и с автоматическим приёмом параметров с командной строки. Упрощённый пример: есть скрипт, который создаёт папку на диске. При этом скрипт должен принимать в качестве аргумента командной строки, так и user interface. В принципе, это реализовать несложно. А вот если ещё захотеть сделать так, что при отсутствии аргументов с использованием user interface было принято некое значение по умолчанию, то тут могут возникнуть маленькие трудности. Давайте рассмотрим несколько вариантов.

Итак, тело простого скрипта, который принимает параметры только с командной строки. Как изветно, для передачи аргументов из командной строки в скрипт используется команда param:

param ($path)
New-Item -Path $path -ItemType directory -force

если этот скрипт сохранить и запустить из командной строки, то формат запуска будет такой:

powershell C:\Temp\folder.ps1 'C:\Test'

данный скрипт будет работать только при запуске с командной строки и обязательной передачей аргументов. Следует запомнить, что param должен быть первой рабочей строкой в скрипте, кроме комментариев. Если при отсутствии аргументов хотим использовать заранее предопределённое значение (default), то нужно добавить проверку переданного параметра:

param ($path)
if (!$path) {$path = "C:\Temp"}
New-Item -Path $path -ItemType directory -force

здесь я во второй строке проверяю переменную $path. Обратите внимание, что я проверяю её отрицание (выделил восклицательный знак). Данная строка говорит, что если переменная $path равна $null (т.е. ничему), то переменной $path присваивается дефолтное значение C:\Temp. Здесь вопросов как бы не возникает. Теперь посмотрим, как сделать интерактивное окно, с использованием командлета Read-Host.

$path = $(read-host "Введите путь")
New-Item -Path $path -ItemType directory -force

При запуске этих строк появится приглашение ввести путь. Всё очень просто. Теперь сделаем предустановленное значение:

$path = "С:\Temp"
$path = $(read-host "Введите путь")
$path

Здесь я первой строкой сделал значение по умолчанию, второй строкой приглашение ввести путь и третьей строкой отображаю содержимое переменной. Вот выход:

[vPodans] .\path.ps1                                                                                                                        
Введите путь: D:\
D:\
[vPodans] .\path.ps1
Введите путь: welcome
welcome
[vPodans] .\path.ps1
Введите путь:

[vPodans]

Тут хорошо видно, что переменная $path принимает всё, что мы вводим. Однако, как мы видим в последнем случае, даже если просто нажать Enter, то заранее определённая переменная $path будет перезаписана командой Read-Host, а именно будет присвоено значение $null. Для того, чтобы определить, содержит ли переменная $path какое-то значение (т.е. не был нажат пустой Enter) нужно в коде делать проверку переменной:

$path = "С:\Temp"
$InputPath = $(read-host "Введите путь")
if ($InputPath) {$path = $InputPath}

для этой проверки пришлось вводить новую переменную. А теперь посмотрим, как это всё можно скомпоновать под задачу: передача аргументов из командной строки и через интерфейс пользователя. Если параметры не переданы, то в качестве аргумента принять значение заранее определённой переменной.

В любом случае первой строкой будет команда param, которая в любом случае перезапишет объявленную переменную во второй строке. То же самое касается и команды Read-Host. Однако с Read-Host дела обстоят немного проще, чем с param, по причине, что Read-Host не обязан быть первой строкой в скрипте. Задачу можно решить примерно таким образом:

param ($ArgPath)
$path = "C:\Temp"
if ($ArgPath) {$path = $ArgPath} else {
$InputPath = $(Read-Host "Введите путь. По умолчанию будет использован путь $path")
if ($InputPath) {$path = $InputPath}}

Вот здесь пришлось задействовать 3 переменные и заняло 5 строчек. Не очень-то и удобно. Но в PowerShell есть есть одна очень приятная во всех отношениях специальная переменная, а именно: $args. Данная переменная всегда хранит переданные аргументы с командной строки (неважно, приняты они через param или нет. Правда есть некоторые нюансы, о которых я расскажу чуть позже). Сейчас покажу как можно эффективно использовать эту переменную:

$path = "C:\Temp"
if ($args) {$path = $args} else {$InputPath = $(Read-Host "Введите путь. По умолчанию будет использован путь $path")
if ($InputPath) {$path = $InputPath}}
New-Item -Path $path -ItemType directory -WhatIf

Я выкинул из первой строки param и переменную $ArgPath. Во второй строке я проверяю содержимое переменной $args, т.е. скрипт запущен с аргументами (несопровождаемый режим) или без аргументов (интерактивный режим). Если интерактивный режим, где пользователю предлагается ввести запрашиваемое значение, то делается последняя проверка - ввёл пользователь что-то или просто нажал Enter. В любом случае оба действия будут весьма корректно и ожидаемо обрабатываться. Ну и всё уместилось в 3 строчки плюс строчка создания папки :)

[vPodans] .\path.ps1                                                                                                                        
[vPodans] .\path.ps1 D:\User
What if: Performing operation "Create Directory" on Target "Destination: D:\User".
[vPodans] .\path.ps1
Введите путь. По умолчанию будет использован путь C:\Temp: D:\Temp
What if: Performing operation "Create Directory" on Target "Destination: D:\Temp".
[vPodans] .\path.ps1
Введите путь. По умолчанию будет использован путь C:\Temp:
What if: Performing operation "Create Directory" on Target "Destination: C:\Temp".
[vPodans]

Переменная $args может содержать не один, а несколько переданных аргументов:

[vPodans] function ShowArgs {
>> $args
>> }
>>
[vPodans] ShowArgs 1..10
1..10
[vPodans] function ShowArgs {
>> $args
>> }
>>
[vPodans] ShowArgs Arg1 Arg2 Param3
Arg1
Arg2
Param3
[vPodans]

Как здесь мы можем наблюдать, переменная $args принимает аргументы не только для скрипта в целом с использованием param, но и для функций тоже. Ну и в завершении хочу сказать несколько слов про $args. Если в скрипте используется param для приёма аргументов из командной строки, или фнукция с заданными принимаемыми аргументами, то в переменную $args будут помещены только те аргументы, которые не определены в списке принимаемых аргументов:

[vPodans] function ShowArgs ($a, $b, $c) {$args}
[vPodans] ShowArgs 1 2 3
[vPodans] ShowArgs 1 2 3 4 5
4
5
[vPodans]

при создании функции я указал, что для вызова функции потребуется передать 3 аргумента. Когда я передал ровно 3 аргумента, то $args мне ничего не вернула. Когда же я при вызове передал больше аргументов, чем опредено в функции, то лишки помещаются в переменную $args. Если же не используется param или в функции не определены аргументы, то $args будет содержать все аргументы, которые были переданы с вызовом функций. Ну и как вы заметили $args является полноценным массивом и с ней можно проделывать практически все те же операции, что и с обычными массивами.

Ну вот, на сегодня и всё :)

2008/8/8

Странности Get-Eventlog

Ни для кого не секрет, что PowerShell умеет очень замечательно работать с системным журналом событий. И, так же, ни для кого не секрет, что для этого используется командлет Get-Eventlog. Если в WindowsXP/Windows Server 2003 он работает замечательно, то в Windows Vista результаты несколько странноватые. Давайте посмотрим, что у нас есть. Для примера попробуем выбрать аудит неудачных входов в систему. В WindowsXP и Windows Server 2003 Failed user logon имеет код события EventID = 529, а в Windows Vista - 4625. Пробуем выбрать эвенты из Windows Server 2003:

[C:\] Get-EventLog security | ? {$_.EventID -eq 529} | select -first 3

Index  Time             Type   Source                EventID Message
-----    ----                ----    ------                -------       -------
4224  Aug 08 11:46  Fail    Security                  529    Logon Failure:...
4150  Aug 08 11:33  Fail    Security                  529    Logon Failure:...
4107  Aug 08 11:29  Fail    Security                  529    Logon Failure:...

Вот у меня тут есть 3 события. Давайте посмотрим текст первого эвента:

[C:\] (Get-EventLog security | ? {$_.EventID -eq 529})[0].message
Logon Failure:
        Reason:         Unknown user name or bad password
        User Name:      Administrator
        Domain:         CONTOSO
        Logon Type:     2
        Logon Process:  User32
        Authentication Package: Negotiate
        Workstation Name:       TEMPLATE
        Caller User Name:       TEMPLATE$
        Caller Domain:  CONTOSO
        Caller Logon ID:        (0x0,0x3E7)
        Caller Process ID:      320
        Transited Services:     -
        Source Network Address: 127.0.0.1
        Source Port:    0

Ну вот, вроде как всё понятно тут. А теперь попробуем вытащить ту же самую информацию из Windows Vista:

[System32] Get-EventLog security | ? {$_.EventID -eq 4625} | select -first 3

Index  Time              Type Source                       EventID  Message
-----    ----                ----   ------                          -------    -------
13651 Aug 05 21:56  Fail   Microsoft-Windows...    4625      The description for Event ID '4625' in Source 'Microsoft-Wind...
13645 Aug 05 19:15  Fail   Microsoft-Windows...    4625      The description for Event ID '4625' in Source 'Microsoft-Wind...
13593 Aug 03 13:12  Fail   Microsoft-Windows...    4625      The description for Event ID '4625' in Source 'Microsoft-Wind...

Смотрим текстовое сообщение первого эвента:

[System32] (Get-EventLog security | ? {$_.EventID -eq 4625} | select -first 3)[0].message
The description for Event ID '4625' in Source 'Microsoft-Windows-Security-Auditing' cannot be found.  The local compute
r may not have the necessary registry information or message DLL files to display the message, or you may not have perm
ission to access them.  The following information is part of the event:'S-1-5-18', 'THOR$', 'WORKGROUP', '0x3e7', 'S-1-
0-0', 'vPodans', 'THOR', '0xc000006d', '%%2313', '0xc000006a', '7', 'User32 ', 'Negotiate', 'THOR', '-', '-', '0', '0xd
34', 'C:\Windows\System32\winlogon.exe', '127.0.0.1', '0'

Как видно из первой части - мы не можем найти описание эвента. Далее мы получаем информацию в совершенно нечитабельном виде. В GUI это выглядит вот так:

evt1

Всё красиво и понятно. Но это ещё пол беды. На многих эвентах вообще ничего прочитать через PowerShell нельзя:

[System32] Get-EventLog application | select -first 5

Index Time               Type  Source                        EventID  Message
-----    ----                ----    ------                           -------    -------
13268 Aug 08 10:36  Info    Desktop Window Ma...  9009      The Desktop Window Manager has exited with code (0x40010004)
13267 Aug 08 10:36  Erro    EventSystem               4621      The description for Event ID '-1073737203' in Source 'EventSy...
13266 Aug 08 10:34  Info    Microsoft-Windows...        1        The description for Event ID '1' in Source 'Microsoft-Windows...
13265 Aug 08 10:34  Info    Winlogon                     4101      Windows license validated.
13264 Aug 07 20:06  Info    VSS                            8224      The VSS service is shutting down due to idle timeout....

2 и 3-й эвенты тоже предсказывают стать нечитабельными. Например 3-й эвент с кодом 1:

[System32] (Get-EventLog application | select -first 5)[2].message
The description for Event ID '1' in Source 'Microsoft-Windows-CertificateServicesClient' cannot be found.  The local co
mputer may not have the necessary registry information or message DLL files to display the message, or you may not have
permission to access them.  The following information is part of the event:

В консоли MMC по факту всё красиво и гламурно:

evt2

1, 4 и 5-й эвенты уже отсюда видно, что они читабельны:

[System32] (Get-EventLog application | select -first 5)[4].message
The VSS service is shutting down due to idle timeout.

Вот теперь всё очень даже понятно и читабельно. Подведя резюме к экспресс тесту командлета Get-Eventlog можно констатировать, что по сути мы не имеем возможности парсить логи журналов событий в Windows Vista и Windows Server 2008 при использовании как минимум Windows PowerShell 1.0 RTM, что есть не очень хорошо.

Кстати говоря, для примеров использовался Windows PowerShell 1.0 RTM без установленных расширений под управлением Windows Server 2003 Enterprise Edition SP2 R2 и Windows Vista Business SP1 (обе x86).

2008/7/18

Полезная безделушка Hash SHA1 на PowerShell

В сети есть уйма маленьких консольных и графических утилит, которые позволяют считать хэши MD5/SHA1/SHA256/etc для файлов. Это иногда очень полезно. Расслабляясь после ряда статей по управлению Share Permissions решил от нечего делать написать свою утилитку полностью на PowerShell, которая интегрируется с контекстным меню Windows Explorer. Ну и самописный инсталлятор на PowerShell, который всю эту интеграцию и выполнит. Конечно же, это будет выглядеть продуктом, который выполнен на коленке в поезде, но тем не менее, это неплохой пример для начала работы с GUI в PowerShell. Ну что-ж, давайте разбираться.

У нас в Windows Explorer при правом клике на файле (только файле) будет контекстное меню вот такого вида:

hash1

Делается это очень просто, мы просто создадим ветку в реестре и зададим ей значение следующего вида:

Ключ реестра:
HKEY_CLASSES_ROOT\*\Shell\Hash SHA1\command
Значение элемента Default:
C:\WINDOWS\system32\WindowsPowerShell\v1.0\powershell.exe\" -nologo -noninteractive -noprofile -command get-location | hash.ps1 '%1'

Данная команда запускает PowerShell без логотипа и без профиля. Ключ -command показывает какие команды следует исполнить. В нашем случае это будет вызов скрипта hash.ps1 с аргументом в виде переменной %1 . Данная переменная будет содержать путь к файлу и использоваться как аргумент для скрипта. Как я уже рассказывал раньше про передачу аргументов из командной строки в скрипт, то при использовании пробелов в аргументах (а путь к файлу очень даже может содержать пробелы), то его нужно заключить в одинарные кавычки. Теперь давайте вспомним, как создавать путь в реестре и присваивать им занчения из PowerShell:

New-Item -Path "Registry::HKLM\Software\Classes\*\Shell\Hash SHA1\command" -Force

В конце строки я использовал ключ -Force, чтобы путь создался полностью, даже если реальный путь обрывается посередине. Без этого ключа PowerShell вернёт ошибку, что ключ реестра не существует и не сможет создать последующие ключи. Теперь пора внести запись в реестр. Выше я писал ключ реестра, который находится в ветке HKEY_CLASSES_ROOT, а создаю ключ в ветке HKLM (HKEY_LOCAL_MACHINE). Всё очень просто. Тут нужно понимать структуру реестра в Windows NT системах. На самом деле на диске физически хранятся лишь HKLM и множество кустов HKCU (по одному кусту на каждого пользователя), которые в итоге составляют один общий раздел HKU. Остальные же ветки реестра, которые мы видим в классическом реестре являются лишь ссылками на соответствующие ключи этих двух основных файлов кустов реестра. Для удобочитаемости кода я длинные значения присвоил переменным и в основном коде использую уже переменные:

$RegPath = "Registry::HKLM\Software\Classes\*\Shell\Hash SHA1\command"
$RegValue = "C:\WINDOWS\system32\WindowsPowerShell\v1.0\powershell.exe -nologo -noninteractive -noprofile -command C:\hash.ps1 '%1'"
New-Item -Path $RegPath -Force
New-ItemProperty -Path $RegPath -Name "(Default)" -Value $RegValue

После выполнения этих команд мы получим в контекстном меню элемент Hash SHA1. А теперь уже можно начинать писать основной код, который будет считать хэш файла. Мне из хэш-методов больше нравится SHA1 (кому-то может и MD5 или ещё что), поэтому писать буду применительно под него (что характерно от используемого хэш-метода код изменится только в названии класса и всё). Для SHA1 в .NET есть класс System.Security.Cryptography.SHA1. Давайте создадим новый инстанс с этим классом. Если посмотреть список членов класса SHA1, то найдём там Create. В принципе, этого хватает вполне:

$hasher = [System.Security.Cryptography.SHA1]::Create()

Хорошо, инстанс создали, что дальше? Дальше нужно подобрать метод для данного класса. Из списка методов нам для решения поставленной задачи пригодится метод ComputeHash(Stream). Stream в данном случае означает, что будет создаваться поток байтов и пропускаться через метод ComputeHash для вычисления хэша. Значит, нам нужно создать поток байтов. В описании метода ComputeHash в описании параметров указано как создавать поток байтов, а именно с использованием System.IO.Stream. Давайте создадим объект с этим классом и в качестве параметра ему передадим путь к файлу. Путь файла сожержится в переменной $file:

$inputStream = New-Object System.IO.StreamReader ($file)

теперь файл будет преобразован в виде потока байтов. Ну что, теперь можно этот поток байтов можно передать в метод ComputeHash класса System.Security.Cryptography.SHA1. Сказано - сделано:

$hashBytes = $hasher.ComputeHash($inputStream.BaseStream)

Теперь завершаем поток байтов:

$inputStream.Close()

В принципе, если вместо переменной $file подставить путь к файлу (пока без вызова из контекстного меню, а просто из GUI, то мы увидим результат работы скрипта:

[C:\] $hasher = [System.Security.Cryptography.SHA1]::Create()
[C:\] $inputStream = New-Object System.IO.StreamReader ("C:\windows\Notepad.exe")
[C:\] $hashBytes = $hasher.ComputeHash($inputStream.BaseStream)
[C:\] $inputStream.Close()
[C:\] return $hashbytes
125
108
181
162
72
110
38
219
208
171
69
138
233
234
9
49
57
116
174
96

На выходе мы получили массив посчитанных байтов. Чтобы собрать этот массив в строку мы воспользуемся классом System.Text.StringBuilder. Создадим объект с этим классом:

$builder = New-Object System.Text.StringBuilder

Теперь посмотрим, какие методы у него есть - StringBuilder Members. Здесь нас заинтересует метод Append Method (Byte). Т.к. у нас массив байтов, то нужно данный метод использовать в цикле foreach-object и необходимо его привести в HEX формат:

$hashBytes | Foreach-Object { [void] $builder.Append($_.ToString("X2")) }
$output = New-Object PsObject
$output | Add-Member NoteProperty HashValue ([string] $builder.ToString())

В первой строке мы только сложили массив байтов в строку. Теперь нужно весь этот результат куда-то разместить. Для этого второй строкой мы создаём собственный абстрактный объект и добавляем ему свойство HashValue (можно и на своё усмотрение название придумать) и присваиваем ему значение. Давайте посмотрим, что после исполнения кода мы будем иметь в переменной $output:

[C:\] $output.hashvalue
7D6CB5A2486E26DBD0AB458AE9EA09313974AE60

Жирным я выделил содержимое переменной, а именно - итоговый хэш-код файла notepad.exe по системе SHA1! Отлично, сам конструктив уже готов (код, который для файлов считает хэш). Теперь немного GUI. Объявляем часть GUI:

[void][reflection.assembly]::LoadWithPartialName("System.Windows.Forms")

В принципе, эта команда используется всегда перед использованием GUI элементов. Давайте создадим простенькую небольшую форму и зададим ей какие-нибудь свойства:

$form = new-object Windows.Forms.Form
$form.Text = "$file SHA1 Hash"
$form.width = 314
$form.height = 110
$form.startposition = "CenterScreen"
$form.MaximizeBox = 0
$form.MinimizeBox = 0
$form.FormBorderStyle = "FixedSingle"
$form.autosize = 0

Первой строкой мы создали объект Windows.Forms.Form а в последующих строках заполнил свойства этого объекта. Касательно размеров я не действовал наугад, а создал примерное окно в Visual Studio с нужными размерами. И уже из Visual Studio выбирал нужные значения для свойств объекта. Напрямую из PowerShell не очень удобно рассчитывать размеры и компоновать элементы внутри формы. Я считаю, что тут комментарии излишни по коду, т.к. значения свойств понятны из самих названий. Например, свойство Text для формы определяет заголовок формы. У меня в заголовке динамически подставляется путь из переменной $file и добавил текст SHA1 Hash. На форме у меня будет ещё два элемента - Метка (Label) и кнопка (Button). Начнём с метки:

$label = new-object Windows.Forms.label
$label.Location = New-Object System.Drawing.Size(12,18)
$label.autosize = 1
$label.text = $output.hashvalue

местоположение метки относительно верхнего левого угла формы я взял из Visual Studio. В свойство Text я вписал содержимое переменной $output.hashvalue, которая динамически для каждого файла будет содержать SHA1 хэш. От созерцания хэша легче мне не станет, поэтому его нужно как-то изъять из метки простым способом (метка не поддерживает выделение и копирование текста. Даже при использовании элемента EditBox пришлось бы выделять текст и вручную копировать, что не сильно удобно). Для этого я на форме разместил кнопку, которая при нажатии будет копировать содержимое метки в буфер обмена и закрывать форму:

$button = new-object Windows.Forms.button
$button.Location = New-Object System.Drawing.Size(12,39)
$button.text = "Скопировать в буфер обмена"
$button.width = 274
$button.height = 23
$button.add_click({$label.text | clip;
$form.close()})

Примечение: вот тут мне пришлось отступиться от чистого кода на .NET, т.к. PowerShell не умеет нативно использовать буфер обмена и для этого нужно писать собственную функцию. В принципе, она уже есть в PowerShell Community Extensions и позволяет как записывать в буфер обмена, так и извлекать оттуда данные. У меня же нету задачи по извлечению данных из буфера обмена, поэтому я схитрил и использовал системную утилиту clip.exe, которая без расходования лишних строк позволяет скопировать данные в буфер обмена.

Вот и всё, больше ничего на моей форме не будет, поэтому можно активировать форму и объявлять в ней мои элементы:

$form.controls.add($label)
$form.controls.add($button)
$form.Add_Shown({$form.Activate()})
$form.ShowDialog()

Если создать нужные ключи реестра и сохранить этот код в файл, то при клике на контекстное меню Hash SHA1 появится вот такое окошко:

image

Конечно же тут есть один недостаток - на заднем плане нашей формы будет маячить чёрная консоль PowerShell. К сожалению я пока не придумал, как сделать по-тихому, чтобы на экране была бы только форма без окна консоли. Если придумаю, то обязательно напишу или, если у вас есть уже готовые наработки или идеи, оставьте их в комментариях. Теперь настало время подвести итог и написать конечный PS1 файл:

########################################################
# Hash SHA1_install.ps1
# Version 1.0
#
# SHA1 hash calculator for a file from context menu
#
# Vadims Podans (c) 2008
#
http://vpodans.spaces.live.com/
########################################################

$FilePath = Read-Host "Укажите путь для размещения Hash.PS1"
if (Test-Path $FilePath) {
$FilePath = $FilePath + "\" + "hash.ps1"
$RegPath = "Registry::HKLM\Software\Classes\*\Shell\Hash SHA1\command"
$RegValue = "C:\WINDOWS\system32\WindowsPowerShell\v1.0\powershell.exe -nologo -noninteractive -noprofile -command $FilePath '%1'"
New-Item -Path $RegPath -Force -ErrorAction SilentlyContinue
if (Test-Path $RegPath) {
New-ItemProperty -Path $RegPath -Name "(Default)" -Value $RegValue
New-Item -ItemType file -Path $FilePath -Force -ErrorAction SilentlyContinue
if (Test-Path $FilePath) {
$exefile = 'param ($file)
$hasher = [System.Security.Cryptography.SHA1]::Create()
$inputStream = New-Object System.IO.StreamReader ($file)
$hashBytes = $hasher.ComputeHash($inputStream.BaseStream)
$inputStream.Close()
$builder = New-Object System.Text.StringBuilder
$hashBytes | Foreach-Object { [void] $builder.Append($_.ToString("X2")) }
$output = New-Object PsObject
$output | Add-Member NoteProperty HashValue ([string]$builder.ToString())

# creating a form

[void][reflection.assembly]::LoadWithPartialName("System.Windows.Forms")
$form = new-object Windows.Forms.Form
$form.Text = "$file SHA1 Hash"
$form.width = 314
$form.height = 110
$form.startposition = "CenterScreen"
$form.MaximizeBox = 0
$form.MinimizeBox = 0
$form.FormBorderStyle = "FixedSingle"
$form.autosize = 0

# add a label

$label = new-object Windows.Forms.label
$label.Location = New-Object System.Drawing.Size(12,18)
$label.autosize = 1
$label.text = $output.hashvalue

# add a button

$button = new-object Windows.Forms.button
$button.Location = New-Object System.Drawing.Size(12,39)
$button.text = "Скопировать в буфер обмена"
$button.width = 274
$button.height = 23
$button.add_click({$label.text | clip;
$form.close()})

# showing the form

$form.controls.add($label)
$form.controls.add($button)
$form.Add_Shown({$form.Activate()})
$form.ShowDialog()'
Set-Content -Path $FilePath -Value $exefile}
else {Write-Warning "Не удалось создать файл по пути: $FilePath. Путь указан неверно, или у вас нету прав на запись в данную директорию. Операция прервана."}}
else {Write-Warning "Не удалось создать ключ реестра. Возможно у вас нету прав на запись в раздел HKLM. Операция прервана."}}

В конечном итоге я обвязал код некоторыми проверками, которые проверяют возможность записи в реестр (для работы скрипта нужно иметь право записи в HKLM) и в указанную пользователем папку. Если какое-либо действие не удастся, то код выдаст соответствующее сообщение об ошибке и прервёт дальнейшую операцию. Если обе операции записи прошли успешно, то происходит копирование рабочего кода (содержимое переменной $exefile), который уже не содержит инсталляционных и проверочных строк кода. Вот как бы и всё на сегодня :) .

2008/7/17

PowerShell - убийца WMI классов?

В процессе изучения вопросов управления Share Permissions для сетевых папок я угробил 4 виртуальные машины (я ленивый и бэкап их не делал). Точнее ряд классов WMI, которые относились в той или иной степени к сетевым папкам. Я по разному тестировал код и в процессе оптимизации команд иногда забывал дописывать до конца команды. Я долго не мог понять в причину проблемы. И пока укладывал одну за одной виртуальные машины я обнаружил закономерность, что у меня отваливались классы WMI в процессе работы функции по удалению сетевых папок. После написания финального скрипта по управлению безопасностью сетевых папок я решил заняться исследованием проблемы и обнаружил крайне досадную вещь, а именно - неполное написание команды для получения WMI-класса и использование метода Delete для данного класса приводит к удалению WMI. Заранее предупреждаю:

Не выполняйте на рабочих серверах и компьютерах нижеприведённые команды, а только в виртуальных машинах (при желании)!

Вот как всё было и что мы получили в итоге:

[C:\]$shares = ([WMIClass] "Win32_Share")
[C:\]$shares

Win32_Share

[C:\]$shares.delete()

В первой строке я не указал действие Get-WmiObject для класса Win32_Share, чтобы получить объект со списком сетевых папок на компьютере, а просто вызвал данный класс. В результате этого упущения в переменную $shares попал сам класс Win32_Share, как это видно в результате исполнения второй строки. Следующей командой я использовал метод Delete для класса Win32_Share. В ответ PowerShell мне промолчал. Ну что ж, давайте попробуем исправить эту ситуацию и вызовем класс Win32_Share, но уже с использованием команды Get-WmiObject:

[C:\]gwmi win32_share
Get-WmiObject : Invalid class
At line:1 char:5
+ gwmi  <<<< win32_share

Опаньки! И что это было? А это, оказывается, мы удалили (или ещё что сделали с ним) класс Win32_Share. Подумал немного, ну ладно, потеряли один класс, велика ли беда? А один ли? Теперь пробуем извлечь другой класс, скажем Win32_LogicalShareSecuritySetting, который я использовал для извлечения параметров безопасности из Security Descriptor:

[C:\]gwmi Win32_LogicalShareSecuritySetting
Get-WmiObject : Invalid class
At line:1 char:5
+ gwmi  <<<< Win32_LogicalShareSecuritySetting

Ну совсем прекрасно. И его мы потеряли тоже. Остальные классы вроде выглядят как рабочие (хотя, далеко не факт), но тем не менее проблема такая существует и она может доставить вам много неприятностей. Поэтому при работе с классами WMI в PowerShell нужно быть осторожными.

2008/7/16

Управление безопасностью общих папок (сетевых шар) в PowerShell (часть 4, заключительная)

Ну что ж, настало время подвести итоги по материалу управления сетевыми шарами и управлению доступа к ним. Как и обещал, я написал скрипт, который выполнен в виде функций при помощи которого можно действительно легко и просто управлять самими сетевыми папками и их безопасностью из PowerShell. Интеграция данного скрипта в профиль PowerShell позволяет использовать уже готовые функции как простые командлеты. Я считаю, это действительно большим плюсом, т.к. до этого администратору для решения этих задач приходилось писать длиннющий код в каждом скрипте, который касался безопасности сетевых шар. Итак, какой же функционал заложен в данный скрипт:

  1. создание сетевой папки;
  2. удаление сетевой папки;
  3. получения перечня всех сетевых папок на сервере с выводом необходимой информации о них;
  4. установка ACL сетевой папки;
  5. добавление ACE к существующему ACL сетевой папки;
  6. удаление единичных ACE изи ACL сетевой папки;
  7. просмотр текущих списков ACL сетевой папки;
  8. экспорт всех сведений (включая списки ACL) сетевых папок в CSV файл;
  9. импорт всех сведений (включая списки ACL) сетевых папок из CSV файла.

Касательно последнего пункта, то хочу отметить, что импорт при отсутствии наличия папки для расшаривания создаст папку и расшрарит с данными из CSV файла. Сначала я приведу список команд, которые доступны при использовании скрипта и их синтаксис:

  1. New-Share Name Path Description
    где Name - сетевое имя для папки;
    Path - путь к физической папке;
    Description описание к сетевой папке. При наличии пробелов -  заключить в кавычки (не обязательный параметр);
  2. Remove-Share Name
    где Name - сетевое имя папки;
  3. Get-Share Name
    где Name - имя сетевой папки (не обязательный параметр);
  4. Set-SharePermission Name User AccessMask AceType
    где Name - имя сетевой папки;
    User - имя пользователя/группы, которой предоставляется доступ;
    AccessMask - маска доступа. Этот параметр должен иметь одно из значений FullControl/Change/Read;
    AceType - тип доступа. Этот параметр должен иметь одно из значений Allow/Deny;
  5. Add-SharePermission Name User AccessMask AceType
    где Name - имя сетевой папки;
    User - имя пользователя/группы, которой предоставляется доступ;
    AccessMask - маска доступа. Этот параметр должен иметь одно из значений FullControl/Change/Read;
    AceType - тип доступа. Этот параметр должен иметь одно из значений Allow/Deny;
  6. Remove-SharePermission User
    где User - имя пользователя/группы, которого следует удалить из ACL сетевой папки;
  7. Get-SharePermission Name
    где Name - имя сетевой папки (не обязательный параметр);
  8. Export-ShareInfo Path
    где Path - путь к CSV файлу (включая имя файла). Если в пути присутствуют пробелы, то путь заключить в кавычки;
  9. Import-ShareInfo Path
    где Path - путь к CSV файлу (включая имя файла). Если в пути присутствуют пробелы, то путь заключить в кавычки.

Ну и собственно сам скрипт с некоторыми комментариями по работе самих функций. Для желающих подробно изучить скрипт я не делал особо комментариев, т.к. построение кода описано в предыдущих частях по управлению безопасностью сетевых папок. Ссылки на предыдущие части:

  1. Управление общими сетевыми ресурсами (шарами) в PowerShell
  2. Управление безопасностью общих папок (сетевых шар) в PowerShell (часть 1)
  3. Управление безопасностью общих папок (сетевых шар) в PowerShell (часть 2)
  4. Управление безопасностью общих папок (сетевых шар) в PowerShell (часть 3)

########################################################
# ShareUtils.ps1
# Version 0.0.0.5
#
# Functions for advanced share management
#
# Vadims Podans (c) 2008
#
http://vpodans.spaces.live.com/
########################################################

Write-Host "Vadims Podans's ShareUtils are installed"

# внутренняя функция, которая преобразовывает числовой код возврата операции записи ACL
# в текстовое значение.

function _ShareUtils_Get-Code ($share) {
switch ($Share.ReturnValue) {
   "0" {"Успешно"}
   "2" {"Отказано в доступе"}
   "8" {"Неизвестная ошибка"}
   "9" {"Указано недопустимое имя шары"}
   "21" {"Указан неправильный параметр"}
   "22" {"Сетевая шара уже существует"}
   "23" {"Путь перенаправлен"}
   "24" {"Указан неверный путь"}
   "25" {"Сетевое имя не найдено"}
   }
}

# основная функция экспорта сведений о сетевых папках в CSV файл.
function Export-ShareInfo ($path, $name) {
# если переменная $name пустая, то функция вовзращает все сетевые папки с типом DiskDrive
if ($name -ne $null) {
$shares = Get-WmiObject Win32_Share -filter "name = '$name'"
} Else {$shares = Get-WmiObject Win32_Share -filter 'type = 0'}
$Shareinfo = @()
# цикл извлечения сведений о каждой сетевой папке в переменную $ShareInfo
foreach ($share in $shares) {
  $ShareSec = Get-WmiObject Win32_LogicalShareSecuritySetting  -filter "name='$($share.name)'"
  if($shareSec) {
    $sd = $sharesec.GetSecurityDescriptor()
    $ShareInfo += $SD.Descriptor.DACL | % {
      $_ | select @{e={$share.name};n='Name'},
        @{e={$share.Path};n='Path'},
        @{e={$share.Description};n='Description'},
        AccessMask,
        AceFlags,
        AceType,
        @{e={$_.trustee.Name};n='User'},
        @{e={$_.trustee.Domain};n='Domain'},
        @{e={$_.trustee.SIDString};n='SID'}
      }
    }
  }
# если переменная $path не передана, то сведения о сетевых папках передаётся в вызывющую
# функцию для последующей обработки, в частности добавления и удаления ACE из ACL
# списка сетевой папки

  if ($path -eq $null) {
  $shareinfo} else {
# собственно сам экспорт содержимого $ShareInfo в CSV файл
  $ShareInfo | select Name, Path, Description, User, Domain, SID, AccessMask, AceFlags, AceType | export-csv -noType $path
# если указан путь к CSV файлу, то после экспорта данных в файл проверяется, что файл действительно был создан
    if (Test-Path $path) {Write-Host "Выполнено!"} else {
    Write-Warning "Не удалось создать файл $path. Возможно у вас не хватает прав или путь недоступен."}
}}

# внутренняя функция для записи уже сформированной переменной $ShareInfo в ACL сетевой папки
function _ShareUtils_WriteShare ($ShareInfo, $shares, $param) {
$ShareInfo | select -unique name, Path, Description | ForEach-Object {
  $name = $_.name
  $path = $_.Path
  $description = $_.Description
  $SD = ([WMIClass] "Win32_SecurityDescriptor").CreateInstance()
  $ace = ([WMIClass] "Win32_Ace").CreateInstance()
  $Trustee = ([WMIClass] "Win32_Trustee").CreateInstance()
  $sd.DACL = @()
  $ShareInfo | where {$_.name -eq $name} | ForEach-Object {
    $SID = new-object security.principal.securityidentifier($_.SID)
    [byte[]] $SIDArray = ,0 * $SID.BinaryLength
    $SID.GetBinaryForm($SIDArray,0)
    $Trustee.Name = $_.user
    $Trustee.SID = $SIDArray
    $ace.AccessMask = $_.AccessMask
    $ace.AceType = $_.AceType
    $ace.AceFlags = $_.AceFlags
    $ace.trustee = $Trustee
    $sd.DACL += $ACE.psObject.baseobject
  }
# здесь проверяется наличие промежуточного параметра $param, который после сборки определяет
# тип записи. Если $param пустой, то производится запись только в конкретную сетевую папку. Если
# же $param не пустой, то при записи и отсутствии сетевой папки она будет создана и в неё будут записаны
# данные из CSV файла. Конструкция после Else используется только при импорте данных о сетевых папках
# включая Access из заранее подготовленного CSV файла.
if ($param -eq $null) {
$inParams = $shares.psbase.GetMethodParameters("SetShareInfo")
$inParams.Access = $SD
$write = $shares.psbase.invokemethod("setshareinfo", $inParams, $null)
Write-Host "Запись DACL сетевой папки:"
_ShareUtils_Get-Code $write}
else {
$shares = ([WMIClass] "Win32_Share")
$inParams = $shares.psbase.GetMethodParameters("Create")
$inParams["Name"] = $_.name
$inParams["Type"] = 0
$inParams["Path"] = $_.Path
$inParams["Description"] = $_.Description
$inParams["Access"] = $SD.PsObject.BaseObject
$write = $shares.psbase.invokemethod("Create", $inParams, $null)
Write-Host "Обработка сетевой папки $name по пути $path:"
_ShareUtils_Get-Code $write
}}}

# основная функция для импорта данных о сетевых папках из CSV файла. Переменная $path
# должна содержать путь к CSV файлу. Внутри функции проверяется, чтобы был указан
# верный путь к CSV файлу.

function Import-ShareInfo ($path) {
if (Test-Path $path) {
$param = "param"
$ShareInfo = Import-Csv $path
_ShareUtils_WriteShare $ShareInfo -param $param}
Else {Write-Warning "путь к CSV файлу указан неверный!"
}}

# основная функция для компоновки объекта $AddInfo параметрами безопасности, которые
# включают в себя как имя сетевой папки, имени пользователя, который должен иметь к ней
# доступ, и типах доступа, как чтение/запись и действие разрешено/запрещено. Переменная
# $param определяет действие с готовым объектом - отправить на запись сразу (при этом все
# существующие разрешения будут удалены и заменены только данными из текущего объекта)
# или вернуть обратно в вызывющую функцию, для присоединения этого объекта к уже имеющимся,
# для окончательной компоновки объекта с полным списком ACL.
function Set-SharePermission ($name, $user, $AceType, $AccessMask, $param) {
$shares = gwmi Win32_share -Filter "name = '$name'"
if ($shares -eq $null) {Write-Warning "Указанная сетевая шара не найдена"}
else {
# здесь я использовал хэш-таблицы для преобразования текстовых значений маски и типа
# доступа, которые вводит пользователь в числовые значения, которые затем транслируются и
# и помещаются в текущий объект с параметрами безопасности.
$masks = @{FullControl = 2032127; Change = 1245631; Read = 1179817}
$types = @{Allow = 0; Deny = 1}
$AddInfo = New-Object System.Management.Automation.PSObject
# здесь происходит инициализация свойств объекта. Значение каждого параметра приравнял к $null
# для того, чтобы при отсутствии каких-либо данных они либо оставались пустыми, либо заполнялись
# системой автоматически.
$AddInfo | Add-Member NoteProperty Name  ([PSObject]$null)
$AddInfo | Add-Member NoteProperty Path  ([PSObject]$null)
$AddInfo | Add-Member NoteProperty Description  ([PSObject]$null)
$AddInfo | Add-Member NoteProperty AccessMask  ([uint32]$null)
$AddInfo | Add-Member NoteProperty AceFlags  ([uint32]$null)
$AddInfo | Add-Member NoteProperty AceType  ([uint32]$null)
$AddInfo | Add-Member NoteProperty User  ([PSObject]$null)
$AddInfo | Add-Member NoteProperty Domain  ([PSObject]$null)
$AddInfo | Add-Member NoteProperty SID  ([PSObject]$null)
# собственно заполнение свойств созданного объекта данными, которые были переданы из вызывющей
# функции.
$AddInfo.Name = $name
$AddInfo.Path = $shares.Path
$AddInfo.Description = $Shares.Description
$AddInfo.User = $user
$AddInfo.SID = (new-object security.principal.ntaccount $user).translate([security.principal.securityidentifier])
$AddInfo.AccessMask = $masks.$AccessMask
$AddInfo.AceType = $types.$AceType
# тут так же использовалась временная переменная $param, которая определяет дальнейшее действие
# с данным объектом - отправка объекта на запись (только при использовании функции Set-SharePermission),
# либо возврат в вызываемую функцию (только при использовании функции Add-SharePermission).
if ($param -ne $null) {
$AddInfo} else {
_ShareUtils_WriteShare $AddInfo $shares
}}}

# основная функция для добавления участников безопасности к имеющимуся списку ACL. Данная функция
# сперва использует функцию экспорта для извлечения сведений об указанной сетевой папке, после чего
# вызывается функция Set-SharePermission в качестве промежуточной функции, т.к. в неё передаётся перменная
# $param, то вызываемая функция не будет записывать новый ACL, а вернёт скомпонованный объект $AddInfo.

function Add-SharePermission ($name, $user, $AceType, $AccessMask) {
$shares = gwmi Win32_share -Filter "name = '$name'"
if ($shares -eq $null) {Write-Warning "Указанная сетевая шара не найдена"}
else {
# здесь нужно быть внимательным, т.к. нужно обязательно использовать обозначение массива @() для того,
# чтобы переменная $ShareInfo смогла бы содержать массив объектов с параметрами безопасности. Один объект
# содержит один ACE для каждого пользователя/группы. Если не использовать обозначение массива, то данная
# переменная сможет содержать только один объект (т.е. только одного участника безопасности).
$ShareInfo = @(Export-ShareInfo -name $name)
$param = "param"
$ShareInfoNew = Set-SharePermission $name $user $AceType $AccessMask $param
# вот здесь происходит присоединение с нуля созданного объекта (ACE) к имеющемуся массиву текущих
# ACE. Таким образом мы можем добавлять участников безопасности к ACL сетевой папки без удаления
# текущих ACE.
$ShareInfo += $ShareInfoNew
_ShareUtils_WriteShare $ShareInfo $shares
}}

# основная функция для удаления единичного ACE из ACL сетевой папки. Процесс сводится к извлечению
# текущего списка ACL и фильтрации ACE в этом списке по методу Not Equal. Всё, что не подпадает под
# это действие записываются обратно в переменную, а всё, что подпало (указанный пользователь) обратно
# в переменную $ShareInfo не записывается.
function Remove-SharePermission ($name, $user) {
$shares = gwmi Win32_share -Filter "name = '$name'"
if ($shares -eq $null) {Write-Warning "Указанная сетевая шара не найдена"}
else {
$ShareInfo = Export-ShareInfo -name $name
$ShareInfo = $shareInfo | where {$_.name -eq "$name" -and $_.user -ne "$user"}
_ShareUtils_WriteShare $ShareInfo $shares}}

# основная функция для создания новых сетевых папок на локальном компьютере. Здесь я использую упрощённый
# вариант создания сетевой папки, но учитывая один большой нюанс я добавил одно действие. Суть проблемы
# изложена тут:
http://vpodans.spaces.live.com/blog/cns!BB1419A2CFC1E008!170.entry
# поэтому при создании новой сетевой папки я вручную создаю с нуля список ACL, который содержит
# только группу Everyone и с правом Allow Read.

function New-Share ($name, $path, $Description) {
$Share = ([wmiClass] 'Win32_share').Create($path, $name, 0, $null, $Description)
Write-Host "Создание сетевой шары $name :"
$Return = _ShareUtils_Get-Code $share
$Return
if ($Return -eq "Успешно") {
# для использования скрипта в мультиязычных системах без лишних правок в скрипте я вместо именования
# группы Everyone я использовал трансляцию её уникального для всех систем SID в строковое значение,
# которое может отличаться в зависимости от языка системы.
$user = (new-object security.principal.securityidentifier "S-1-1-0").translate([security.principal.ntaccount])
Set-SharePermission $name $user.value "Allow" "Read"}
}

# основная функция для удаления сетевой шары (равносильно Stop Sharing в консоли Shares). Сама папка
# и её содержимое не удаляется.
function Remove-Share ($name) {
$share = gwmi Win32_share -Filter "name = '$name'"
if ($share -eq $null) {Write-Warning "Указанная сетевая шара не найдена"} else {
$share.delete($null)
Write-Host "Удаление сетевой шары $name :"
_ShareUtils_Get-Code $share
}}

# функция, которая возвращает на экран пользователю список всех сетевых папок на локальном компьютере.
# можно так же получить сведения только об одной сетевой папке, которую нужно указать при вызове.
function Get-Share ($name) {
if ($name -eq $null) {
gwmi Win32_Share -Filter 'type = 0' | fl}
else {
gwmi Win32_Share -Filter "name='$name'" | fl}
}

# основная функция для вывода на экран сведений о безопасности (содержимого списка ACL) как для всех
# сетевых папок (если вызывается функция без параметров), так и для конкретной сетевой папки. Т.к. маски и типы
# доступа приводятся в числовых значениях после вывода сведений выводится краткая справка по трансляции
# данных значений. Считаю, что нету смысла писать транслятор, который перед выводом информации на экран
# данных сам автоматически переводил бы в понятные текстовые значения.
function Get-SharePermission ($name) {
Export-ShareInfo -name $name | select name, user, AccessMask, AceType | ft -a -group name
Write-Host "Данные колонки AccessMask имеют следующие значения:
2032127 - FullControl
1245631 - Change
1179817 - Read `n
Данные колонки AceType имеют следующие значения:
0 - Allow
1 - Deny
2- SystemAudit (группы Administrators и System имеют право Allow FullControl" -foregroundcolor "Yellow"}

Вот так это всё выглядит. На первый взгляд много и страшно, но если прочитать все предыдушие статьи по данной теме, то данный код уже будет обретать некий смысл. На этом я предлагаю поставить жирную точку в вопросе управления сетевыми папками и безопасностью (Share Permissions) сетевых папок в PowerShell.

И на последок добавлю, что принимаются любые замечания, поправки предложения по данному скрипту, т.к. это лишь мой первый пробный многофункциональный скрипт по ACL и что-то может быть упущено или неоптимизировано. Вобщем, если есть что сказать, то отписывайтесь в комментариях.

2008/7/8

Управление безопасностью общих папок (сетевых шар) в PowerShell (часть 3)

В предыдущей части мы рассмотрели чтение Share Permissions, их редактирование и удаление ACE из полного списка ACL. Здесь осталось рассмотреть вопрос добавления участников безопасности в DACL сетевой шары. Этот процесс, к сожалению, не такой и простой, как может показаться, но тем не менее его тоже нужно решать. Для решения этой задачи нам нужно создать такой же объект с такими же свойствами как и содержимое $ShareInfo. Давайте посмотрим, какими свойствами обладают элементы массива $ShareInfo:

[C:\] $shareinfo | Get-Member

   TypeName: System.Management.Automation.PSCustomObject

Name           MemberType   Definition
----              ----------         ----------
Equals           Method          System.Boolean Equals(Object obj)
GetHashCode Method          System.Int32 GetHashCode()
GetType         Method         System.Type GetType()
ToString         Method         System.String ToString()
AccessMask  NoteProperty   System.UInt32 AccessMask=1245631
AceFlags       NoteProperty   System.UInt32 AceFlags=0
AceType       NoteProperty   System.UInt32 AceType=0
Description   NoteProperty   System.Management.Automation.PSObject
Domain        NoteProperty   System.Management.Automation.PSObject
Name           NoteProperty   System.Management.Automation.PSObject
Path             NoteProperty   System.Management.Automation.PSObject
SID              NoteProperty   System.Management.Automation.PSObject
User             NoteProperty   System.Management.Automation.PSObject

Нас тут будут интересовать только NoteProperty. Давайте теперь исходя из этих данных создадим свой объект, который будет обладать вот этими свойствами. Тип объекта будет такой же - System.Management.Automation.PSObject  (без Custom). Новый объект создаётся командной New-Object, а члены объекта создаются командой Add-Member:

# создаём новый объект с типом System.Management.Automation.PSObject
$AddInfo = new-object System.Management.Automation.PSObject
# добавляем по очереди членов NoteProperty объекта как и в исходном варианте
$AddInfo | add-member NoteProperty Name  ([PSObject])
$AddInfo | add-member NoteProperty Path  ([PSObject])
$AddInfo | add-member NoteProperty Description  ([PSObject])
$AddInfo | add-member NoteProperty AccessMask  ([uint32])
$AddInfo | add-member NoteProperty AceFlags  ([uint32])
$AddInfo | add-member NoteProperty AceType  ([uint32])
$AddInfo | add-member NoteProperty User  ([PSObject])
$AddInfo | add-member NoteProperty Domain  ([PSObject])
$AddInfo | add-member NoteProperty SID  ([PSObject])

Теперь можно посмотреть на результаты нашей работы:

[C:\] $AddInfo

Name           :
Path             :
Description   :
AccessMask  : 0
AceFlags       : 0
AceType       : 0
User             :
Domain        :
SID              :

Ну что ж, уже лучше, теперь можно заполнять эти поля в соответствии с нашими требованиями. Но чтобы не заполнять все поля вручную, предполагается, что для добавления нового участника безопасности в Share Permissions пользователь укажет только имя сетевой шары, имя пользователя, маску доступа и тип доступа (Allow/Deny). Поэтому нам потребуется вытащить информацию о текущей шаре (которая общая для всей шары, как имя, путь, описание и т.д.) и передать эти значения в новую переменную $AddInfo. Остальную часть информации мы обработаем на основании уже переданных параметров и запишем оставшиеся поля переменной $AddInfo, которая в конечном итоге будет содержать всю необходимую информацию. Т.к. уже неоднократно говорилось в предыдущих частях, что метод SetShareInfo перезаписывает полностью информацию о сетевой шаре, поэтому для сохранения существующих ACE мы произведём уже известным способом чтение существующих DACL и сделаем инкремент (добавим к существующим DACL нами созданный DACL). Когда все данные будут скомпонованы мы произведём запись обновлённого списка DACL в ACL шары. Итак, поехали:

# принимаем вводные параметры от пользователя из командной строки
param ($share, $user, $AccessMask, $AceType)
# предполагается, что данный скрипт выполняет только добавление участников безопасности
# поэтому сразу создаём новый объект с необходимыми членами и указанием типа принимаемых
# данных
$AddInfo = New-Object System.Management.Automation.PSObject
$AddInfo | Add-Member NoteProperty Name  ([PSObject])
$AddInfo | Add-Member NoteProperty Path  ([PSObject])
$AddInfo | Add-Member NoteProperty Description  ([PSObject])
$AddInfo | Add-Member NoteProperty AccessMask  ([uint32])
$AddInfo | Add-Member NoteProperty AceFlags  ([uint32])
$AddInfo | Add-Member NoteProperty AceType  ([uint32])
$AddInfo | Add-Member NoteProperty User  ([PSObject])
$AddInfo | Add-Member NoteProperty Domain  ([PSObject])
$AddInfo | Add-Member NoteProperty SID  ([PSObject])
# зная имя сетевой шары делаем её поиск и копируем информацию о имени, пути
# и описании (поле Description) и выставим прочие параметры
$CustomShare = Get-WmiObject Win32_Share -filter "name = '$share'"
$AddInfo.Name = $share
$AddInfo.Path = $CustomShare.Path
$AddInfo.Description = $CustomShare.Description
$AddInfo.Domain = $null
$AddInfo.AceFlags = 3
# далее заполняется информация о пользователе и его доступе, поэтому
# дальше мы ничего не копируем, а обрабатываем уже переданные параметры:

$AddInfo.User = $user
# преобразовываем маску доступа из текстовой в численный формат с использованием
# конструкции Switch.

switch ($AccessMask) {
  "Full"   {$AddInfo.AccessMask = 2032127}
  "Change" {$AddInfo.AccessMask = 1245631}
  "Read"   {$AddInfo.AccessMask = 1179817}
}
# таким же образом обрабатываем и переменную $AceType:
switch ($AceType) {
  "Allow"   {$AddInfo.AceType = 0}
  "Deny" {$AddInfo.AceType = 1}
}
# заполняем последнее поле SID путём трансляции имени пользователя/группы в его SID
$AddInfo.SID = (new-object security.principal.ntaccount $user).translate([security.principal.securityidentifier])
# теперь считываем текущий список DACL с указанной сетевой шары в переменную $ShareInfo
$shares = Get-WmiObject Win32_Share -filter "name='$share'"
$Shareinfo = @()
foreach ($share in $shares) {
  $shareSec = Get-WmiObject Win32_LogicalShareSecuritySetting  -filter "name='$($share.name)'"
  if($shareSec) {
    $sd = $sharesec.GetSecurityDescriptor()
    $ShareInfo += $sd.Descriptor.DACL | ForEach-Object {
      $_ | select @{e={$share.name};n='Name'},
        @{e={$share.Path};n='Path'},
        @{e={$share.Description};n='Description'},
        AccessMask,
        AceFlags,
        AceType,
        @{e={$_.trustee.Name};n='User'},
        @{e={$_.trustee.Domain};n='Domain'},
        @{e={$_.trustee.SIDString};n='SID'}
    }
  }
}
# теперь делаем добавление (инкремент) созданного нами массива объектов с зполненными полями к
# существующему массиву объектов $ShareInfo

$ShareInfo += $AddInfo
# Можно для верности убедиться, что $ShareInfo обладает всей необходимой информацией, которую
# теперь можно записать в шару. В принципе, эта строчка несёт в себе лишь отладочную информацию
# и когда отладка будет завершена эту строчку можно будет удалить или закомментировать для
# отладки скрипта в будущем.

$ShareInfo
# Если всё в порядке, то можно перезаписывать эти данные в DACL указанной шары:
$ShareInfo | select -unique name, Path, Description | ForEach-Object {
  $name = $_.name
  $path = $_.Path
  $description = $_.Description
  "Processing : $name $path $description"
  $SD = ([WMIClass] "Win32_SecurityDescriptor").CreateInstance()
  $ace = ([WMIClass] "Win32_Ace").CreateInstance()
  $Trustee = ([WMIClass] "Win32_Trustee").CreateInstance()
  $sd.DACL = @()
  $ShareInfo | where {$_.name -eq $name} | ForEach-Object {
    $SID = new-object security.principal.securityidentifier($_.SID)
    [byte[]] $SIDArray = ,0 * $SID.BinaryLength
    $SID.GetBinaryForm($SIDArray,0)
    $Trustee.Name = $_.user
    $Trustee.SID = $SIDArray
    $ace.AccessMask = $_.AccessMask
    $ace.AceType = $_.AceType
    $ace.AceFlags = $_.AceFlags
    $ace.trustee = $Trustee
    $sd.DACL += $ACE.psObject.baseobject
  }
$inParams = $CustomShare.psbase.GetMethodParameters("SetShareInfo")
$inParams.Access = $SD
$CustomShare.psbase.invokemethod("setshareinfo", $inParams, $null)
}

Формат запуска данного скрипта из командной строки CMD или меню Run будет следующим:

powershell %path%\AddUser.ps1 -share "Имя сетевой шары" -user "имя добавляемой группы" -mask "маска доступа, Full/Change/Read" -type "тип доступа, Allow/Deny"

эту команду следует выполнять в одну строчку. В качестве переменной %path% нужно указать путь к папке со скриптом, если он заранее не добавлен в системную переменную %path%. В качестве маски доступа нужно указать одно из 3-х значений, которое может быть Full, Change или Read. Более одного параметра указывать нельзя. Ну и в качестве типа доступа указать либо Allow, что даст доступ, либо Deny, что явно запретит доступ.

Ну вот как бы и всё на данном этапе. Здесь я много чего не пояснял, т.к. достаточно (в моём понимании) разобрал в предыдущих 3-х частях о работе с сетевыми шарами в PowerShell. Но это ещё не всё. Главный девиз PowerShell - быть удобным для использования и кратким для написания (это я сам придумал :) ), однако при работе с сетевыми шарами это совершенно не прослеживается и даже может создаться впечатление громоздкости (хотя тот, кто считает этот код громоздким может написать скрипт короче на VBS с использованием только WMI/.NET :) ), поэтому в следующей части я постараюсь исправить сей момент путём написания единого (относительно компактного по возможности) и представить его как готовое решение, которое в работе будет действительно удобным. Не отключайтесь, продолжение обязательно будет :)

p.s. Данный скрипт не обязательно является самым простым решением, т.к. возможно, что данную операцию можно провести более удобным и изящным способом (хотя, после чтения документации на MSDN мне так не кажется, что это возможно), но в любом случае адекватные замечания/поправки/дополнения к этому скрипту всячески приветствуются.

2008/7/7

Управление безопасностью общих папок (сетевых шар) в PowerShell (часть 2)

В первой части мы рассмотрели использование PowerShell для добавления DACL (Discretionary Access Control List) к сетевой шаре как для создания списка ACL и получения возможности в дальнейшем работать с этим списком из PowerShell. Данный метод полностью заменяет текущий список DACL сетевой папки, т.е. все текущие участники безопасности будут удалены и заменены теми, которые были указаны в теле скрипта. Теперь стоит поговорить об извлечении и чтении DACL для сетевых папок как для анализа, так и для выборочного добавления/удаления участников безопасности в списке ACL. Класс Win32_Share имеет лишь один метод для добавления ACL списка - SetShareInfo, который полностью заменяет текущий список DACL и не имеет нативного метода добавления частичного DACL к имеющемуся списку DACL. Поэтому для добавления или удаления участников безопасности из этого списка нам потребуется сначала считать этот список, проанализировать, внести необходимые изменения и записать этот список с внесёнными изменениями в ACL сетевой папки.

Процесс извлечения DACL из ACL сетевой шары будет происходить примерно в обратном порядке записи. В предыдущей части я говорил про процесс упаковывания ("инкапсуляции") массива в массив, и сейчас нам придётся обратно расшивать (извлекать) массивы из массивов. Как уже говорилось в первой части, для чтения DACL сетевой папки используется класс Win32_LogicalShareSecuritySettings:

Get-WmiObject Win32_LogicalShareSecuritySetting | Format-List [a-z]*

Так мы получим все ShareSecuritySettnigs для всех расшаренных папок. Давайте отфильтруем только ту, конкретно с которой будем работать и выберем её в переменную:

$ShareSec = Get-WmiObject Win32_LogicalShareSecuritySetting  -filter "name='UserShare'" | Format-List [a-z]*

Если теперь теперь посмотреть содержимое переменной, то ничего интересного там мы не увидим:

Caption      : Security settings of UserShare
ControlFlags : 32772
Description  : Security settings of UserShare
Name         : UserShare
SettingID    :

Давайте покопаем поглубже. В процессе создания DACL для шары мы записывали уже готовый SecurityDescriptor, в котором хранится DACL и внутри которого уже хранятся ACE и Trustee - вот эти объекты нам и нужны. Для извлечения этого класс Win32_LogicalShareSecuritySettings имеет метод GetSecurityDescriptor

$SD = $ShareSec.GetSecurityDescriptor() | Format-List [a-z]*

И мы увидим две строчки (я специально отфильтровал вывод ненужной для нас в данный момент информации, которая начинается с символа подчёркивания "_"):

Descriptor       : System.Management.ManagementBaseObject
ReturnValue      : 0

Вот здесь нас будет интересовать параметр Descriptor:

[C:\]$SD.Descriptor


ControlFlags : 32772
DACL         : {System.Management.ManagementBaseObject, System.Management.ManagementBaseObject, System.Management.ManagementBaseObject}
Group        : System.Management.ManagementBaseObject
Owner        : System.Management.ManagementBaseObject
SACL         :

Вот мы уже добрались до DACL:

[C:\]$SD.Descriptor.DACL | Format-List [a-z]*

AccessMask              : 1179817
AceFlags                   : 0
AceType                   : 0
GuidInheritedObjectType :
GuidObjectType          :
Trustee                     : System.Management.ManagementBaseObject

AccessMask              : 1245631
AceFlags                   : 0
AceType                   : 0
GuidInheritedObjectType :
GuidObjectType          :
Trustee                     : System.Management.ManagementBaseObject

AccessMask              : 2032127
AceFlags                   : 0
AceType                   : 0
GuidInheritedObjectType :
GuidObjectType          :
Trustee                     : System.Management.ManagementBaseObject

Вот здесь мы видим 3 ACE с разными AccessMask. Но имён почему-то не видно..а не видно, потому что имена участников безопасности запрятаны ещё глубже, а именно в Trustee (вы помните, как мы записывали имя участника безопасности в класс Trustee). Давайте посмотрим первый элемент:

[C:\]$SD.Descriptor.DACL[0].Trustee

Domain    :
Name      : Everyone
SID       : {1, 1, 0, 0...}
SidLength : 12
SIDString : S-1-1-0

И команда нам вернёт имя пользователя. У меня это Everyone. AccessMask = 1179817 означает Read&Execute + Synchronize. Узнать это очень легко, достаточно набрать в консоли:

[C:\] [System.Security.AccessControl.FileSystemRights]1179817
ReadAndExecute, Synchronize
[C:\] [System.Security.AccessControl.FileSystemRights]1245631
Modify, Synchronize
[C:\] [System.Security.AccessControl.FileSystemRights]2032127
FullControl

Теперь посмотрим имя второго участника безопасности, который имеет маску доступа 1245631 (Change):

[C:\]$SD.Descriptor.DACL[1].Trustee

Domain    : CONTOSO
Name      : Domain Users
SID       : {1, 5, 0, 0...}
SidLength : 28
SIDString : S-1-5-21-3321262099-2632712065-3158606685-513

И т.д. можно перебирать все элементы массива. Давайте для начала выведем весь этот список в CSV. Здесь я не буду изобретать велосипед, а уже буду использовать готовое решение от /\/\o\/\/:

$filename = 'C:\ShareInfo.csv'
$shares = Get-WmiObject Win32_Share -filter 'type=0'
$Shareinfo = @()
foreach ($share in $shares) {
  $shareSec = gwmi Win32_LogicalShareSecuritySetting  -filter "name='$($share.name)'"
  if($shareSec) {
    $sd = $sharesec.GetSecurityDescriptor()
    $ShareInfo += $SD.Descriptor.DACL |% {
      $_ | select @{e={$share.name};n='Name'},
        @{e={$share.Path};n='Path'},
        @{e={$share.Description};n='Description'},
        AccessMask,
        AceFlags,
        AceType,
        @{e={$_.trustee.Name};n='User'},
        @{e={$_.trustee.Domain};n='Domain'},
        @{e={$_.trustee.SIDString};n='SID'}
    }
  }
}
$ShareInfo | select Name,Path,Description,User,Domain,SID, AccessMask,AceFlags,AceType | export-csv -noType $filename

На выходе мы получим CSV файл, где в простеньком (но вполне удобочитаемом) формате есть вся информация о имеющихся сетевых шарах на локальном компьютере. К слову говоря, этот CSV файл потом можно будет импортировать обратно для восстановления Share Permissions и другой информации о шарах. Давайте посмотрим на содержимое переменной $ShareInfo:

Name           : UserShare
Path             : C:\Test
Description   :
AccessMask  : 1179817
AceFlags       : 0
AceType       : 0
User             : Everyone
Domain        :
SID              : S-1-1-0

Name           : UserShare
Path             : C:\Test
Description   :
AccessMask  : 1245631
AceFlags       : 0
AceType       : 0
User             : Domain Users
Domain        : CONTOSO
SID              : S-1-5-21-3321262099-2632712065-3158606685-513

Name           : UserShare
Path             : C:\Test
Description   :
AccessMask  : 2032127
AceFlags       : 0
AceType       : 0
User             : Administrators
Domain        : BUILTIN
SID              : S-1-5-32-544

Name           : CertEnroll
Path             : C:\WINDOWS\system32\CertSrv\CertEnroll
Description   : Certificate Services share
AccessMask  : 1179817
AceFlags       : 3
AceType       : 0
User            : Everyone
Domain        :
SID              : S-1-1-0

<...>

Остальное я не стал показывать, т.к. будет содержать идентичную информацию для остальных ресурсов. Обладая этой информацией мы можем гибко изменять наши настройки, например добавлять или удалять выборочно единичных участников безопасности. Давайте начнём с удаления. К примеру, удалим из ACL шары UserShare группу Everyone. Не уверен, что этот метод является самым правильным, но мы сделаем переприсвоение массива с фильтрацией по имени шары и имени пользователя, которого будем удалять:

$ShareInfo = $shareInfo | where {$_.name -eq "UserShare" -and $_.user -ne "Everyone"}

Теперь можем посмотреть содержимое переменной $ShareInfo:

Name           : UserShare
Path             : C:\Test
Description   :
AccessMask  : 1245631
AceFlags       : 0
AceType       : 0
User             : Domain Users
Domain        : CONTOSO
SID              : S-1-5-21-3321262099-2632712065-3158606685-513

Name           : UserShare
Path             : C:\Test
Description   :
AccessMask  : 2032127
AceFlags       : 0
AceType       : 0
User             : Administrators
Domain        : BUILTIN
SID              : S-1-5-32-544

Как мы видим, переменная $ShareInfo уже не содержит ACE группы Everyone. Таким образом можно фильтровать нужные элементы массива по любым критериям, как группа пользователей, имя сетевой шары, тип доступа и т.д. Теперь можно импортировать эту переменную в нашу шару. Для импорта этой информации воспользуемся тем же методом, каким мы создавали Share Permissions с нуля. Разница будет лишь в том, что для создания нужных объектов (как User, SID, AccessMask) мы будем использовать значения из переменной $ShareInfo:

$ShareInfo | select -unique name, Path, Description | ForEach-Object {
  $name = $_.name
  $path = $_.Path
  $description = $_.Description
  "Processing : $name $path $description"
  $SD = ([WMIClass] "Win32_SecurityDescriptor").CreateInstance()
  $ace = ([WMIClass] "Win32_Ace").CreateInstance()
  $Trustee = ([WMIClass] "Win32_Trustee").CreateInstance()
  $SD.DACL = @()
  $ShareInfo | where {$_.name -eq $name} | Froeach-Object {
    $SID = new-object security.principal.securityidentifier($_.SID)
    [byte[]] $SIDArray = ,0 * $SID.BinaryLength
    $SID.GetBinaryForm($SIDArray,0)
    $Trustee.Name = $_.user
    $Trustee.SID = $SIDArray
    $ace.AccessMask = $_.AccessMask
    $ace.AceType = $_.AceType
    $ace.AceFlags = $_.AceFlags
    $ace.trustee = $Trustee
    $SD.DACL += $ACE.psObject.baseobject
  }
$Share = Get-WmiObject win32_share -filter "name='UserShare'"
$inParams = $share.psbase.GetMethodParameters("SetShareInfo")
$inParams.Access = $SD
$share.psbase.invokemethod("SetShareInfo", $inParams, $null)
}

Если посмотреть на скрипт из предыдущей статьи, то можно проследить практически идентичную процедуру записи новых SharePermissions. Вот так мы получили готовый шаблон для изменения SharePermissions, который включает 3 этапа:

  1. чтение текущих SharePermissions (и прочей информации) в переменную;
  2. изменение необходимых параметров внутри переменной;
  3. запись новой переменной с обновлёнными Share Permissions обратно в ACL сетевой папки.

Кстати говоря, для изменения одного конкретного ACE (без удаления) можно использовать следующий метод. Он заключается в определении местоположения необходимого элмента в массиве и редактировании конкретно этого элемента. Делается это следующим образом, пишем функцию (за функцию отдельное спасибо Васе Гусеву):

function findinarr ($array, $value) {for ($i=0; $i -lt $array.count;$i++){if($array[$i].user -eq $value){$i}}}

Теперь определяем порядковый номер элемента в массиве, который содержит группу Domain Users и вместо Change дадим этой группе право только Read:

[C:\] $ShareInfo = $ShareInfo | where {$_.name -eq "UserShare"}
[C:\] $ShareInfo

Name           : UserShare
Path             : C:\Test
Description   :
AccessMask  : 1245631
AceFlags       : 0
AceType       : 0
User             : Domain Users
Domain        : CONTOSO
SID              : S-1-5-21-3321262099-2632712065-3158606685-513

Name           : UserShare
Path             : C:\Test
Description   :
AccessMask  : 1245631
AceFlags       : 0
AceType       : 0
User             : Administrators
Domain        : BUILTIN
SID              : S-1-5-32-544

[C:\] function findinarr ($array, $value) {for ($i=0; $i -lt $array.count;$i++){if($array[$i].user -eq $value){$i}}}
[C:\] $count = findinarr $shareInfo "Domain Users"
[C:\] $ShareInfo[$count].AccessMask = 1179817
[C:\] $ShareInfo

Name           : UserShare
Path             : C:\Test
Description   :
AccessMask  : 1179817
AceFlags       : 0
AceType       : 0
User             : Domain Users
Domain        : CONTOSO
SID              : S-1-5-21-3321262099-2632712065-3158606685-513

Name           : UserShare
Path             : C:\Test
Description   :
AccessMask  : 1245631
AceFlags       : 0
AceType       : 0
User             : Administrators
Domain        : BUILTIN
SID              : S-1-5-32-544

Вот теперь Группа Domain Users имеет маску доступа не Change, а Read. Теперь осталось только записать изменённые данные в ACL шары по вышеприведённому примеру, где мы удаляли группу Everyone из ACL.

В этой части мы рассмотрели чтение информации о сетевых шарах, в частности чтение Share Permissions, экспорт этих данных в CSV файл, а так же рассмотрели вопросы удаления единичных ACE из списка ACL и их редактирования. В следующей части я расскажу, как добавлять участников безопасности к существующему списку ACL и в качестве итогового резюме мы приведём в порядок весь материал, который изучили, чтобы его можно было удобно использовать в производственной среде. Так что продолжение следует.

2008/7/4

Управление безопасностью общих папок (сетевых шар) в PowerShell (часть 1)

В предыдущей статье я сделал вводну часть по управлению сетевыми папками в PowerShell. В ней я не рассматривал вопрос управления Share Permissions, т.к. это дело весьма непростое. Итак, что нам для этого потребуется? А потребуются нам следующие классы WMI:

Основной класс, который позволит нам извлечь параметры безопасности - Win32_LogicalShareSecuritySettings. Как я говорил в предыдущей статье, упрощённый вариант создания новой сетевой папки - не самый лучшй вариант создания для последующего управления. Сейчас я продемонстрирую вам проблему:

[C:\] ([wmiClass] 'Win32_share').Create("C:\Test", "UserShare", "0", "100", "Network share for users")

__GENUS                 : 2
__CLASS                  : __PARAMETERS
__SUPERCLASS        :
__DYNASTY              : __PARAMETERS
__RELPATH               :
__PROPERTY_COUNT: 1
__DERIVATION         : {}
__SERVER                :
__NAMESPACE          :
__PATH                    :
ReturnValue            : 0

[C:\] Get-WmiObject Win32_Share

Name                                 Path                                                           Description
----                                    ----                                                             -----------
C$                                     C:\                                                              Default share
UserShare                        C:\Test                                                       Network share for users
CertEnroll                           C:\WINDOWS\system32\CertSrv\CertEnroll    Certificate Services share
IPC$                                                                                                     Remote IPC
ADMIN$                              C:\WINDOWS                                               Remote Admin
SYSVOL                              C:\WINDOWS\SYSVOL\sysvol                        Logon server share
NETLOGON                          C:\WINDOWS\SYSVOL\sysvol\contoso.com...  Logon server share

[C:\] Get-WmiObject Win32_LogicalShareSecuritySetting | Format-List [a-z]*

Caption        : Security settings of CertEnroll
ControlFlags : 32772
Description   : Security settings of CertEnroll
Name           : CertEnroll
SettingID      :

Caption        : Security settings of SYSVOL
ControlFlags : 32772
Description   : Security settings of SYSVOL
Name           : SYSVOL
SettingID      :

Caption        : Security settings of NETLOGON
ControlFlags : 32772
Description   : Security settings of NETLOGON
Name           : NETLOGON
SettingID      :

Итак, первой строчкой мы создали новую шару. В выводе команды я выделил последнюю строчку ReturnValue и его значение 0. Ноль нам говорит, что шара создалась успешно. Чтобы в этом убедиться в следующей строчке я ввёл команду, которая показывает список всех расшаренных папок на локальной машине. Я так же выделил жирным строчку, которая показывает нашу шару. Третей командой я вывел параметры безопасности всех доступных сетевых ресурсов на локальной машине. И что мы видим? Точнее, чего мы не видим - а не видим мы административных шар (которые заканчиваются на знак $) и, так же, не увидели созданной нами сетевой папки UserShare. И это означает, что мы в последующем через классы WMI не сможем извлекать параметры безопасности для последующей обработки. Поэтому более правильным будет создание новой сетевой папки с явным назначением Share Permissions для неё.

Если посмотреть в описание метода Create класса Win32_Share, то там видно, что в качестве последнего параметра выступает класс Win32_SecurityDescriptor. Посмотрим, что нужно для него. Я для него нужны классы Win32_Ace и Win32_Trustee. Ну что ж, приступим к разбору. Итак, у нас есть уже шара с дефолтными правами. Чтобы создать новый набор прав нам нужно знать следующее:

  • Имя сетевой папки для которой будем изменять права сетевого доступа;
  • имя пользователя, которому будут назначены права;
  • тип доступа, который нужно назначить (FullControl, Change, Read).

Теперь нужно объявляем переменные для них:

$share="Test"
$user = "Accounting"
$mask = "Сhange
"

Теперь объявляем вышеуказанные классы WMI:

$SD = ([WMIClass] "Win32_SecurityDescriptor").CreateInstance()
$ace = ([WMIClass] "Win32_Ace").CreateInstance()
$Trustee = ([WMIClass] "Win32_Trustee").CreateInstance()

Теперь преобразовываем имя пользователя/группы в его SID. Преобразование я описывал вот в этой статье:

$SID = (new-object security.principal.ntaccount $user).translate([security.principal.securityidentifier])

В переменной $SID теперь хранится SID данного пользователя. Правда, здесь он содержится в строковом формате. Но если мы посмотрим в свойства класса Win32_Trustee, то увидим, что тип данных для свойства SID должен быть: Data type: uint8 array. Получить такой массив можно следующей конструкцией:

[byte[]] $SIDArray = ,0 * $SID.BinaryLength
$SID.GetBinaryForm($SIDArray,0)

Здесь мы объявили переменную $SIDArray с длиной равной BinaryLength. Длину BinaryLength можно узнать выполнив преобразование имени пользователя в SID, как это рассказано в ссылке приведённой выше. Вот теперь у нас переменная $SID содержит уже байтовый массив из SID пользователя/группы, который получили из $SIDArray. С этим мы закончили. Теперь эти данные можно передать в класс Win32_Trustee, который является абстрактным классом для описания пользователя или группы. Он нам потребуется для дальнейшей передачи этих данных в класс Win32_Ace.

$Trustee.Name = $user
$Trustee.SID = $SIDArray

Сам класс Win32_Trustee уже объявлен, поэтому мы просто добавляем значения свойств для этого класса.

Примечание: На первый взгляд это может показаться очень сложным материалом (мне он тоже дался не очень легко). По сути здесь присутствует множественная инкапсуляция данных и преобразование этих данных из одного типа в другой. Т.е. из исходных данных у нас есть разве что имя пользователя/группы и тип устанавливаемых прав. Если внимательно изучить ссылки, которые приведены в самом начале, то можно увидеть следующую последовательность преобразований:

  • имя пользователя/группы в SID;
  • SID в байтовый массив SIDArray;
  • передача SIDArray и имени в Win32_Trustee (первый этап т.н. "инкапсуляции");
  • Передача преобразованного типа прав доступа в класс Win32_Ace (второй этап инкапсуляции);
  • Передача готового Trustee в класс Win32_Ace (третий этап "инкапсуляции");
  • Передача сформированного ACE в класс Win32_SecurityIdentifier (четвёртый этап "инкапсуляции");
  • Передача уже сформированного SecurityDescriptor в метод SetShareInfo;
  • И применение самого метода SetShareInfo для записи параметров безопасности (прав доступа) в ACL самой шары.

Вот так выглядит общая схема последовательности действий, которая является шаблоном для данной операции.

А теперь продолжим. Теперь нам нужно преобразовать права доступа в класс FileSystemRights. Преобразование типа прав так же было ранее мною описано в первой части Управление ACL в PowerShell. Пара слов о том, чем отличаются права Read, Change и Full Control в контексте разрешений сетевого ресурса от контекста разрешений NTFS. Этим правам в NTFS сопоставляются Read + Execute, Modify и Full Control соответственно и плюс право Synchonize. Поэтому давайте запишем маску доступа в переменную $ace:

$ace.AccessMask = [System.Security.AccessControl.FileSystemRights]"Modify, Synchronize"

Ещё в начале мы объявили переменную $mode и вписали текстовое значение типа доступа. Здесь я его использовать не буду, а лишь преобразую его сразу в аналог FileSystemRights. Теперь в этот класс нужно передать тип доступа (Allow/Deny) в класс Win32_Ace. первой части Управление ACL в PowerShell я показывал команду, которая позволяет перечислять доступные значения свойств класса. Применим его снова, но в контексте AceType

[system.enum]::getnames([System.Security.AccessControl.AceType])

И в этом списке нас будут интересовать только первые 2

  • AccessAllowed;
  • AccessDenied.

Их можно указывать по порядковым номерам. Например, 0 будет означать AccessAllowed, а 1 - AccessDenied. Теперь заканчиваем второй этап и делаем третий этап нашей "инкапсуляции":

$ace.AceType = 0 
$ace.Trustee = $Trustee

Далее, следуя нашему шаблонному алгоритму нужно сформированную переменную $ace передать в класс Win32_SecurityDescriptor. Здесь нам нужно будет заполнить только свойство DACL, которое будет содержать массив параметров безопасности из переменной $ace:

$SD.DACL = $ace

Ну что ж, вот теперь у нас полностью готов объект SecurityDescriptor для применения его в качестве параметра для метода SetShareInfo. Для начала нам нужно указать шару, для которой будет применяться данный метод:

$share = get-WmiObject win32_share -filter "name='$share'"

Теперь нужно получить шаблон набора свойств для метода SetShareInfo:

$inParams = $share.PsBase.GetMethodParameters("SetShareInfo")

Так мы получили форму с необходимыми свойствами и типами данных, которые мы должны заполнить. Метод SetShareInfo использует следующий синтаксис (приведено из ссылки MSDN):

uint32 SetShareInfo(
  [in]            uint32 MaximumAllowed,
  [in, optional]  string Description,
  [in]            Win32_SecurityDescriptor Access
);

Это нам говорит, что требуется именно параметр Access, который нужно заполнить данными из класса Win32_SeceurityDescriptor. У нас уже есть переменная $SD, в которой заполнен параметр DACL. Вот теперь эту всю переменную запишем в качестве параметра Access в форму набора свойств:

$inParams.Access = $SD

Ну вот и всё. Теперь нам только осталось записать всё это в нашу сетевую папку:

$share.PsBase.InvokeMethod("setshareinfo", $inParams, $null)

Здесь я указал шару и использовал PsBase.InvokeMethod, чтобы вызвать метод SetShareInfo и после указания метода перечислил передаваемые прараметры Ну и конечный результат всех наших терзаний в едином целом:

$share="UserShare"
([wmiClass] 'Win32_share').Create("C:\Test", $share, "0", "100", "Network share for users")
$user = "Domain Users"
$mask = "Change"
$SD = ([WMIClass] "Win32_SecurityDescriptor").CreateInstance()
$ace = ([WMIClass] "Win32_Ace").CreateInstance()
$Trustee = ([WMIClass] "Win32_Trustee").CreateInstance()
$SID = (new-object security.principal.ntaccount $user).translate([security.principal.securityidentifier])
[byte[]] $SIDArray = ,0 * $SID.BinaryLength
$SID.GetBinaryForm($SIDArray,0)
$Trustee.Name = $user
$Trustee.SID = $SIDArray
$ace.AccessMask = [System.Security.AccessControl.FileSystemRights]"Modify, Synchronize"
$ace.AceType = 0 
$ace.Trustee = $Trustee
$SD.DACL = $ace
$share = get-wmiObject win32_share -filter "name='$share'"
$inParams = $share.psbase.GetMethodParameters("SetShareInfo")
$inParams.Access = $SD
$share.psbase.invokemethod("setshareinfo", $inParams, $null)

Ну и, собственно, вторая цель, которой мы добивались:

[C:\] gwmi Win32_LogicalShareSecuritySetting | fl [a-z]*

Caption          : Security settings of UserShare
ControlFlags  : 32772
Description    : Security settings of UserShare
Name             : UserShare
SettingID       :

Caption        : Security settings of CertEnroll
ControlFlags : 32772
Description   : Security settings of CertEnroll
Name           : CertEnroll
SettingID      :

Caption        : Security settings of SYSVOL
ControlFlags : 32772
Description   : Security settings of SYSVOL
Name           : SYSVOL
SettingID      :

Caption        : Security settings of NETLOGON
ControlFlags : 32772
Description   : Security settings of NETLOGON
Name           : NETLOGON
SettingID      :

 

Вот теперь мы увидели нашу шару через Win32_LogicalShareSecuritySetting и благодаря чему в последующем можно будет работать с DACL списком этого сетевого ресурса. Как говорит /\/\o\/\/ - Enjoy! :)

Кстати, хотелось бы отметить, что по этому вопросу /\/\o\/\/ в своё время проделал очень большую работу, как часть вопроса управления безопасностью объектов в Windows и значительная часть материала была написана с использованием его наработок.

Давайте, теперь поговорим о том, как добавить несколько участников безопасности. Суть процесса не изменится. Для решения данной задачи я воспользуюсь следующими приёмами:

  • циклом foreach;
  • hashtables;
  • конструкцией switch.

Если посмотреть код выше, то для добавления других пользователей/групп в Share Permissions, то нетрудно предположить, что нам нужно записать в $SD.DACL столько ACE, сколько пользователей/групп нам требуется завести. Следовательно, тут нужен цикл. Имена пользователей/групп и назначемые им права будут выбираться из hashtables. Hashtables - по сути является массивом сопоставлений. Он представляется двумя столбцами - именем параметра в первом столбце и его значением во втором. В нашем случае в качестве параметра будет выступать пользователь/группа, а его значение будет - тип предоставляемого доступа. Hashtables, на мой вгляд, хорошо описаны в книге PowerShell in Action (сегодня постараюсь не забыть прикрепить к блогу список полезной литертуры по PowerShell). Т.к. набор прав у нас будет для каждого пользователя/группы разный, поэтому я применю конструкцию Switch, которая будет сопоставлять текстовому значению типа доступа его значению в классе FileSystemRights.

Итак, давайте проведём анализ, какую часть кода нам нужно поместить в цикл для многократной обработки и получения для пользователя собственного ACE. У нас индивидуальным для каждого пользователя будет SID, Trustee, ACE. Остальное же будет универсальным (общим) для всех пользователей. Вот его и поместим в цикл foreach. Но для foreach нужно передать параметры - имя пользователя/группы и маску доступа (тип предоставляемого доступа). Для этого, как я уже говорил, нам потребуется Hashtables. Общий формат хэш-таблиц выглидт следующим образом:

<hashLiteral> = '@{' <keyExpression> '=' <pipeline> [ <separator> <keyExpression> '=' <pipeline> ] * '}'
<separator> = ';' | <newline>

Поэтому изменив предыдущий код мы заменим переменные $user и $mode на одну переменную $user с указанием имён пользователей и маски предоставляемого доступа согласно формату хэш-таблиц:

$user = @{"Everyone" = "Read"; "Administrators" = "FullControl"; "Domain Users" = "Change"}

Если после этой строки в консоли набрать $user, то мы получим нашу хэш-таблицу:

Name                           Value
----                              -----
Everyone                      Read
Domain Users               Change
Administrators               FullControl

Теперь готовим цикл foreach:

$user.keys | foreach {

Мы передали содержимое хэш-таблицы в цикл. Теперь нужно изменить несколько строк кода. А именно: мы передадим в цикл сперва имя пользователя/группы. Для этого нужно изменить строчку преобразования аккаунта, чтобы туда при каждой итерации цикла попадало новое имя пользователя:

$SID = (new-object security.principal.ntaccount $_ ).translate([security.principal.securityidentifier])

В комбинацию "$_" будут попадать только значения столбца Name из нашей хэш-таблицы (если указать, как и раньше $user, то туда попадёт вся хэш-таблица и скрипт даст ошибку преобразования аккаунта). Далее, у нас будет передаваться индивидуальная маска доступа. В предыдущем примере мы использовали всего лишь одну строчку, которая начиналась с $ace.AccessMask. В том примере было всё просто - один пользователь = одна маска. Теперь пользователей/групп уже 3 и маски тоже 3. Здесь мы воспользуемся конструкцией Switch, которая будет принимать наше текстовое значение искать ему сопоставление внутри конструкции и выполнять только одно действие:

switch ($($user[$_])) {
  "FullControl"   {$ace.AccessMask = [System.Security.AccessControl.FileSystemRights]"FullControl, Synchronize"}
  "Change" {$ace.AccessMask = [System.Security.AccessControl.FileSystemRights]"Modify, Synchronize"}
  "Read"   {$ace.AccessMask = [System.Security.AccessControl.FileSystemRights]"ReadAndExecute, Synchronize"}
  }

Я выделил $($user[$_]), для того, чтобы показать, как выбирать только значения из столбца Value нашей хэш-таблицы. Т.к. эта часть кода находится в теле цикла foreach, то сюда будет подставляться новое значение с каждой итерацией. Ну и последний, завершающий штрих - запись нескольких ACE в массив DACL. Если в предыдущем примере мы делали просто

$SD.DACL = $ace

То теперь такой вариант не подойдёт. А почему? А потому, что операция присвоения каждый раз будет заменять значение DACL последним ACE. Т.к. DACL у нас является массивом данных Win32_Ace, то для добавления новых ACE воспользуемся оператором добавления - "+="

$SD.DACL += $ace

Вот теперь в SD.DACL каждый новый ACE будет добавляться как новый элемент массива. Кстати, говоря, именно этой строчкой мы завершаем цикл foreach, т.к. SecurityDescriptor у нас уже готов и его уже можно как обычно отправлять дальше в код.

Подытожив всю полученную информацию мы в конечном итоге получим вот такой код:

$share="UserShare"
([wmiClass] 'Win32_share').Create("C:\Test", $share, "0", "100", "Network share for users")
$user = @{"Everyone" = "Read"; "Administrators" = "FullControl"; "Domain Users" = "Change"}
$SD = ([WMIClass] "Win32_SecurityDescriptor").CreateInstance()
$ace = ([WMIClass] "Win32_Ace").CreateInstance()
$Trustee = ([WMIClass] "Win32_Trustee").CreateInstance()
$user.keys | foreach {
$SID = (new-object security.principal.ntaccount $_ ).translate([security.principal.securityidentifier])
[byte[]] $SIDArray = ,0 * $SID.BinaryLength
$SID.GetBinaryForm($SIDArray,0)
$Trustee.Name = $_
$Trustee.SID = $SIDArray
switch ($($user[$_])) {
  "FullControl"   {$ace.AccessMask = [System.Security.AccessControl.FileSystemRights]"FullControl, Synchronize"}
  "Change" {$ace.AccessMask = [System.Security.AccessControl.FileSystemRights]"Modify, Synchronize"}
  "Read"   {$ace.AccessMask = [System.Security.AccessControl.FileSystemRights]"ReadAndExecute, Synchronize"}
  }
$ace.AceType = 0
$ace.Trustee = $Trustee
$SD.DACL += $ace.psobject.baseobject}
$share = Get-WmiObject win32_share -filter "name='$share'"
$inparams = $share.psbase.GetMethodParameters("SetShareInfo")
$inParams.Access = $SD
$share.psbase.invokemethod("setshareinfo", $inParams, $null)

Вот так немного модифицировав код мы добились более широкого функционала, а именно - возможность добавления множества ACE в едином теле скрипта.

Здесь я переступил логику. По логике следовало сначала разобрать чтение Share Permissions, а потом уже запись, но ввиду одного момента, о котором я рассказал в начале пришлось сначала изучить возможность установки DACL, а потом уже чтение их из Share Permissions. Об этом я уже расскажу следующий раз. Ждите продолжения :)

2008/7/3

Управление общими сетевыми ресурсами (шарами) в PowerShell

В предыдущих постах я рассказывал про управление списками ACL для объектов файловой системы и реестра. Для этих целей использовались встроенные команддеты Get-Acl и Set-Acl. Весьма тесно с этой темой выступает тема управления правами доступа общего ресурса (т.н. Share Permissions). PowerShell не имеет встроенного механизма управления сетевыми ресусрами, только средствами классов WMI. Управление шарами достаточно очень простое в PowerShell до тех пор, пока это не коснётся вопроса управления Share Permissions. Но начнём с самого простого - создание и удаление сетевого ресурса, а так же маппингом сетевых дисков. Для этого используется WMI класс Win32_Share. По ссылке можно узнать какими свойствами и методами обладает этот класс, поэтому здесь перечислять всё это не буду. Для создания нового сетевого ресурса (шары) нам потребуется метод Create с указанием необходимых параметров (которые так же перечислены по ссылке). Попробуем:

$share = [wmiClass] 'Win32_share'
$share.create("C:\Test", "UserShare", "0", "100", "Network share for users")

В первой строчке я вызвал класс Win32_Share и второй строчкой использовал метод Create, чтобы создать новый сетевой ресурс из папки C:\Test с именем UserShare, тип сетевого ресура - Disk Drive, максимальным количеством подключений равным 100 и описанием (поле Descritpion) Network share for users. В Windows Server 2003 и выше при создании нового общего ресурса права на ресурс назначаются только группе Everyone - Allow Read. Этот код можно записать и одной строчкой вообще без переменных:

([wmiClass] 'Win32_share').Create("C:\Test", "UserShare", "0", "100", "Network share for users"

Эта строка выдаст результат в виде таблицы. Последняя строчка ReturnValue будет содержать код возврата, который содержит информацию об операции. Если это 0, то значит, что команда исполнилась успешно и сетевая шара создалась (таблица сопоставления кода возврата текстовому значению находится в описании метода Create). Чтобы посмотреть список сетевых ресурсов на локальном компьютере достаточно выполнить команду Get-WmiObject Win32_Share. Используя метод Delete можно удалять сетевые ресурсы:

$share = Get-WmiObject Win32_Share | where {$_.name -eq "usershare"}
$share.Delete()

или одной строчкой:

(Get-WmiObject Win32_Share | where {$_.name -eq "usershare"}).Delete()

Здесь вместо параметра Name можно указать любой другой, который содержится к классе Win32_Share. Диски так же можно мапить, причём сделать это можно двумя путями. Первый вариант создаст новый диск PowerShell с именем User и который будет доступен только из PowerShell (из Explorer'а и других приложений он не будет доступен. Иногда это очень удобно) и классический маппинг дисков, который будет доступен из проводника:

New-PSDrive User FileSystem \\DC1\UserShare
(new-object -com WScript.Network).MapNetworkDrive("z:",\\dc1\UserShare)

Как же отмапить диски обратно? С точностью до наоборот:

Remove-PSDrive User
(new-object -com WScript.Network).RemoveNetworkDrive("z:")

В принципе, этот материал достаточно обширно расписан в интернете на англоязычных ресурсах и я опубликовал этот пост в качестве вводной статьи для использования метода SetShareInfo, который является достаточно полезным, интересным и, в то же время, достаточно сложным. Так же в следующей статье расскажу о некоторых недостатках упрощённого создания сетевых папок, как показано в примерах этой статьи. Вобщем, ждите продолжения :)

2008/6/27

Ещё несколько слов об управлении ACL в PowerShell и полезных деталях

Во второй части серии статей про управление списками ACL в PowerShell я опубликовал скрипт, который создаёт общую папку для папок перенаправления и расставляет необходимые ACE в списке ACL. Перечитывая свой пост я обнаружил, что забыл рассказать об одной вещи. А именно - установка набора разрешений для группы/пользователя.

Итак, нашем примере группе Everyone требовались следующие права:

  • ExecuteFile
  • ReadData
  • ReadAttributes
  • AppendData

В скрипте мы каждой строчкой добавляли разрешения по одиночке для пользователя и потом записывали их в ACL при помощи метода AddAccessRule. Не самое лучшее решение. А если одной строкой перечислить все права?

$AccessRule = new-object System.Security.AccessControl.FileSystemAccessRule ("Everyone", "ExecuteFile, ReadData, ReadAttributes, AppendData", "Allow")
$ACL.SetAccessRule($AccessRule)

Вот так одной строчкой мы смогли добавить набор прав для группы. Здесь хочу лишь отметить, что пользователей так перечислять нельзя. Для каждого пользователя нужно создавать отдельный ACE.

Теперь немного поговорим о практической стороне вопроса. А именно - передача параметров запуска в тело скрипта. В примере я явно указал путь и имя домашней папки. Во всяком случае я использую такой путь. Кто-то хочет другой путь, кто-то хочет других пользователей использовать и т.д. Это придётся в каждом частном случае править скрипт по новой. Например, путь к папке в скрипте используется 3 раза. А если их там будет 5, 10? Это ни разу не прибавляет гибкости скрипту. А что если мы будем использовать один скрипт и передавать праметры (например, параметр пути) из командной строки CMD? А давайте! Строка запуска очень простая:

powershell %path%\Test.ps1 [parameter1] [parameter2] [parameter3] -SwitchParameter

Разберём, что мы тут имеем:

  • powershell - тут комментарии излишни, это команда, которая будет обрабатывать наш скрипт
  • %path%\Test.ps1 - это полный путь к файлу скрипта. Можно не указывать путь к скрипту, а только его имя, если путь к скрипту указан в системной или пользовательской переменной %path%. Скажем, я держу скрипты в одной рабочей папке и добавил путь к ней в переменную %path%, чтобы не указывать постоянно длиннющий путь.
  • [parameters] - пользовательские параметры, которые передаются в тело скрипта. Параметры просто перечисляются через пробел. Например: powershell %path%\Test.ps1 E:\Users, 'Domain Users' - передаст в тело скрипта путь к домашней папке и группу Domain Users. Если передаваемый параметр содержит пробел, то этот параметр нужно заключить в одинарные кавычки.
  • -SwitchParameter - это параметр запуска самого приложения PowerShell. Доступные параметры запуска можно посмотреть, набрав в консоли CMD (или в самом PowerShell) powershell /?

В скрипте эти параметры принимаются командой Param. Синтаксис тут тоже очень простой:

Param ($parameter1, $parameter2, $parameter3)

Команда Param в последовательном порядке выбирает параметры и по очереди присваивает их переменным, которые уже будут работать в скрипте. В качестве примера я передам в скрипт путь к папке и группу Domain Users:

powershell c:\Test.ps1 d:\Users\Home 'Domain Users'

отмечу, что если скрипт запускается из консоли PowerShell то имя исполняемого файла указывать не надо, а начинать сразу с пути к скрипту.

и сам скрипт:

Param ($path, $users)
New-Item -ItemType directory -Path $path
$inheritCO = [system.security.accesscontrol.InheritanceFlags]"ContainerInherit, ObjectInherit"
$propagationIO = [system.security.accesscontrol.PropagationFlags]"InheritOnly"
$PropagationN = [system.security.accesscontrol.PropagationFlags]"None"
$ACL = Get-Acl $path
$ACL.SetAccessRuleProtection($True, $false)
($ACL).Access | foreach {$ACL.PurgeAccessRules($_.IdentityReference)}
$AccessRule = new-object System.Security.AccessControl.FileSystemAccessRule ("Creator Owner", "FullControl", $inheritCO, $propagationIO,  "Allow")
$ACL.SetAccessRule($AccessRule)
$AccessRule = new-object System.Security.AccessControl.FileSystemAccessRule ("System", "FullControl",$inheritCO, $propagationN, "Allow")
$ACL.SetAccessRule($AccessRule)
$AccessRule = new-object System.Security.AccessControl.FileSystemAccessRule ("Domain Admins", "FullControl", $inheritCO, $propagationN, "Allow")
$ACL.SetAccessRule($AccessRule)
$AccessRule = new-object System.Security.AccessControl.FileSystemAccessRule ("$users", "ExecuteFile, ReadData, ReadAttributes, AppendData", "Allow")
$ACL.SetAccessRule($AccessRule)
$ACL | Set-Acl $path

Я выделил жирным участки кода, где будут использовать переменные с переданными в них параметрами командной строки.
2008/6/25

Особенности конвертирования hex-значений в PowerShell

Как известно, PowerShell позволяет очень просто конвертировать hexadecimal значения в decimal. Достаточно в консоли набрать 0xhex_значение. Вот несколько примеров:

0xFF
255
0x12345
74565
0xDead
57005

Кажется тут всё очень просто и говорить как бы не о чем даже. Однако при работе с относительно большими величинами нужно учитывать тип вводимых данных. К типам данных относятся следующие типы:

  • Int32 - signed integer (знаковое целое, 32-х разрядное). Диапазон чисел от -2147483648 до 2147483647
  • UInt32 - unsigned integer (беззнаковое целое, 32-х разрядное). Диапазон чисел от 0 до 4294967295
  • Int64 - signed integer (знаковое целое, 64-х разрядное). Диапазон чисел от -9223372036854775808 до 9223372036854775807
  • UInt64 - unsigned integer (беззнаковое целое, 64-х разрядное). Диапазон чисел от 0 до 1,84467440737096E+19

и много других типов (Decimal, Single, Double, etc). В контексте hexadecimal чисел нас будут интересовать только эти 4 типа представления чисел. К чему я это? А к тому, что при конвертировании определённых чисел из hex в decimal вы можете получить не совсем ожидаемый результат. Например:

0x7FFFFFFF
2147483647

и если мы прибавим единичку к этому выражению, то получим hex значение 0x80000000 и которое должно равняться 2147483648 в десятичном выражении. Посмотрим, что скажет нам PowerShell:

0x80000000
-2147483648

откуда появился знак минуса? А появился он очень просто. Я специально выше указал диапазоны значений для каждого типа данных. Дело в том, что 0x7FFFFFFF является самым большим положительным числом для типа Int32 и 0x80000000 в десятичной форме будет самым большим отрицательным числом для Int32. Диапазон отрицательных чисел для Int32 находится между -2147483648 (0x8000000) и -1 (0xFFFFFFFF). По умолчанию PowerShell старается присвоить число к типу Int32 или Int64. Как в этом можно убедиться? А очень просто:

(0xDead).gettype().fullname
System.Int32
(4294967296).gettype().fullname
System.Int64

Здесь видно, что PowerShell число 0xDead присвоил к типу Int32, а число 4294967296 присвоил к типу Int64. И если мы работаем только с арифметикой (только положительными числами), то при использовании типов Int32/Int64 мы будем получать весьма неожиданные результаты в виде отрицательных чисел. Поэтому нам нужен механизм, который бы подсказал PowerShell'у тот тип данных, который нам нужен. Это делается следующим образом - [тип_данных]данные:

[Uint64]0x123ABC
1194684
([uint64]0x123ABC).gettype().fullname
System.UInt64

UInt32/UInt64 не содержит отрицательных чисел в десятичном представлении, поэтому при работе только с положительными числами нужно использовать тип UInt32/UInt64, который избавит нас от проблем с появлением отрицательных чисел:

[UInt32]0x80000000
Cannot convert value "-2147483648" to type "System.UInt32". Error: "Value was either too large or too small for a     UInt32."
At line:1 char:9
+ [UInt32]0 <<<< x80000000

Как же так? Мы явно указали, что следует использовать тип UInt32!!! И вот я вас подвёл к тому, что хотел сказать - на самом деле PowerShell всю процедуру преобразования чисел в типы данных выполняет следующим образом:

  1. сперва пытается преобразовать число в Int32;
  2. Если удалось преобразовать в Int32, то уже после этого переводит число в тот тип, который мы указали явно.
  3. Если не удалось преобразовать в Int32, то делается попытка преобразовать его в Int64;
  4. Если удалось преобразовать в Int64, то после этого переводит в тот тип, который мы указали явно;
  5. Если не удалось преобразовать число в Int64, то делается попытка преобразовать его в другие типы чисел (например, Double, если число не является целым).

Согласно этой последовательности действий PowerShell сначала преобразовал число 0x8000000 в Int32 и ему это удалось (получил -2147483648), после чего PowerShell перешёл к шагу 2 и попытался преобразовать число -2147483648 в UInt32. Но, как я уже сказал, UInt32/UInt64 не могут содержать отрицательных чисел, поэтому мы и получили ошибку. Это определённый недостаток, что PowerShell сначала делает своё внутреннее преобразование числа, а только потом пытается преобразовать к тому типу, который мы указываем явно. Однако PowerShell имеет механизм (большое спасибо разработчикам PowerShell за очень быструю реакцию в решении этого вопроса), который переопределяет порядок преобразований типов данных. Для того, чтобы PowerShell чётко следовал нашим инструкциям и сразу преобразовывал число в указанный явно тип данных нужно данное hexadecimal число включить в одинарные кавычки.

[UInt32]'0x80000000'
2147483648

вот теперь мы получаем ожидаемый результат. Исходя из всего выше сказанного при работе с hexadecimal числами следует взять за правило явное указание типа используемых данных и включение hex числа в одинарные кавычки, как это показано на последнем примере.

Кстати говоря, использование кавычек (как одинарных, так и двойных) имеет достаточно много интересных и полезных особенностей. Для вводного курса по кавычкам советую почитать в блоге Василия Гусева:

Всякие загогулинки

Иногда кавычки имеют значение ;)

Хорошо всё то, что хорошо заканчивается :)

2008/6/23

Смена владельца (Owner) папки или файла

В 3-й части стати Управление ACL в PowerShell я говорил, что не представляется возможным нативно через метод SetOwner изменить владельца объекта на кого-нибудь, кроме себя или группы Administrators (Администраторы). Эту операцию невозможно было сделать и в GUI в Windows 2000/XP. Но в Windows Server 2003 появилась возможность изменять владельца объекта на любого пользователя или группу из GUI. Для этой операции требуется лишь право Se_Restore - Restore directories and files в локальной политике безопасности, разделе User Rights Assignment. По умолчанию данное право дано администраторам (Administrators) и операторам архива (Backup Operators). Однако, данное право не включено по умолчанию. К сожалению .NET пока что не имеет "родного" метода, чтобы включить данное право, в результате чего используя скрипты PowerShell мы не сможем полноценно использовать новую в Windows Server 2003 функцию. Но не всё так плохо, как может показаться - есть обходное решение данной проблемы. Данную проблему решили в проекте PowerShell Community Extensions.

Для использования этой привилегии необходимо установить пакет PowerShell Community Extensions 1.1.1 (там же и описание возможностей, которые включены в эти расширения). После установки пакета можно начинать использовать данное расширение:

# считываем в переменную список ACL объекта
$ACL = Get-Acl C:\Test
# Переводим имя нового владельца в SID
$Account = new-object System.Security.Principal.NTAccount("TestUser")
# создаём необходимую привилегию для использования и записываем её в переменную
$SeRestore = new-object Pscx.Interop.TokenPrivilege "SeRestorePrivilege", $true
# здесь уже записываем созданную привилегию командой SetPrivilege
Set-Privilege $SeRestore
# запись нового владельца объекта стандартным методом SetOwner
$ACL.SetOwner($Account)
# запись нового ACL в папку
$ACL | Set-Acl C:\Test

Это было в расширенном варианте. Данный скрипт можно смело упростить до:

$ACL = Get-Acl C:\Windows
Set-Privilege (new-object Pscx.Interop.TokenPrivilege "SeRestorePrivilege", $true)
$ACL.SetOwner((new-object System.Security.Principal.NTAccount("TestUser")))
$ACL | Set-Acl C:\Test

Решение в виде установки дополнительных расширений не является самым идеальным, но тем не менее позволяет использовать чистый PowerShell для операции (а ведь можно было из PowerShell запустить SubInacl и им уже сменить владельца).