diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..a5df2c7 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto \ No newline at end of file diff --git a/.gitignore b/.gitignore index 3e759b7..4318a56 100644 --- a/.gitignore +++ b/.gitignore @@ -1,330 +1,22 @@ -## Ignore Visual Studio temporary files, build results, and -## files generated by popular Visual Studio add-ons. -## -## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore - -# User-specific files -*.suo -*.user -*.userosscache -*.sln.docstates - -# User-specific files (MonoDevelop/Xamarin Studio) -*.userprefs - -# Build results -[Dd]ebug/ -[Dd]ebugPublic/ -[Rr]elease/ -[Rr]eleases/ -x64/ -x86/ -bld/ -[Bb]in/ -[Oo]bj/ -[Ll]og/ - -# Visual Studio 2015/2017 cache/options directory -.vs/ -# Uncomment if you have tasks that create the project's static files in wwwroot -#wwwroot/ - -# Visual Studio 2017 auto generated files -Generated\ Files/ - -# MSTest test Results -[Tt]est[Rr]esult*/ -[Bb]uild[Ll]og.* - -# NUNIT -*.VisualState.xml -TestResult.xml - -# Build Results of an ATL Project -[Dd]ebugPS/ -[Rr]eleasePS/ -dlldata.c - -# Benchmark Results -BenchmarkDotNet.Artifacts/ - -# .NET Core -project.lock.json -project.fragment.lock.json -artifacts/ -**/Properties/launchSettings.json - -# StyleCop -StyleCopReport.xml - -# Files built by Visual Studio -*_i.c -*_p.c -*_i.h -*.ilk -*.meta -*.obj -*.iobj -*.pch -*.pdb -*.ipdb -*.pgc -*.pgd -*.rsp -*.sbr -*.tlb -*.tli -*.tlh -*.tmp -*.tmp_proj -*.log -*.vspscc -*.vssscc -.builds -*.pidb -*.svclog -*.scc - -# Chutzpah Test files -_Chutzpah* - -# Visual C++ cache files -ipch/ -*.aps -*.ncb -*.opendb -*.opensdf -*.sdf -*.cachefile -*.VC.db -*.VC.VC.opendb - -# Visual Studio profiler -*.psess -*.vsp -*.vspx -*.sap - -# Visual Studio Trace Files -*.e2e - -# TFS 2012 Local Workspace -$tf/ - -# Guidance Automation Toolkit -*.gpState - -# ReSharper is a .NET coding add-in -_ReSharper*/ -*.[Rr]e[Ss]harper -*.DotSettings.user - -# JustCode is a .NET coding add-in -.JustCode - -# TeamCity is a build add-in -_TeamCity* - -# DotCover is a Code Coverage Tool -*.dotCover - -# AxoCover is a Code Coverage Tool -.axoCover/* -!.axoCover/settings.json - -# Visual Studio code coverage results -*.coverage -*.coveragexml - -# NCrunch -_NCrunch_* -.*crunch*.local.xml -nCrunchTemp_* - -# MightyMoose -*.mm.* -AutoTest.Net/ - -# Web workbench (sass) -.sass-cache/ - -# Installshield output folder -[Ee]xpress/ - -# DocProject is a documentation generator add-in -DocProject/buildhelp/ -DocProject/Help/*.HxT -DocProject/Help/*.HxC -DocProject/Help/*.hhc -DocProject/Help/*.hhk -DocProject/Help/*.hhp -DocProject/Help/Html2 -DocProject/Help/html - -# Click-Once directory -publish/ - -# Publish Web Output -*.[Pp]ublish.xml -*.azurePubxml -# Note: Comment the next line if you want to checkin your web deploy settings, -# but database connection strings (with potential passwords) will be unencrypted -*.pubxml -*.publishproj - -# Microsoft Azure Web App publish settings. Comment the next line if you want to -# checkin your Azure Web App publish settings, but sensitive information contained -# in these scripts will be unencrypted -PublishScripts/ - -# NuGet Packages -*.nupkg -# The packages folder can be ignored because of Package Restore -**/[Pp]ackages/* -# except build/, which is used as an MSBuild target. -!**/[Pp]ackages/build/ -# Uncomment if necessary however generally it will be regenerated when needed -#!**/[Pp]ackages/repositories.config -# NuGet v3's project.json files produces more ignorable files -*.nuget.props -*.nuget.targets - -# Microsoft Azure Build Output -csx/ -*.build.csdef - -# Microsoft Azure Emulator -ecf/ -rcf/ - -# Windows Store app package directories and files -AppPackages/ -BundleArtifacts/ -Package.StoreAssociation.xml -_pkginfo.txt -*.appx - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!*.[Cc]ache/ - -# Others -ClientBin/ -~$* -*~ -*.dbmdl -*.dbproj.schemaview -*.jfm -*.pfx -*.publishsettings -orleans.codegen.cs - -# Including strong name files can present a security risk -# (https://github.com/github/gitignore/pull/2483#issue-259490424) -#*.snk - -# Since there are multiple workflows, uncomment next line to ignore bower_components -# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) -#bower_components/ - -# RIA/Silverlight projects -Generated_Code/ - -# Backup & report files from converting an old project file -# to a newer Visual Studio version. Backup files are not needed, -# because we have git ;-) -_UpgradeReport_Files/ -Backup*/ -UpgradeLog*.XML -UpgradeLog*.htm -ServiceFabricBackup/ -*.rptproj.bak - -# SQL Server files -*.mdf -*.ldf -*.ndf - -# Business Intelligence projects -*.rdl.data -*.bim.layout -*.bim_*.settings -*.rptproj.rsuser - -# Microsoft Fakes -FakesAssemblies/ - -# GhostDoc plugin setting file -*.GhostDoc.xml - -# Node.js Tools for Visual Studio -.ntvs_analysis.dat -node_modules/ - -# Visual Studio 6 build log -*.plg - -# Visual Studio 6 workspace options file -*.opt - -# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) -*.vbw - -# Visual Studio LightSwitch build output -**/*.HTMLClient/GeneratedArtifacts -**/*.DesktopClient/GeneratedArtifacts -**/*.DesktopClient/ModelManifest.xml -**/*.Server/GeneratedArtifacts -**/*.Server/ModelManifest.xml -_Pvt_Extensions - -# Paket dependency manager -.paket/paket.exe -paket-files/ - -# FAKE - F# Make -.fake/ - -# JetBrains Rider -.idea/ -*.sln.iml - -# CodeRush -.cr/ - -# Python Tools for Visual Studio (PTVS) -__pycache__/ -*.pyc - -# Cake - Uncomment if you are using it -# tools/** -# !tools/packages.config - -# Tabs Studio -*.tss - -# Telerik's JustMock configuration file -*.jmconfig - -# BizTalk build output -*.btp.cs -*.btm.cs -*.odx.cs -*.xsd.cs - -# OpenCover UI analysis results -OpenCover/ - -# Azure Stream Analytics local run output -ASALocalRun/ - -# MSBuild Binary and Structured Log -*.binlog - -# NVidia Nsight GPU debugger configuration file -*.nvuser - -# MFractors (Xamarin productivity tool) working folder -.mfractor/ + +# ignore the settings folder and files for VSCode and PSS +.vscode/* +*.psproj +*TempPoint* + +# Ignore staging info from Visual Studio +library/AaronLocker/.vs/* +library/AaronLocker/AaronLocker/bin/* +library/AaronLocker/AaronLocker/obj/* + +# ignore PowerShell Studio MetaData +AaronLocker/AaronLocker.psproj +AaronLocker/AaronLocker.psproj.bak +AaronLocker/AaronLocker.psprojs +AaronLocker/AaronLocker.psproj + +# ignore the TestResults +TestResults/* + +# ignore the publishing Directory +publish/* \ No newline at end of file diff --git a/AaronLocker/AaronLocker.psd1 b/AaronLocker/AaronLocker.psd1 new file mode 100644 index 0000000..9159c7b --- /dev/null +++ b/AaronLocker/AaronLocker.psd1 @@ -0,0 +1,102 @@ +@{ + # Script module or binary module file associated with this manifest + RootModule = 'AaronLocker.psm1' + + # Version number of this module. + ModuleVersion = '1.0.0.0' + + # ID used to uniquely identify this module + GUID = 'ee9f8d0f-b919-47ce-aa02-24fa4520e6ed' + + # Author of this module + Author = 'Aaron Margosis' + + # Company or vendor of this module + CompanyName = '' + + # Copyright statement for this module + Copyright = 'Copyright (c) 2018 Aaron Margosis' + + # Description of the functionality provided by this module + Description = 'Manages and applies AppLocker policies' + + # Minimum version of the Windows PowerShell engine required by this module + PowerShellVersion = '5.0' + + # Modules that must be imported into the global environment prior to importing + # this module + RequiredModules = @('AppLocker') + + # Assemblies that must be loaded prior to importing this module + RequiredAssemblies = @('bin\AaronLocker.dll') + + # Type files (.ps1xml) to be loaded when importing this module + TypesToProcess = @('xml\AaronLocker.Types.ps1xml') + + # Format files (.ps1xml) to be loaded when importing this module + FormatsToProcess = @('xml\AaronLocker.Format.ps1xml') + + # Functions to export from this module + FunctionsToExport = @( + 'Get-ALConfiguration' + 'Set-ALConfiguration' + + 'ConvertTo-ALAppLockerXML' + 'Export-ALAppLockerPolicy' + + 'Export-ALPolicy' + 'Get-ALPolicy' + 'Import-ALPolicy' + 'New-ALPolicy' + 'Remove-ALPolicy' + 'Set-ALActivePolicy' + + 'Add-ALRule' + 'Add-ALRuleHash' + 'Add-ALRulePath' + 'Add-ALRulePublisher' + 'Add-ALRuleSourcePath' + 'Get-ALRule' + 'Remove-ALRule' + ) + + # Cmdlets to export from this module + CmdletsToExport = '' + + # Variables to export from this module + VariablesToExport = '' + + # Aliases to export from this module + AliasesToExport = '' + + # List of all modules packaged with this module + ModuleList = @() + + # List of all files packaged with this module + FileList = @() + + # Private data to pass to the module specified in ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. + PrivateData = @{ + + #Support for PowerShellGet galleries. + PSData = @{ + + # Tags applied to this module. These help with module discovery in online galleries. + # Tags = @() + + # A URL to the license for this module. + # LicenseUri = '' + + # A URL to the main website for this project. + # ProjectUri = '' + + # A URL to an icon representing this module. + # IconUri = '' + + # ReleaseNotes of this module + # ReleaseNotes = '' + + } # End of PSData hashtable + + } # End of PrivateData hashtable +} \ No newline at end of file diff --git a/AaronLocker/AaronLocker.psm1 b/AaronLocker/AaronLocker.psm1 new file mode 100644 index 0000000..795dbce --- /dev/null +++ b/AaronLocker/AaronLocker.psm1 @@ -0,0 +1,85 @@ +$script:ModuleRoot = $PSScriptRoot +$script:ModuleVersion = "1.0.0.0" + +# Detect whether at some level dotsourcing was enforced +$script:doDotSource = $false +if ($AaronLocker_dotsourcemodule) { $script:doDotSource = $true } + +<# +Note on Resolve-Path: +All paths are sent through Resolve-Path/Resolve-PSFPath in order to convert them to the correct path separator. +This allows ignoring path separators throughout the import sequence, which could otherwise cause trouble depending on OS. +Resolve-Path can only be used for paths that already exist, Resolve-PSFPath can accept that the last leaf my not exist. +This is important when testing for paths. +#> + +# Detect whether at some level loading individual module files, rather than the compiled module was enforced +$importIndividualFiles = $false +if ($AaronLocker_importIndividualFiles) { $importIndividualFiles = $true } +if (Test-Path "$($script:ModuleRoot)\..\.git") { $importIndividualFiles = $true } +if (-not (Test-Path "$($script:ModuleRoot)\commands.ps1")) { $importIndividualFiles = $true } + +function Import-ModuleFile +{ + <# + .SYNOPSIS + Loads files into the module on module import. + + .DESCRIPTION + This helper function is used during module initialization. + It should always be dotsourced itself, in order to proper function. + + This provides a central location to react to files being imported, if later desired + + .PARAMETER Path + The path to the file to load + + .EXAMPLE + PS C:\> . Import-ModuleFile -File $function.FullName + + Imports the file stored in $function according to import policy + #> + [CmdletBinding()] + Param ( + [string] + $Path + ) + + if ($doDotSource) { . (Resolve-Path $Path) } + else { $ExecutionContext.InvokeCommand.InvokeScript($false, ([scriptblock]::Create([io.file]::ReadAllText((Resolve-Path $Path)))), $null, $null) } +} + +if ($importIndividualFiles) +{ + # Execute Preimport actions + . Import-ModuleFile -Path "$ModuleRoot\internal\scripts\preimport.ps1" + + # Import all internal functions + foreach ($function in (Get-ChildItem "$ModuleRoot\internal\functions" -Filter "*.ps1" -Recurse -ErrorAction Ignore)) + { + . Import-ModuleFile -Path $function.FullName + } + + # Import all public functions + foreach ($function in (Get-ChildItem "$ModuleRoot\functions" -Filter "*.ps1" -Recurse -ErrorAction Ignore)) + { + . Import-ModuleFile -Path $function.FullName + } + + # Execute Postimport actions + . Import-ModuleFile -Path "$ModuleRoot\internal\scripts\postimport.ps1" +} +else +{ + if (Test-Path "$($script:ModuleRoot)\resourcesBefore.ps1") + { + . Import-ModuleFile -Path "$($script:ModuleRoot)\resourcesBefore.ps1" + } + + . Import-ModuleFile -Path "$($script:ModuleRoot)\commands.ps1" + + if (Test-Path "$($script:ModuleRoot)\resourcesAfter.ps1") + { + . Import-ModuleFile -Path "$($script:ModuleRoot)\resourcesAfter.ps1" + } +} \ No newline at end of file diff --git a/AaronLocker/bin/AaronLocker.dll b/AaronLocker/bin/AaronLocker.dll new file mode 100644 index 0000000..c5e5ef8 Binary files /dev/null and b/AaronLocker/bin/AaronLocker.dll differ diff --git a/AaronLocker/bin/AaronLocker.pdb b/AaronLocker/bin/AaronLocker.pdb new file mode 100644 index 0000000..c079875 Binary files /dev/null and b/AaronLocker/bin/AaronLocker.pdb differ diff --git a/AaronLocker/bin/AaronLocker.xml b/AaronLocker/bin/AaronLocker.xml new file mode 100644 index 0000000..f51dbf7 --- /dev/null +++ b/AaronLocker/bin/AaronLocker.xml @@ -0,0 +1,471 @@ + + + + AaronLocker + + + + + Whether to permit or deny execution in a given rule + + + + + Allow execution + + + + + Deny execution + + + + + Whether a policy should be enforced + + + + + The policy has yet to be configured + + + + + The policy is designed for audit only + + + + + The policy will be enforced + + + + + A rule to apply based on hash values + + + + + The hash value to apply the rule by + + + + + The name of the actual file the hash targets + + + + + The original input file's length + + + + + Attach rule to policy + + The AppLocker policy to integrate into. + The policy object that calls for this integration. + + + + + + + A rule enforcing compliance based on path. + + + + + The path to which to apply this rule + + + + + Items or folders under the path to exclude from this rule + + + + + Attach rule to policy + + The AppLocker policy to integrate into. + The policy object that calls for this integration. + + + + + + + An AppLocker policy, containing rules and offering tools to convert / integrate into output generation. + + + + + List of all rules that are part of this policy + + + + + Number of rules stored in the policy + + + + + An arbitrary name for this policy. Internal use only, to help distinguishing between different policies. + + + + + Add a neat description, telling your future self what this was all about + + + + + When was the last update to the policy + + + + + List of rules that failed to execute during the last compilation effort + + + + + Returns XML string of the finished AppLocker policy + + How the policy should be enforced + XML Text + + + + Rule acting based on publisher that signed a file. + + + + + The name of the publisher + + + + + Name of the product + + + + + Name of the file + + + + + Minimum version to apply this to + + + + + Last version to apply this rule to. + + + + + Path to an exampel file to use to generate publisher information + + + + + Whether to also use the product information, when recording from an example file. + + + + + Resolves an Exemplar into the publisher rule relevant data + + + + + Attach rule to policy + + The AppLocker policy to integrate into. + The policy object that calls for this integration. + + + + + + + Base class for AppLocker rules + + + + + The name of the rule + + + + + A description of what this rule is all about + + + + + Group or user the rule applies to + + + + + An ID of the rule. Leave this empty, if you do not want to hardcode a specific GUid for a specific rule. + + + + + What scope does the rule apply to (specifically: Is it designed to affect dlls, executables or scripts). + + + + + What kind of rule is this? + + + + + Whether to allow (Whitelist) or deny (Blacklist) the target of this rule + + + + + Each rule must be able to attach itself to an XML document representing an AppLocker rule. + + The AppLocker policy to integrate into. + The policy object that calls for this integration. + + + + Clones the current rule + + + + + Copies the base properties of a rule object. Used by Clone() implementations. + + The object to copy properties into. + + + + Class representing a rule that failed to properly resolve. + Used when resolving rules from a Policy object. + + + + + The type of rule it was + + + + + The label the rule was meant to carry + + + + + The source rule object + + + + + The actual exception that prevented success + + + + + Creates an empty rule failure + + + + + Creates a preconfigured rule failure + + The rule that failed + The exception describing the failure + + + + The kind of rule a rule is + + + + + A rule based on controlling execution of files signed by a specific publisher + + + + + A rule that controls execution based on a specific file hash + + + + + A rule controlling execution based on path content is executed from + + + + + A temporary rule that will be converted into regular rules when realized. + + + + + The various rule scope types available in an AaronLocker based Applocker Rule + + + + + The rule applies to executables + + + + + The rule applies to Dynamic Link Libraries + + + + + The rule applies to script files + + + + + The default package applies to executables, dlls and script files + + + + + The rule applies to installer files + + + + + The rule applies to AppX UWP Apps + + + + + The rule applies to all types of files + + + + + Typeconverter that does the heavy lifting of maintaining type integrity across process borders. + + + + + Whether the source can be converted to its destination + + The value to convert + The type to convert to + Whether this action is possible + + + + Converts an object + + The data to convert + The type to convert to + This will be ignored, but must be present + This will be ignored, but must be present + The converted object + + + + Whether the input object can be converted to the Destination type + + Input value + The type to convert to + + + + + Converts an object + + The data to convert + The type to convert to + This will be ignored, but must be present + This will be ignored, but must be present + The converted object + + + + Registers an assembly resolving event + + + + + Whether an object can be serialized + + The object to test + Whether the object can be serialized + + + + Whether a type can be serialized + + The type to test + Whether the specified type can be serialized + + + + The validation check on whether a type is serializable + + The type to test + Returns whether that type can be serialized + + + + Used to obtain the information to write + + The object to dissect + A memory stream. + + + + A rule based on a source file, can be converted to the most constrained rule object type. + + + + + The path to the item + + + + + Whether the specified path should be resolved recursively + + + + + Whether the found version of a product should be enforced, if the rule resolves into a Publisher Rule. + + + + + Processes the path specified and generates rules based on it. + + Rules that are as restrictive as possible + + + + Scriptblock used to resolve the specified path into rule objects + + + + + Attach rule to policy + + The AppLocker policy to integrate into. + The policy object that calls for this integration. + + + + + + diff --git a/AaronLocker/bin/System.Management.Automation.dll b/AaronLocker/bin/System.Management.Automation.dll new file mode 100644 index 0000000..aa7aa14 Binary files /dev/null and b/AaronLocker/bin/System.Management.Automation.dll differ diff --git a/AaronLocker/bin/readme.md b/AaronLocker/bin/readme.md new file mode 100644 index 0000000..3379e65 --- /dev/null +++ b/AaronLocker/bin/readme.md @@ -0,0 +1,7 @@ +# bin folder + +The bin folder exists to store binary data. And scripts related to the type system. + +This may include your own C#-based library, third party libraries you want to include (watch the license!), or a script declaring type accelerators (effectively aliases for .NET types) + +For more information on Type Accelerators, see the help on Set-PSFTypeAlias \ No newline at end of file diff --git a/AaronLocker/bin/types.ps1 b/AaronLocker/bin/types.ps1 new file mode 100644 index 0000000..c533a11 --- /dev/null +++ b/AaronLocker/bin/types.ps1 @@ -0,0 +1,3 @@ +#TODO: Remove -ErrorAction Ignore +try { Add-Type -AssemblyName "Microsoft.Security.ApplicationId.PolicyManagement.PolicyModel" -ErrorAction Ignore } +catch { } \ No newline at end of file diff --git a/AaronLocker/data/blacklist_defaultApplications.psd1 b/AaronLocker/data/blacklist_defaultApplications.psd1 new file mode 100644 index 0000000..d080d81 --- /dev/null +++ b/AaronLocker/data/blacklist_defaultApplications.psd1 @@ -0,0 +1,24 @@ +@{ + # Files used to bypass whitelisting: + DotNetApplications = @( + "InstallUtil.exe" + "IEExec.exe" + "RegAsm.exe" + "RegSvcs.exe" + "MSBuild.exe" + ) + + FullPath = @( + # Files used to bypass whitelisting: + "$env:windir\System32\mshta.exe" + "$env:windir\System32\PresentationHost.exe" + "$env:windir\System32\wbem\WMIC.exe" + # Note: also need Code Integrity rules to block other bypasses + + # Files used by ransomware + "$env:windir\System32\cipher.exe" + + # Block common credential exposure risk (also need to disable GUI option via registry, and SecondaryLogon service) + "$env:windir\System32\runas.exe" + ) +} \ No newline at end of file diff --git a/AaronLocker/data/whitelist_defaultPublisherRules.psd1 b/AaronLocker/data/whitelist_defaultPublisherRules.psd1 new file mode 100644 index 0000000..213e456 --- /dev/null +++ b/AaronLocker/data/whitelist_defaultPublisherRules.psd1 @@ -0,0 +1,209 @@ +@{ + Common = @( + @{ + # Allow Microsoft-signed OneDrive EXE and DLL files with the OneDrive product name; + # This rule doesn't cover all of OneDrive's files because they include files from other products (Visual Studio, QT5, etc.) + label = "Microsoft OneDrive (partial)" + PublisherName = "O=MICROSOFT CORPORATION, L=REDMOND, S=WASHINGTON, C=US" + ProductName = "MICROSOFT ONEDRIVE" + } + + @{ + label = "Microsoft-signed MSI files" + RuleCollection = "Msi" + PublisherName = "O=MICROSOFT CORPORATION, L=REDMOND, S=WASHINGTON, C=US" + } + + @{ + # Windows' built-in troubleshooting often involves running Microsoft-signed scripts in the user's profile + label = "Microsoft-signed script files" + RuleCollection = "Script" + PublisherName = "O=MICROSOFT CORPORATION, L=REDMOND, S=WASHINGTON, C=US" + } + ) + MSFC_MVC = @( + ########################################################################### + # Visual Studio 2005 + ########################################################################### + + @{ + label = "MSVC runtime DLL" + RuleCollection = "Dll" + PublisherName = "O=MICROSOFT CORPORATION, L=REDMOND, S=WASHINGTON, C=US" + ProductName = "MICROSOFT® VISUAL STUDIO® 2005" + BinaryName = "MSVCP80.DLL" + } + + @{ + label = "MSVC runtime DLL" + RuleCollection = "Dll" + PublisherName = "O=MICROSOFT CORPORATION, L=REDMOND, S=WASHINGTON, C=US" + ProductName = "MICROSOFT® VISUAL STUDIO® 2005" + BinaryName = "MSVCR80.DLL" + } + + ########################################################################### + # Visual Studio 2008 + ########################################################################### + + @{ + label = "MFC runtime DLL" + RuleCollection = "Dll" + PublisherName = "O=MICROSOFT CORPORATION, L=REDMOND, S=WASHINGTON, C=US" + ProductName = "MICROSOFT® VISUAL STUDIO® 2008" + BinaryName = "MFC90U.DLL" + } + + @{ + label = "MSVC runtime DLL" + RuleCollection = "Dll" + PublisherName = "O=MICROSOFT CORPORATION, L=REDMOND, S=WASHINGTON, C=US" + ProductName = "MICROSOFT® VISUAL STUDIO® 2008" + BinaryName = "MSVCP90.DLL" + } + + @{ + label = "MSVC runtime DLL" + RuleCollection = "Dll" + PublisherName = "O=MICROSOFT CORPORATION, L=REDMOND, S=WASHINGTON, C=US" + ProductName = "MICROSOFT® VISUAL STUDIO® 2008" + BinaryName = "MSVCR90.DLL" + } + + ########################################################################### + # Visual Studio 2010 + ########################################################################### + + @{ + label = "MSVC runtime DLL" + RuleCollection = "Dll" + PublisherName = "O=MICROSOFT CORPORATION, L=REDMOND, S=WASHINGTON, C=US" + ProductName = "MICROSOFT® VISUAL STUDIO® 2010" + BinaryName = "MSVCP100.DLL" + } + + @{ + label = "MSVC runtime DLL" + RuleCollection = "Dll" + PublisherName = "O=MICROSOFT CORPORATION, L=REDMOND, S=WASHINGTON, C=US" + ProductName = "MICROSOFT® VISUAL STUDIO® 2010" + BinaryName = "MSVCR100_CLR0400.DLL" + } + + ########################################################################### + # Visual Studio 2012 + ########################################################################### + + @{ + label = "MFC runtime DLL" + RuleCollection = "Dll" + PublisherName = "O=MICROSOFT CORPORATION, L=REDMOND, S=WASHINGTON, C=US" + ProductName = "MICROSOFT® VISUAL STUDIO® 2012" + BinaryName = "MFC110.DLL" + } + + @{ + label = "MSVC runtime DLL" + RuleCollection = "Dll" + PublisherName = "O=MICROSOFT CORPORATION, L=REDMOND, S=WASHINGTON, C=US" + ProductName = "MICROSOFT® VISUAL STUDIO® 2012" + BinaryName = "MSVCP110.DLL" + } + + @{ + label = "MSVC runtime DLL" + RuleCollection = "Dll" + PublisherName = "O=MICROSOFT CORPORATION, L=REDMOND, S=WASHINGTON, C=US" + ProductName = "MICROSOFT® VISUAL STUDIO® 2012" + BinaryName = "MSVCR110.DLL" + } + + ########################################################################### + # Visual Studio 2013 + ########################################################################### + + @{ + label = "MFC runtime DLL" + RuleCollection = "Dll" + PublisherName = "O=MICROSOFT CORPORATION, L=REDMOND, S=WASHINGTON, C=US" + ProductName = "MICROSOFT® VISUAL STUDIO® 2013" + BinaryName = "MFC120.DLL" + } + + @{ + label = "MFC runtime DLL" + RuleCollection = "Dll" + PublisherName = "O=MICROSOFT CORPORATION, L=REDMOND, S=WASHINGTON, C=US" + ProductName = "MICROSOFT® VISUAL STUDIO® 2013" + BinaryName = "MFC120U.DLL" + } + + @{ + label = "MSVC runtime DLL" + RuleCollection = "Dll" + PublisherName = "O=MICROSOFT CORPORATION, L=REDMOND, S=WASHINGTON, C=US" + ProductName = "MICROSOFT® VISUAL STUDIO® 2013" + BinaryName = "MSVCP120.DLL" + } + + @{ + label = "MSVC runtime DLL" + RuleCollection = "Dll" + PublisherName = "O=MICROSOFT CORPORATION, L=REDMOND, S=WASHINGTON, C=US" + ProductName = "MICROSOFT® VISUAL STUDIO® 2013" + BinaryName = "MSVCR120.DLL" + } + + ########################################################################### + # Visual Studio 2015 + ########################################################################### + + @{ + label = "MSVC runtime DLL" + RuleCollection = "Dll" + PublisherName = "O=MICROSOFT CORPORATION, L=REDMOND, S=WASHINGTON, C=US" + ProductName = "MICROSOFT® VISUAL STUDIO® 2015" + BinaryName = "MSVCP140.DLL" + } + + @{ + label = "MSVC runtime DLL" + RuleCollection = "Dll" + PublisherName = "O=MICROSOFT CORPORATION, L=REDMOND, S=WASHINGTON, C=US" + ProductName = "MICROSOFT® VISUAL STUDIO® 2015" + BinaryName = "VCRUNTIME140.DLL" + } + + ########################################################################### + # Visual Studio 2017 + ########################################################################### + + @{ + label = "MSVC runtime DLL" + RuleCollection = "Dll" + PublisherName = "O=MICROSOFT CORPORATION, L=REDMOND, S=WASHINGTON, C=US" + ProductName = "MICROSOFT® VISUAL STUDIO® 2017" + BinaryName = "MSVCP140.DLL" + } + + @{ + label = "MSVC runtime DLL" + RuleCollection = "Dll" + PublisherName = "O=MICROSOFT CORPORATION, L=REDMOND, S=WASHINGTON, C=US" + ProductName = "MICROSOFT® VISUAL STUDIO® 2017" + BinaryName = "VCRUNTIME140.DLL" + } + + ########################################################################### + # Visual Studio 10 + ########################################################################### + + @{ + label = "MFC runtime DLL" + RuleCollection = "Dll" + PublisherName = "O=MICROSOFT CORPORATION, L=REDMOND, S=WASHINGTON, C=US" + ProductName = "MICROSOFT® VISUAL STUDIO® 10" + BinaryName = "MFC100U.DLL" + } + ) +} \ No newline at end of file diff --git a/AaronLocker/en-us/about_AaronLocker.help.txt b/AaronLocker/en-us/about_AaronLocker.help.txt new file mode 100644 index 0000000..665331e --- /dev/null +++ b/AaronLocker/en-us/about_AaronLocker.help.txt @@ -0,0 +1,11 @@ +TOPIC + about_AaronLocker + +SHORT DESCRIPTION + Explains how to use the AaronLocker powershell module + +LONG DESCRIPTION + + +KEYWORDS + AaronLocker \ No newline at end of file diff --git a/AaronLocker/functions/configuration/Get-ALConfiguration.ps1 b/AaronLocker/functions/configuration/Get-ALConfiguration.ps1 new file mode 100644 index 0000000..36133ef --- /dev/null +++ b/AaronLocker/functions/configuration/Get-ALConfiguration.ps1 @@ -0,0 +1,31 @@ +function Get-ALConfiguration +{ +<# + .SYNOPSIS + Returns all configuration settings stored by AaronLocker. + + .DESCRIPTION + Returns all configuration settings stored by AaronLocker. + + .EXAMPLE + PS C:\> Get-ALConfiguration + + Returns all configuration settings stored by AaronLocker. +#> + [CmdletBinding()] + Param ( + + ) + + process + { + $resultHash = @{ + PSTypeName = 'AaronLocker.Configuration.Settings' + } + foreach ($property in $script:config.PSObject.Properties) + { + $resultHash[$property.Name] = $property.Value + } + [pscustomobject]$resultHash + } +} \ No newline at end of file diff --git a/AaronLocker/functions/configuration/Set-ALConfiguration.ps1 b/AaronLocker/functions/configuration/Set-ALConfiguration.ps1 new file mode 100644 index 0000000..0ecb19b --- /dev/null +++ b/AaronLocker/functions/configuration/Set-ALConfiguration.ps1 @@ -0,0 +1,74 @@ +function Set-ALConfiguration +{ +<# + .SYNOPSIS + Command that controls the settings of this module. + + .DESCRIPTION + Command that controls the settings of this module. + Use this to register required resources or tune the module's behavior. + + Settings applied here will be persisted across multiple sessions for the current user. + + .PARAMETER PathAccessChk + The path to where AccessChk.exe is stored. + This sysinternals application is critical to commands enumerating access. + It can be downloaded from sysinternals.com + + .PARAMETER AddKnownAdmins + Users to add to the list of known administrator accounts. + Used in commands that take administrative privileges into account. + + .PARAMETER RemoveKnownAdmins + Users to remove from the list of known administrator accounts. + Used in commands that take administrative privileges into account. + + .PARAMETER ActivePolicy + Change the set of active AppLocker rules worked upon. + Generally not directly configured. Use Set-ALActivePolicy to update this setting. + + .EXAMPLE + PS C:\> Set-ALConfiguration -PathAccessChk "C:\Program Files\Sysinternals\AccessChk.exe" + + Configures the module to look for the AccessChk application in "C:\Program Files\Sysinternals\AccessChk.exe" +#> + [CmdletBinding()] + param ( + [string] + $PathAccessChk, + + [string[]] + $AddKnownAdmins, + + [string[]] + $RemoveKnownAdmins, + + [ValidateScript({ (Get-ALPolicy -PolicyName "*").Name -contains $_ })] + [string] + $ActivePolicy + ) + process + { + if ($PSBoundParameters.ContainsKey('PathAccessChk')) + { + Write-Verbose "Updating path to AccessChk.exe to: $($PathAccessChk)" + $script:config.PathAccessChk = $PathAccessChk + } + if ($PSBoundParameters.ContainsKey('AddKnownAdmins')) + { + Write-Verbose "Adding to known admins: $($AddKnownAdmins -join ", ")" + $script:config.KnownAdmins = $script:config.KnownAdmins, $AddKnownAdmins | Select-Object -Unique + } + if ($PSBoundParameters.ContainsKey('RemoveKnownAdmins')) + { + Write-Verbose "Updating path to AccessChk.exe to: $($RemoveKnownAdmins)" + $script:config.KnownAdmins = $script:config.KnownAdmins | Where-Object { $_ -notin $RemoveKnownAdmins } + } + if ($PSBoundParameters.ContainsKey('ActivePolicy')) + { + Write-Verbose "Setting the current ruleset to: $($ActivePolicy)" + $script:config.ActivePolicy = $ActivePolicy + } + Export-Configuration + } +} \ No newline at end of file diff --git a/AaronLocker/functions/export/ConvertTo-ALAppLockerXML.ps1 b/AaronLocker/functions/export/ConvertTo-ALAppLockerXML.ps1 new file mode 100644 index 0000000..45eb2c3 --- /dev/null +++ b/AaronLocker/functions/export/ConvertTo-ALAppLockerXML.ps1 @@ -0,0 +1,72 @@ +function ConvertTo-ALAppLockerXML +{ +<# + .SYNOPSIS + Generates AppLocker XML data from AaronLocker policies. + + .DESCRIPTION + Generates AppLocker XML data from AaronLocker policies. + + .PARAMETER PolicyName + Name of the AaronLocker policy to work with. + + .PARAMETER Policy + The AaronLocker policy object to work with. + Those objects are returned by 'Get-ALPolicy'. + + .PARAMETER EnforcementMode + Whether the generated XML is being enforced, audited or unconfigured. + + .EXAMPLE + PS C:\> ConvertTo-ALAppLockerXML + + Returns the AppLocker XML of the currently active policy. +#> + [OutputType([System.String])] + [CmdletBinding(DefaultParameterSetName = 'name')] + Param ( + [Parameter(ParameterSetName = 'name')] + [string] + $PolicyName, + + [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'policy')] + [AaronLocker.Policy[]] + $Policy, + + [AaronLocker.EnforcementMode] + $EnforcementMode = [AaronLocker.EnforcementMode]::NotConfigured + ) + + begin + { + if ($PSCmdlet.ParameterSetName -eq 'name') + { + try { $PolicyName = Resolve-ALPolicy -PolicyName $PolicyName } + catch { Write-ALError -ErrorRecord $_ -Terminate } + } + } + process + { + #region Process by name + if ($PolicyName) + { + # Should only return one policy ever - Resolve-ALPolicy will only resolve to one policy currently. + # Still looping in case this design decision changes later on. + $policyObjects = Get-ALPolicy -PolicyName $PolicyName + foreach ($policyObject in $policyObjects) + { + Write-Verbose "Returning XML of $($policyObject.Name)" + $policyObject.GetXml($EnforcementMode) + } + } + #endregion Process by name + + #region Process by object + foreach ($policyObject in $Policy) + { + Write-Verbose "Returning XML of $($policyObject.Name)" + $policyObject.GetXml($EnforcementMode) + } + #endregion Process by object + } +} \ No newline at end of file diff --git a/AaronLocker/functions/export/Export-ALAppLockerPolicy.ps1 b/AaronLocker/functions/export/Export-ALAppLockerPolicy.ps1 new file mode 100644 index 0000000..e55e863 --- /dev/null +++ b/AaronLocker/functions/export/Export-ALAppLockerPolicy.ps1 @@ -0,0 +1,86 @@ +function Export-ALAppLockerPolicy +{ +<# + .SYNOPSIS + Export AaronLocker policies to AppLocker rule XML file. + + .DESCRIPTION + Export AaronLocker policies to AppLocker rule XML file. + This will create three files for each policy in the targetd folder: + - AuditOnly + - Enabled + - NotConfigured + + .PARAMETER PolicyName + Name of the AaronLocker policy to work with. + + .PARAMETER Policy + The AaronLocker policy object to work with. + Those objects are returned by 'Get-ALPolicy'. + + .PARAMETER Path + The path to a folder to export to. + Folder must exist, do not specify a file. + By default exports to the current folder. + + .EXAMPLE + PS C:\> Export-ALAppLockerPolicy + + Exports the currently active policy to the current folder, creating three XML files. + + .EXAMPLE + PS C:\> Get-ALPolicy -List | Export-ALAppLockerPolicy -Path C:\Policies + + Exports all policies into XML files into the C:\Policies folder. +#> + [CmdletBinding(DefaultParameterSetName = 'name')] + Param ( + [Parameter(ParameterSetName = 'name')] + [string] + $PolicyName, + + [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'policy')] + [AaronLocker.Policy[]] + $Policy, + + [string] + $Path = "." + ) + + begin + { + if ($PSCmdlet.ParameterSetName -eq 'name') + { + try { $PolicyName = Resolve-ALPolicy -PolicyName $PolicyName } + catch { Write-ALError -ErrorRecord $_ -Terminate } + } + try + { + $resolvedPath = Resolve-ALPath -Path $Path -Provider FileSystem -SingleItem + if (-not (Get-Item -Path $resolvedPath).PSIsContainer) { throw "Path is not a folder: $($resolvedPath)" } + } + catch { Write-ALError -ErrorRecord $_ -Terminate } + } + process + { + #region Process by name + if ($PolicyName) + { + Write-Verbose "Exporting the policy: $($PolicyName)" + ConvertTo-ALAppLockerXML -PolicyName $PolicyName -EnforcementMode NotConfigured | Set-Content -Path "$($resolvedPath)\$($PolicyName).NotConfigured.xml" -Encoding Unicode -NoNewline + ConvertTo-ALAppLockerXML -PolicyName $PolicyName -EnforcementMode AuditOnly | Set-Content -Path "$($resolvedPath)\$($PolicyName).AuditOnly.xml" -Encoding Unicode -NoNewline + ConvertTo-ALAppLockerXML -PolicyName $PolicyName -EnforcementMode Enabled | Set-Content -Path "$($resolvedPath)\$($PolicyName).Enabled.xml" -Encoding Unicode -NoNewline + } + #endregion Process by name + + #region Process by object + foreach ($policyObject in $Policy) + { + Write-Verbose "Exporting the policy: $($policyObject.Name)" + ConvertTo-ALAppLockerXML -Policy $policyObject -EnforcementMode NotConfigured | Set-Content -Path "$($resolvedPath)\$($policyObject.Name).NotConfigured.xml" -Encoding Unicode -NoNewline + ConvertTo-ALAppLockerXML -Policy $policyObject -EnforcementMode AuditOnly | Set-Content -Path "$($resolvedPath)\$($policyObject.Name).AuditOnly.xml" -Encoding Unicode -NoNewline + ConvertTo-ALAppLockerXML -Policy $policyObject -EnforcementMode Enabled | Set-Content -Path "$($resolvedPath)\$($policyObject.Name).Enabled.xml" -Encoding Unicode -NoNewline + } + #endregion Process by object + } +} \ No newline at end of file diff --git a/AaronLocker/functions/policy/Export-ALPolicy.ps1 b/AaronLocker/functions/policy/Export-ALPolicy.ps1 new file mode 100644 index 0000000..cef5c57 --- /dev/null +++ b/AaronLocker/functions/policy/Export-ALPolicy.ps1 @@ -0,0 +1,100 @@ +function Export-ALPolicy +{ +<# + .SYNOPSIS + Exports AaronLocker policies to file. + + .DESCRIPTION + Exports AaronLocker policies to file. + This is NOT an export of AppLocker compatible data. + Use this to transport an entire AaronLocker policy from one computer to another. + It will require using Import-ALPolicyObject on the target machine to reuse it. + + .PARAMETER PolicyName + Name of the AaronLocker policy to work with. + + .PARAMETER Policy + The AaronLocker policy objects to export. + + .PARAMETER Path + The path to export to. + Either specify the full file path or the folder in which to place it. + In either way, the folder must exist, if no filename was speccified it will be created based on the policy name. + + .EXAMPLE + PS C:\> Export-ALPolicy + + Exports the current policy into the current path under its own name. + + .EXAMPLE + PS C:\> Get-ALPolicy -List | Export-ALPolicy -Path C:\policies + + Exports all managed policies into the C:\policies folder, each under its own name. + + .EXAMPLE + PS C:\> Export-ALPolicy -PolicyName 'OneDrive' -Path .\OneDrive.xml + + Exports the OneDrive policy to the file OneDrive.xml in the current folder. + + .EXAMPLE + PS C:\> Get-ALPolicy -List | Export-ALPOlicy -Path { "C:\policies\{0} {1}.xml" -f (Get-Date -Format 'yyyy-MM-dd'), $_.Name } + + Exports all managed policies into the c:\policies, each timestamped and under its own name with an xml extension. +#> + [CmdletBinding(DefaultParameterSetName = 'name')] + Param ( + [Parameter(ParameterSetName = 'name')] + [string] + $PolicyName, + + [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'policy')] + [AaronLocker.Policy[]] + $Policy, + + [Parameter(ValueFromPipelineByPropertyName = $true)] + [string] + $Path = "." + ) + + begin + { + try { $PolicyName = Resolve-ALPolicy -PolicyName $PolicyName } + catch { Write-ALError -ErrorRecord $_ -Terminate } + + $isFolder = $false + if (Test-Path $resolvedPath) + { + $item = Get-Item -Path $resolvedPath + if ($item.PSIsContainer) { $isFolder = $true } + } + } + process + { + switch ($PSCmdlet.ParameterSetName) + { + 'name' + { + try { $resolvedPath = Resolve-ALPath -Path $Path -Provider FileSystem -SingleItem -NewChild } + catch { throw } + + if ($isFolder) { $resolvedPath = Join-Path $resolvedPath "$($PolicyName).policy.clixml" } + Write-Verbose "Exporting policy $($PolicyName) to $($resolvedPath)" + $script:_PolicyData[$PolicyName] | Export-Clixml -Path $resolvedPath + } + 'policy' + { + # Do in process, not in begin, due to pipeline support + try { $resolvedPath = Resolve-ALPath -Path $Path -Provider FileSystem -SingleItem -NewChild } + catch { throw } + + foreach ($policyItem in $Policy) + { + $tempOutPath = $resolvedPath + if ($isFolder) { $tempOutPath = Join-Path $tempOutPath "$($policyItem.Name).policy.clixml" } + Write-Verbose "Exporting policy $($policyItem.Name) to $($tempOutPath)" + $policyItem | Export-Clixml -Path $resolvedPath + } + } + } + } +} diff --git a/AaronLocker/functions/policy/Get-ALPolicy.ps1 b/AaronLocker/functions/policy/Get-ALPolicy.ps1 new file mode 100644 index 0000000..13581d5 --- /dev/null +++ b/AaronLocker/functions/policy/Get-ALPolicy.ps1 @@ -0,0 +1,56 @@ +function Get-ALPolicy +{ +<# + .SYNOPSIS + Returns the current policy object. + + .DESCRIPTION + Returns the current policy object. + Can instead list the available policy sets stored on the computer. + + Note on policies: + AaronLocker manages rules in datasets called "policy". + These can in fact be converted into AppLocker policies using the various tools provided in this module. + This means: + - Any number of rules (one or hundreds) can make up an AaronLocker "policy" + - Any number of policies (one, two, dozens) can be converted into an AppLocker policy + AaronLocker policies are persisted on disk and can be updated at any time later on. + + .PARAMETER PolicyName + The name to filter the policies by. + + .EXAMPLE + PS C:\> Get-ALPolicy + + Shows the current policy + + .EXAMPLE + PS C:\> Get-ALPolicy -List + + List all locally available policies + + .EXAMPLE + PS C:\> Get-ALPolicy -List -PolicyName OneDrive + + Return only the available info on the policy named "OneDrive" +#> + [OutputType([AaronLocker.Policy])] + [CmdletBinding()] + param ( + [Parameter(Position = 0)] + [string] + $PolicyName + ) + + process + { + if (-not $PSBoundParameters.ContainsKey("PolicyName")) + { + $script:_PolicyData[$script:config.ActivePolicy] + } + else + { + $script:_PolicyData.Values | Where-Object Name -Like $PolicyName + } + } +} \ No newline at end of file diff --git a/AaronLocker/functions/policy/Import-ALPolicy.ps1 b/AaronLocker/functions/policy/Import-ALPolicy.ps1 new file mode 100644 index 0000000..f4a0a3b --- /dev/null +++ b/AaronLocker/functions/policy/Import-ALPolicy.ps1 @@ -0,0 +1,94 @@ +function Import-ALPolicy +{ +<# + .SYNOPSIS + Imports AaronLocker policies from file. + + .DESCRIPTION + Imports AaronLocker policies from file. + Only applies to AaronLocker files generated by Export-ALPolicy + + .PARAMETER Path + Path from which to import policies. + Supports wildcards. + + .PARAMETER Force + Overwrite existing policy if present + + .PARAMETER Activate + Configures the imported policy as the new active (=default) policy. + + .EXAMPLE + PS C:\> Import-ALPolicy -Path '.\OneDrive.policy.clixml' + + Imports the policy stored in OneDrive.policy.clixml. + + .EXAMPLE + PS C:\> Import-ALPolicy -Path C:\Import\*.policy.clixml -Force + + Imports all policy export files in C:\Import and imports them, overwriting any policies that may already exist. + + .EXAMPLE + PS C:\> Import-ALPolicy -Path '.\OneDrive.policy.clixml' -Activate + + Imports the policy stored in OneDrive.policy.clixml and selects it as the new activated (default) policy. +#> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] + [string[]] + $Path, + + [switch] + $Force, + + [switch] + $Activate + ) + process + { + foreach ($pathItem in $Path) + { + try { $resolvedPaths = Resolve-ALPath -Path $pathItem -Provider FileSystem } + catch + { + $PSCmdlet.WriteError($_) + continue + } + + foreach ($resolvedPath in $resolvedPaths) + { + Write-Verbose "Processing import from $($resolvedPath)" + if ((Get-Item $resolvedPath).PSIsContainer) + { + Write-Verbose "Is folder, skipping" + continue + } + + try { $policyData = Import-Clixml -Path $resolvedPath -ErrorAction Stop } + catch + { + Write-ALError -ErrorRecord $_ -Message "Failed to import, possibly corrupt or bad data! : $_" + continue + } + + if ($policyData.GetType().FullName -ne 'AaronLocker.Policy') + { + Write-ALError -Message "Incorrect data type! Expected , received <$($policyData.GetType().FullName)>" + continue + } + + if ($script:_PolicyData.ContainsKey($policyData.Name) -and -not $Force) + { + Write-ALError -Message "Policy $($policyData.Name) already exists! Use the '-Force' parameter to overwrite it" + continue + } + + $script:_PolicyData[$policyData.Name] = $policyData + Update-PolicyFile -PolicyName $policyData.Name + + if ($Activate) { Set-ALActivePolicy -PolicyName $policyData.Name } + } + } + } +} diff --git a/AaronLocker/functions/policy/New-ALPolicy.ps1 b/AaronLocker/functions/policy/New-ALPolicy.ps1 new file mode 100644 index 0000000..1d5f190 --- /dev/null +++ b/AaronLocker/functions/policy/New-ALPolicy.ps1 @@ -0,0 +1,72 @@ +function New-ALPolicy +{ +<# + .SYNOPSIS + Create a new AaronLocker policy + + .DESCRIPTION + Create a new AaronLocker policy. + This policy can then receive rules and be converted to other, useful formats. + + .PARAMETER PolicyName + The name of the AaronLocker policy to create. + + .PARAMETER Description + A suitable description of the policy + + .PARAMETER Force + Overwrite existing policy if present + + .PARAMETER Activate + Configures the created policy as the new active (=default) policy. + + .EXAMPLE + PS C:\> New-ALPolicy -PolicyName OneDrive + + Creates a new AaronLocker policy named OneDrive. + + .EXAMPLE + PS C:\> New-ALPolicy -PolicyName TwoDrive -Description 'Some Text' -Force -Activate + + Creates a new AaronLocker policy named TwoDrive, with some arbitrary description. + It will overwrite any already existing policy of that name and it will set the new policy as the default policy to work against. +#> + [CmdletBinding()] + Param ( + [Parameter(Mandatory = $true)] + [string] + $PolicyName, + + [string] + $Description, + + [switch] + $Force, + + [switch] + $Activate + ) + + begin + { + if (-not $Force -and $script:_PolicyData.ContainsKey($PolicyName)) + { + Write-ALError -Message "Policy $PolicyName exists already. Use '-Force' to overwrite." -Terminate + } + } + process + { + $policy = New-Object AaronLocker.Policy + $policy.Name = $PolicyName + $policy.Description = $Description + $script:_PolicyData[$PolicyName] = $policy + Update-PolicyFile -PolicyName $policy.Name + } + end + { + if ($Activate) + { + Set-ALActivePolicy -PolicyName $PolicyName + } + } +} diff --git a/AaronLocker/functions/policy/Remove-ALPolicy.ps1 b/AaronLocker/functions/policy/Remove-ALPolicy.ps1 new file mode 100644 index 0000000..915a492 --- /dev/null +++ b/AaronLocker/functions/policy/Remove-ALPolicy.ps1 @@ -0,0 +1,95 @@ +function Remove-ALPolicy +{ +<# + .SYNOPSIS + Removes an AaronLocker policy. + + .DESCRIPTION + Removes an AaronLocker policy. + This removes the object in memory and the backing file from disk. + Please note that this does NOT have any effect on actively configured AppLocker policies. + This command is designed to clear legacy policies. + + .PARAMETER PolicyName + The name of the policy to remove. + + .PARAMETER Policy + A policy object returned by Get-ALPolicy. + + .EXAMPLE + PS C:\> Remove-ALPolicy -PolicyName 'OneDrive' + + Removes an AaronLocker policy named OneDrive. +#> + [CmdletBinding(SupportsShouldProcess = $true)] + Param ( + [Parameter(Mandatory = $true, ParameterSetName = 'name')] + [string] + $PolicyName, + + [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'policy')] + [AaronLocker.Policy[]] + $Policy + ) + + process + { + #region Delete by name + if ($PolicyName) + { + Write-Verbose "$($PolicyName): Starting to process" + try { $PolicyName = Resolve-ALPolicy -PolicyName $PolicyName } + catch { Write-ALError -ErrorRecord $_ -Terminate } + + # Implement ShouldProcess to terminate if it should NOT proceed + if (-not $PSCmdlet.ShouldProcess($PolicyName, 'Removing AaronLocker Policy')) { return } + + Write-Verbose "$($PolicyName): Removing policy object" + $null = $script:_PolicyData.Remove($PolicyName) + if (Test-Path "$($script:_RulesFolder)\$($PolicyName).policy.clixml") + { + Write-Verbose "$($PolicyName): Removing policy file" + Remove-Item "$($script:_RulesFolder)\$($PolicyName).policy.clixml" + } + else { Write-Verbose "$($PolicyName): No policy file found!" } + } + #endregion Delete by name + + #region Delete by object + foreach ($policyObject in $Policy) + { + Write-Verbose "$($policyObject.Name): Starting to process" + try { $polName = Resolve-ALPolicy -PolicyName $policyObject.Name } + catch { Write-ALError -ErrorRecord $_ -Continue } + + # Implement ShouldProcess to terminate if it should NOT proceed + if (-not $PSCmdlet.ShouldProcess($polName, 'Removing AaronLocker Policy')) { continue } + + Write-Verbose "$($polName): Removing policy object" + $null = $script:_PolicyData.Remove($polName) + if (Test-Path "$($script:_RulesFolder)\$($polName).policy.clixml") + { + Write-Verbose "$($polName): Removing policy file" + Remove-Item "$($script:_RulesFolder)\$($polName).policy.clixml" + } + else { Write-Verbose "$($polName): No policy file found!" } + } + #endregion Delete by object + } + end + { + if ($script:_PolicyData.Keys -notcontains $script:config.ActivePolicy) + { + if ($script:_PolicyData.Keys.Count -gt 0) + { + Write-Warning ("The active policy has been removed, enabling {0} as active policy" -f $script:_PolicyData.Keys[0]) + Set-ALActivePolicy -PolicyName $script:_PolicyData.Keys[0] + } + else + { + Write-Warning "No policy left! Creating a new 'default' policy and enabling it as active policy." + New-ALPolicy -PolicyName 'default' -Description 'The default AaronLocker policy' -Activate + } + } + } +} diff --git a/AaronLocker/functions/policy/Set-ALActivePolicy.ps1 b/AaronLocker/functions/policy/Set-ALActivePolicy.ps1 new file mode 100644 index 0000000..d4fbc5a --- /dev/null +++ b/AaronLocker/functions/policy/Set-ALActivePolicy.ps1 @@ -0,0 +1,56 @@ +function Set-ALActivePolicy +{ +<# + .SYNOPSIS + Select the currently active policy + + .DESCRIPTION + Select the currently active policy. + AaronLocker supports maintaining multiple sets of policies in parallel. + Use this command to switch the default policy, in order to avoid having to explicitly specify it. + + .PARAMETER PolicyName + The name of the policy to enable. + Use 'Get-ALPolicy -List' to receive a list of currently available policies. + Use 'New-ALPolicy' to define a new policy + + .PARAMETER Policy + The policy object to set as active policy + Generate using 'Get-ALPolicy -List'. + Foreign policy objects need to be imported first before enablling them as policy object to use. + + .EXAMPLE + PS C:\> Set-ALActivePolicy -PolicyName 'OneDrive' + + Switches the currently active policy to the policy named OneDrive. + + .EXAMPLE + PS C:\> Get-ALPOlicy -List OneDrive | Set-ALActivePolicy + + Switches the currently active policy to the policy named OneDrive. +#> + [CmdletBinding(DefaultParameterSetName = 'name')] + param ( + [Parameter(Mandatory = $true, ParameterSetName = 'name')] + [string] + $PolicyName, + + [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'policy')] + [AaronLocker.Policy] + $Policy + ) + + begin + { + # Explicit implementation, since default policy obviously doesn't work here + if ($PolicyName -and ((Get-ALPolicy -PolicyName "*").Name -notcontains $PolicyName)) + { + throw "Policy $($PolicyName) not found! Known policies: $((Get-ALPolicy -PolicyName "*").Name -join ',')" + } + } + process + { + if ($PolicyName) { Set-ALConfiguration -ActivePolicy $PolicyName } + else { Set-ALConfiguration -ActivePolicy $Policy.Name } + } +} \ No newline at end of file diff --git a/AaronLocker/functions/readme.md b/AaronLocker/functions/readme.md new file mode 100644 index 0000000..280e32c --- /dev/null +++ b/AaronLocker/functions/readme.md @@ -0,0 +1,7 @@ +# Functions + +This is the folder where the functions go. + +Depending on the complexity of the module, it is recommended to subdivide them into subfolders. + +The module will pick up all .ps1 files recursively \ No newline at end of file diff --git a/AaronLocker/functions/ruledata/Add-ALRule.ps1 b/AaronLocker/functions/ruledata/Add-ALRule.ps1 new file mode 100644 index 0000000..c06c2b2 --- /dev/null +++ b/AaronLocker/functions/ruledata/Add-ALRule.ps1 @@ -0,0 +1,77 @@ +function Add-ALRule +{ +<# + .SYNOPSIS + Adds a finished rule object to an AaronLocker policy. + + .DESCRIPTION + Adds a finished rule object to an AaronLocker policy. + This allows cloning rules from one policy to another. + + .PARAMETER Rule + The rule(s) to add to the policy. + + .PARAMETER PolicyName + Name of the AaronLocker policy to work with. + + .PARAMETER Force + Force overwriting existing rules with the same label. + + .EXAMPLE + PS C:\> Add-ALRule -Rule $Rule + + Adds the rule stored in $Rule to the default policy. + + .EXAMPLE + PS C:\> Get-ALRule -PolicyName OneDrive | Add-ALRule -PolicyName ClientMGMT + + Copies all rules from the policy 'OneDrive' to the policy 'ClientMGMT' +#> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [AaronLocker.RuleBase[]] + $Rule, + + [string] + $PolicyName, + + [switch] + $Force + ) + + begin + { + try { $PolicyName = Resolve-ALPolicy -PolicyName $PolicyName } + catch { Write-ALError -ErrorRecord $_ -Terminate } + + $policy = Get-ALPolicy -PolicyName $PolicyName + } + process + { + foreach ($ruleItem in $Rule) + { + if ($policy.Rules.Label -contains $ruleItem.Label) + { + if (-not $Force) + { + Write-Warning "Rule '$($ruleItem.Label)' already exists in policy $($PolicyName), skipping" + continue + } + else + { + $oldRule = $policy.Rules | Where-Object Label -EQ $ruleItem.Label + Write-Verbose "Removing rule '$($oldRule.Label)' from $($PolicyName)" + $policy.Rules.Remove($oldRule) + } + } + + Write-Verbose "Adding rule '$($ruleItem.Label)' to $($PolicyName)" + $policy.Rules.Add($ruleItem.Clone()) + } + } + end + { + Update-PolicyFile -PolicyName $PolicyName + } +} \ No newline at end of file diff --git a/AaronLocker/functions/ruledata/Add-ALRuleHash.ps1 b/AaronLocker/functions/ruledata/Add-ALRuleHash.ps1 new file mode 100644 index 0000000..17d9fb0 --- /dev/null +++ b/AaronLocker/functions/ruledata/Add-ALRuleHash.ps1 @@ -0,0 +1,123 @@ +function Add-ALRuleHash +{ +<# + .SYNOPSIS + Adds a new hash rule to the list of explicit rules to include. + + .DESCRIPTION + Adds a new hash rule to the list of explicit rules to include. + Use this to include rules generated from other soures, such as event data or gathered from another machine. + + .PARAMETER Collection + The type of item is being allowed to execute. + + .PARAMETER Label + The label for the rule. + Must be unique from all other rules. + + .PARAMETER Description + A description of what the rule allows. + + .PARAMETER Hash + The hash of the file being allowed. + + .PARAMETER FileName + The name of the file being allowed. + Just the filename, not its full path. + + .PARAMETER SourceFileLength + Length of the original input file. + An optional way to increase hash assurance. + + .PARAMETER Identity + SID of the user or group this rule applies to. + Defaults to the "users" group (that is: The rule affects all processes not run with elevation) + + .PARAMETER PolicyName + Name of the AaronLocker policy to work with. + + .EXAMPLE + PS C:\> Add-ALRuleHash -Collection Exe -Label $Label -Description $description -Hash $hash -FileName 'file.exe' + + Explicitly creates a rule for file.exe. + + .EXAMPLE + PS C:\> Import-Csv .\rules.csv | Add-ALRuleHash + + Imports all rules stored in the rules.csv file. + Note: The csv must have collumns with names matching the parameter names for this to work. +#> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] + [Alias('RuleCollection')] + [AaronLocker.Scope] + $Collection = 'Default', + + [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] + [Alias('RuleName', 'Name')] + [string] + $Label, + + [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] + [Alias('RuleDesc')] + [string] + $Description, + + [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] + [Alias('HashVal')] + [string] + $Hash, + + [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] + [string] + $FileName, + + [Parameter(ValueFromPipelineByPropertyName = $true)] + [int] + $SourceFileLength, + + [Parameter(ValueFromPipelineByPropertyName = $true)] + [System.Security.Principal.SecurityIdentifier] + $Identity = 'S-1-5-32-545', + + [string] + $PolicyName + ) + + begin + { + try { $PolicyName = Resolve-ALPolicy -PolicyName $PolicyName } + catch { Write-ALError -ErrorRecord $_ -Terminate } + + $policy = Get-ALPolicy -PolicyName $PolicyName + } + process + { + if ($Hash -notmatch '^0x[0-9A-F]{64}$') + { + Write-ALError -Message "Invalid hash! Please specify a full SHA256 hash (e.g.: '0x67A9B...', 64 hex characters behind the 'x')" -Terminate + } + if ($policy.Rules.Label -contains $Label) + { + Write-Warning "Rule '$Label' already exists, skipping" + return + } + + Write-Verbose "Adding rule '$Label'" + $rule = New-Object AaronLocker.HashRule -Property @{ + Collection = $Collection + Label = $Label + Description = $Description + HashValue = $Hash + FileName = $FileName + SourceFileLength = $SourceFileLength + UserOrGroupSid = $Identity + } + if ($rule) { $policy.Rules.Add($rule) } + } + end + { + Update-PolicyFile -PolicyName $PolicyName + } +} diff --git a/AaronLocker/functions/ruledata/Add-ALRulePath.ps1 b/AaronLocker/functions/ruledata/Add-ALRulePath.ps1 new file mode 100644 index 0000000..ad722c9 --- /dev/null +++ b/AaronLocker/functions/ruledata/Add-ALRulePath.ps1 @@ -0,0 +1,136 @@ +function Add-ALRulePath +{ +<# + .SYNOPSIS + Creates an applocker path rule. + + .DESCRIPTION + Creates a rule for applocker that whitelists or blacklists a specific path. + + Note: For maximum security, consider avoiding path rules where possible and use publisher rules instead. + + .PARAMETER Path + The path to create the rule for. + + .PARAMETER Label + The label of the rule to set. + Labels must be unique within a given policy. + + .PARAMETER Exceptions + Any paths within the specified path to exempt from this rule. + Use this to selectively exclude paths from the outer rule. + This is typically used for whitelisting paths such as the Windows folder and then excluding any folders users have write access to. + + .PARAMETER Description + A description of what the rule allows or forbids. + + .PARAMETER Identity + SID of the user or group this rule applies to. + Defaults to the "users" group (that is: The rule affects all processes not run with elevation) + + .PARAMETER Id + Unique ID of the rule specified. + Highly optional - if left empty, it will be automatically set on policy creation. + + .PARAMETER Collection + The type of item is being allowed or denied to execute. + + .PARAMETER Action + Whether to allow or deny execution. + + .PARAMETER PolicyName + Name of the AaronLocker policy to work with. + + .EXAMPLE + PS C:\> Add-ALRulePath -Path 'C:\Fabrikam\Custom\*' -Label 'Fabrikam - Custom Application' + + Creates a whitelist entry for applications executed from the 'C:\Fabrikam\Custom' folder. + Note: This is a pretty bad idea if regular users have write access to this folder. + + .EXAMPLE + PS C:\> Import-Csv .\pathrules.csv | Add-ALRulePath + + Imports all rules stored in the specified csv. + Note: The csv files needs to have column headers with exactly the same name as this command's parameters in order for this to work. + + .EXAMPLE + PS C:\> Add-ALRulePath -Path 'C:\Fabrikam\Custom\*' -Label 'Fabrikam - Custom Application' -Exceptions 'C:\Fabrikam\Custom\Input\*', 'C:\Fabrikam\Custom\Data\*' + + Creates a whitelist entry for applications executed from the 'C:\Fabrikam\Custom' folder. + The following folders however are explicitly excluded from this exception: + - C:\Fabrikam\Custom\Input\* + - C:\Fabrikam\Custom\Data\* +#> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] + [string] + $Path, + + [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] + [Alias('RuleName', 'Name')] + [string] + $Label, + + [Parameter(ValueFromPipelineByPropertyName = $true)] + [string[]] + $Exceptions, + + [Parameter(ValueFromPipelineByPropertyName = $true)] + [string] + $Description, + + [Parameter(ValueFromPipelineByPropertyName = $true)] + [System.Security.Principal.SecurityIdentifier] + $Identity = 'S-1-5-32-545', + + [Parameter(ValueFromPipelineByPropertyName = $true)] + [System.Guid] + $Id = [guid]::Empty, + + [Parameter(ValueFromPipelineByPropertyName = $true)] + [Alias('RuleCollection')] + [AaronLocker.Scope] + $Collection = 'Default', + + [Parameter(ValueFromPipelineByPropertyName = $true)] + [AaronLocker.Action] + $Action = 'Allow', + + [string] + $PolicyName + ) + + begin + { + try { $PolicyName = Resolve-ALPolicy -PolicyName $PolicyName } + catch { Write-ALError -ErrorRecord $_ -Terminate } + + $policy = Get-ALPolicy -PolicyName $PolicyName + } + process + { + if ($policy.Rules.Label -contains $Label) + { + Write-Warning "Rule '$Label' already exists, skipping" + return + } + + Write-Verbose "Adding path rule $label" + $rule = New-Object AaronLocker.PathRule -Property @{ + Path = $Path + Exceptions = $Exceptions + Label = $Label + Description = $Description + UserOrGroupSid = $Identity + Id = $Id + Collection = $Collection + Action = $Action + } + if ($rule) { $policy.Rules.Add($rule) } + } + end + { + Update-PolicyFile -PolicyName $PolicyName + } +} \ No newline at end of file diff --git a/AaronLocker/functions/ruledata/Add-ALRulePublisher.ps1 b/AaronLocker/functions/ruledata/Add-ALRulePublisher.ps1 new file mode 100644 index 0000000..38221cc --- /dev/null +++ b/AaronLocker/functions/ruledata/Add-ALRulePublisher.ps1 @@ -0,0 +1,183 @@ +function Add-ALRulePublisher +{ +<# + .SYNOPSIS + Adds a publisher rule to the list of explicitly included rules. + + .DESCRIPTION + Adds a publisher rule to the list of explicitly included rules. + These will become part of the output generated from New-ALPolicyScan. + + .PARAMETER Label + Text that is incorporated into the rule name and description. + + .PARAMETER PublisherName + Literal canonical name identifying a publisher to trust. + + .PARAMETER ProductName + Restrict trust just to that product by that publisher. + + .PARAMETER BinaryName + Restrict trust to a specific internal file name. + + .PARAMETER FileVersion + The minimum allowed file version for the specified file. + + .PARAMETER Collection + The type of item is being allowed or denied to execute. + + .PARAMETER Exemplar + Path to a signed file, the publisher to trust is extracted from that signature. + + .PARAMETER UseProduct + Whether to restrict publisher trust only to that file's product name. + + .PARAMETER Description + A description of what the rule allows or forbids. + + .PARAMETER Identity + SID of the user or group this rule applies to. + Defaults to the "users" group (that is: The rule affects all processes not run with elevation) + + .PARAMETER Id + Unique ID of the rule specified. + Highly optional - if left empty, it will be automatically set on policy creation. + + .PARAMETER Action + Whether to allow or deny execution. + + .PARAMETER PolicyName + Name of the AaronLocker policy to work with. + + .EXAMPLE + PS C:\> Add-ALRulePublisher -Label 'Trust all Contoso' -PublisherName 'O=CONTOSO, L=SEATTLE, S=WASHINGTON, C=US' + + Trust everything by a specific publisher + + .EXAMPLE + PS C:\> Add-ALRulePublisher -Label 'Trust all Contoso DLLs' -PublisherName 'O=CONTOSO, L=SEATTLE, S=WASHINGTON, C=US' -Collection Dll + + Trust all DLLs by a specific publisher + + .EXAMPLE + PS C:\> Add-ALRulePublisher -Label 'Trust all CUSTOMAPP files published by Contoso' -PublisherName 'O=CONTOSO, L=SEATTLE, S=WASHINGTON, C=US' -ProductName 'CUSTOMAPP' + + Trust a specific product published by a specific publisher + + .EXAMPLE + PS C:\> Add-ALRulePublisher -Label 'Trust Contoso's SAMPLE.DLL in CUSTOMAPP' -PublisherName 'O=CONTOSO, L=SEATTLE, S=WASHINGTON, C=US' -ProductName 'CUSTOMAPP' -BinaryName 'SAMPLE.DLL' -FileVersion '10.0.15063.0' -Collection 'Dll' + + Trust a specific version of a specific signed file by a specific publisher/product + + .EXAMPLE + PS C:\> Add-ALRulePublisher -Label 'Trust the publisher of Autoruns.exe' -Exemplar 'C:\Program Files\Sysinternals\Autoruns.exe' + + Trust everything signed by the same publisher as the exemplar file (Autoruns.exe) + + .EXAMPLE + PS C:\> Add-ALRulePublisher -Label 'Trust everything with the same publisher and product as LuaBuglight.exe' -Exemplar 'C:\Program Files\Utils\LuaBuglight.exe' -UseProduct + + Trust everything with the same publisher and product as the exemplar file (LuaBuglight.exe) + + .EXAMPLE + PS C:\> Import-Csv .\rules.csv | Add-ALRulePulisher + + Adds all rules stored in rules.csv. + Note: The Csv must contain column names matching the parameters of this command. +#> + [CmdletBinding()] + param ( + [Parameter(ValueFromPipelineByPropertyName = $true, Mandatory = $true)] + [Alias('RuleName', 'Name')] + [string] + $Label, + + [Parameter(ValueFromPipelineByPropertyName = $true, ParameterSetName = 'Explicit', Mandatory = $true)] + [string] + $PublisherName, + + [Parameter(ValueFromPipelineByPropertyName = $true, ParameterSetName = 'Explicit')] + [string] + $ProductName, + + [Parameter(ValueFromPipelineByPropertyName = $true, ParameterSetName = 'Explicit')] + [string] + $BinaryName, + + [Parameter(ValueFromPipelineByPropertyName = $true, ParameterSetName = 'Explicit')] + [Version] + $FileVersion, + + [Parameter(ValueFromPipelineByPropertyName = $true, ParameterSetName = 'Explicit')] + [Alias('RuleCollection')] + [AaronLocker.Scope] + $Collection = 'Default', + + [Parameter(ValueFromPipelineByPropertyName = $true, ParameterSetName = 'Reference', Mandatory = $true)] + [string] + $Exemplar, + + [Parameter(ValueFromPipelineByPropertyName = $true, ParameterSetName = 'Reference')] + [switch] + $UseProduct, + + [Parameter(ValueFromPipelineByPropertyName = $true)] + [string] + $Description, + + [Parameter(ValueFromPipelineByPropertyName = $true)] + [System.Security.Principal.SecurityIdentifier] + $Identity = 'S-1-5-32-545', + + [Parameter(ValueFromPipelineByPropertyName = $true)] + [System.Guid] + $Id = [guid]::Empty, + + [Parameter(ValueFromPipelineByPropertyName = $true)] + [AaronLocker.Action] + $Action = 'Allow', + + [string] + $PolicyName + ) + + begin + { + try { $PolicyName = Resolve-ALPolicy -PolicyName $PolicyName } + catch { Write-ALError -ErrorRecord $_ -Terminate } + + $policy = Get-ALPolicy -PolicyName $PolicyName + } + process + { + if ($policy.Rules.Label -contains $Label) + { + Write-Warning "Rule '$Label' already exists, skipping" + return + } + + $ruleHash = @{ + Label = $Label + Description = $Description + UserOrGroupSid = $Identity + Id = $Id + Collection = $Collection + Action = $Action + } + if ($PublisherName) { $ruleHash["PublisherName"] = $PublisherName } + if ($ProductName) { $ruleHash["ProductName"] = $ProductName } + if ($BinaryName) { $ruleHash["BinaryName"] = $BinaryName } + if ($FileVersion) { $ruleHash["FileVersion"] = $FileVersion } + if ($Collection) { $ruleHash["RuleCollection"] = $Collection } + if ($Exemplar) { $ruleHash["Exemplar"] = $Exemplar } + if ($UseProduct) { $ruleHash["UseProduct"] = $UseProduct } + + Write-Verbose "Adding publisher rule $label" + $rule = New-Object AaronLocker.PublisherRule -Property $ruleHash + if ($rule) { $policy.Rules.Add($rule) } + } + end + { + Update-PolicyFile -PolicyName $PolicyName + } +} diff --git a/AaronLocker/functions/ruledata/Add-ALRuleSourcePath.ps1 b/AaronLocker/functions/ruledata/Add-ALRuleSourcePath.ps1 new file mode 100644 index 0000000..4274c46 --- /dev/null +++ b/AaronLocker/functions/ruledata/Add-ALRuleSourcePath.ps1 @@ -0,0 +1,115 @@ +function Add-ALRuleSourcePath +{ +<# + .SYNOPSIS + Adds a custom source path rule to the list of rules to include when generating AppLocker rules. + + .DESCRIPTION + Adds a custom source path rule to the list of rules to include when generating AppLocker rules. + + A source path rule is a path rule that - when resolved - scans the path it points to for executables to whitelist. + It prefers generating publisher rules where able, when failing this, it will instead create file hash rules. + + .PARAMETER Label + Incorporated into rules' names and descriptions. + + .PARAMETER Path + Identifies one or more paths. + If a path is a directory, rules are generated for the existing files in that directory. + If a path is to a file, a rule is generated for that file. + + .PARAMETER NoRecurse + If specified, rules are generated only for the files in the specified directory or directories. + Otherwise, rules are also generated for files in subdirectories of the specified directory or directories. + + .PARAMETER EnforceMinVersion + If specified, generated publisher rules enforce a minimum file version based on the file versions of the observed files. + Otherwise, the generated rules do not enforce a minimum file version. + + .PARAMETER Identity + SID of the user or group this rule applies to. + Defaults to the "users" group (that is: The rule affects all processes not run with elevation) + + .PARAMETER PolicyName + Name of the AaronLocker policy to work with. + + .EXAMPLE + PS C:\> Add-ALRuleSourcePath -Label 'powershell' -Path "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" + + Creates a path-based rule to allow powershell.exe. + Note: The chosen file for this example is actually signed and should not be allowed using a path rule. + + .EXAMPLE + PS C:\> & .\path_rules.ps1 | ForEach-Object { [PSCustomObject]$_ } | Add-ALRuleSourcePath + + Imports rules defined as hashtables in the specified script. + + .EXAMPLE + PS C:\> Import-Csv .\path_rules.csv | SelectObject Label, @{ N = "Path"; E = { $_.Path.Split(";") }}, @{ N = "NoRecurse"; E = { $_.NoRecurse -eq "True" }}, @{ N = "EnforceMinVersion"; E = { $_.EnforceMinVersion -eq "True" }} | Add-ALRuleSourcePath + + Imports rules from a csv file. + This example assumes from the csv: + - A "Label" column that is filled out for each entry + - A "Path(s)" column that is filled out for each entry. It may contain multiple paths, delimited by a ";" + - Optionally a NoRecurse column, which may have values or may not either. Only "True" will be considered as enabling (casing is being ignored) + - Optionally a EnforceMinVersion column, which may have values or may not either. Only "True" will be considered as enabling (casing is being ignored) +#> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] + [Alias('RuleName', 'Name')] + [string] + $Label, + + [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] + [Alias('Paths')] + [string[]] + $Path, + + [Parameter(ValueFromPipelineByPropertyName = $true)] + [switch] + $NoRecurse, + + [Parameter(ValueFromPipelineByPropertyName = $true)] + [switch] + $EnforceMinVersion, + + [Parameter(ValueFromPipelineByPropertyName = $true)] + [System.Security.Principal.SecurityIdentifier] + $Identity = 'S-1-5-32-545', + + [string] + $PolicyName + ) + + begin + { + try { $PolicyName = Resolve-ALPolicy -PolicyName $PolicyName } + catch { Write-ALError -ErrorRecord $_ -Terminate } + + $policy = Get-ALPolicy -PolicyName $PolicyName + } + process + { + + if ($policy.Rules.Label -contains $Label) + { + Write-Warning "Rule '$Label' already exists, skipping" + return + } + + Write-Verbose "Adding source path rule $label" + $rule = New-Object AaronLocker.SourcePathRule -Property @{ + Label = $Label + Paths = $Path + NoRecurse = $NoRecurse.ToBool() + EnforceMinVersion = $EnforceMinVersion.ToBool() + UserOrGroupSid = $Identity + } + if ($rule) { $policy.Rules.Add($()) } + } + end + { + Update-PolicyFile -PolicyName $PolicyName + } +} diff --git a/AaronLocker/functions/ruledata/Get-ALRule.ps1 b/AaronLocker/functions/ruledata/Get-ALRule.ps1 new file mode 100644 index 0000000..3e4464e --- /dev/null +++ b/AaronLocker/functions/ruledata/Get-ALRule.ps1 @@ -0,0 +1,61 @@ +function Get-ALRule +{ +<# + .SYNOPSIS + Get rules from the specified AaronLocker policy. + + .DESCRIPTION + Get rules from the specified AaronLocker policy. + + .PARAMETER Label + The label of the rule to search by. + + .PARAMETER Type + Only return rules of the specified type(s). + + .PARAMETER PolicyName + Name of the AaronLocker policy to work with. + + .EXAMPLE + PS C:\> Get-ALRule + + Lists all rules under the current policy. + + .EXAMPLE + PS C:\> Get-ALRule -Type Path + + Lists all path rules under the current policy. + + .EXAMPLE + PS C:\> Get-ALRule -Type Hash -PolicyName OneDrive + + Lists all hash rules under the OneDrive policy. +#> + [CmdletBinding()] + Param ( + [string] + $Label = '*', + + [AaronLocker.RuleType[]] + $Type, + + [string] + $PolicyName + ) + + begin + { + try { $PolicyName = Resolve-ALPolicy -PolicyName $PolicyName } + catch { Write-ALError -ErrorRecord $_ -Terminate } + + $policy = Get-ALPolicy -PolicyName $PolicyName + } + process + { + $policy.Rules | Where-Object { + if ($_.Label -notlike $Label) { return $false } + if (($Type) -and ($_.Type -notin $Type)) { return $false } + $true + } + } +} \ No newline at end of file diff --git a/AaronLocker/functions/ruledata/Remove-ALRule.ps1 b/AaronLocker/functions/ruledata/Remove-ALRule.ps1 new file mode 100644 index 0000000..2d33600 --- /dev/null +++ b/AaronLocker/functions/ruledata/Remove-ALRule.ps1 @@ -0,0 +1,101 @@ +function Remove-ALRule +{ +<# + .SYNOPSIS + Removes rules from AaronLocker policies. + + .DESCRIPTION + Removes rules from AaronLocker policies. + + .PARAMETER Rule + The rule(s) to remove from the policy. + Actual comparisson is done using the label property. + + .PARAMETER Label + The label by which to look for rules to remove. + Supports wildcards. + + .PARAMETER PolicyName + Name of the AaronLocker policy to work with. + + .PARAMETER Confirm + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. + + .PARAMETER WhatIf + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. + + .EXAMPLE + PS C:\> Remove-ALRule -Rule $Rule + + Removes the specified rule from the current policy. + + .EXAMPLE + PS C:\> Get-ALRule -PolicyName OneDrive | Remove-ALRule -PolicyName ClientMGMT + + Removes all rules in the policy ClientMGMT that occur in the policy OneDrive +#> + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')] + param ( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [AaronLocker.RuleBase[]] + $Rule, + + [string[]] + $Label, + + [string] + $PolicyName + ) + + begin + { + try { $PolicyName = Resolve-ALPolicy -PolicyName $PolicyName } + catch { Write-ALError -ErrorRecord $_ -Terminate } + + $policy = Get-ALPolicy -PolicyName $PolicyName + } + process + { + #region By Rule Object + foreach ($ruleItem in $Rule) + { + if ($policy.Rules.Label -notcontains $ruleItem.Label) + { + Write-Warning "Rule '$($ruleItem.Label)' does not exist in policy $($PolicyName), skipping" + continue + } + + $oldRule = $policy.Rules | Where-Object Label -EQ $ruleItem.Label + if ($PSCmdlet.ShouldProcess($oldRule.Label, "Remove from $($PolicyName)")) + { + Write-Verbose "Removing rule '$($oldRule.Label)' from $($PolicyName)" + $policy.Rules.Remove($oldRule) + } + } + #endregion By Rule Object + + #region By Rule Name / Label + foreach ($labelItem in $Label) + { + if (-not ($policy.Rules.Label -like $labelItem)) + { + Write-Warning "Could not find a rule matching '$($labelItem)' in policy $($PolicyName), skipping" + continue + } + $oldRules = $policy.Rules | Where-Object Label -EQ $ruleItem.Label + foreach ($oldRule in $oldRules) + { + if ($PSCmdlet.ShouldProcess($oldRule.Label, "Remove from $($PolicyName)")) + { + Write-Verbose "Removing rule '$($oldRule.Label)' from $($PolicyName)" + $policy.Rules.Remove($oldRule) + } + } + } + #endregion By Rule Name / Label + } + end + { + Update-PolicyFile -PolicyName $PolicyName + } +} \ No newline at end of file diff --git a/AaronLocker/internal/functions/Export-Configuration.ps1 b/AaronLocker/internal/functions/Export-Configuration.ps1 new file mode 100644 index 0000000..5f53d66 --- /dev/null +++ b/AaronLocker/internal/functions/Export-Configuration.ps1 @@ -0,0 +1,33 @@ +function Export-Configuration +{ +<# + .SYNOPSIS + Exports the current configuration to file. + + .DESCRIPTION + Exports the current configuration to file. + Should be executed after each configuration change. + + .EXAMPLE + PS C:\> Export-Configuration + + Exports the current configuration to file. +#> + [CmdletBinding()] + Param ( + + ) + + begin + { + $configParent = Split-Path $script:_ConfigPath + if (-not (Test-Path $configParent)) + { + $null = New-Item -Path $configParent -ItemType Directory -Force + } + } + process + { + $script:config | ConvertTo-Json | Set-Content -Path $script:_ConfigPath -Encoding UTF8 + } +} \ No newline at end of file diff --git a/AaronLocker/internal/functions/Import-Configuration.ps1 b/AaronLocker/internal/functions/Import-Configuration.ps1 new file mode 100644 index 0000000..0689d8f --- /dev/null +++ b/AaronLocker/internal/functions/Import-Configuration.ps1 @@ -0,0 +1,37 @@ +function Import-Configuration +{ +<# + .SYNOPSIS + Imports the module configuration from file. + + .DESCRIPTION + Imports the module configuration from file. + The file is a preconfigured path in appdata, allowing the user to control, how the module operates. + + .EXAMPLE + PS C:\> Import-Configuration + + Imports the persisted configuration +#> + [CmdletBinding()] + Param ( + + ) + + process + { + if (Test-Path $script:_ConfigPath) + { + Get-Content -Path $script:_ConfigPath | ConvertFrom-Json + } + else + { + [pscustomobject]@{ + PathAccessChk = "" + KnownAdmins = @() + OutputPath = "$($env:USERPROFILE)\Desktop\AaronLocker" + ActivePolicy = "default" + } + } + } +} \ No newline at end of file diff --git a/AaronLocker/internal/functions/Resolve-ALPath.ps1 b/AaronLocker/internal/functions/Resolve-ALPath.ps1 new file mode 100644 index 0000000..703dfb3 --- /dev/null +++ b/AaronLocker/internal/functions/Resolve-ALPath.ps1 @@ -0,0 +1,112 @@ +function Resolve-ALPath +{ +<# + .SYNOPSIS + Resolves a path. + + .DESCRIPTION + Resolves a path. + Will try to resolve to paths including some basic path validation and resolution. + Will fail if the path cannot be resolved (so an existing path must be reached at). + + .PARAMETER Path + The path to validate. + + .PARAMETER Provider + Ensure the path is of the expected provider. + Allows ensuring one does not operate in the wrong provider. + Common providers include the filesystem, the registry or the active directory. + + .PARAMETER SingleItem + Ensure the path should resolve to a single path only. + This may - intentionally or not - trip up wildcard paths. + + .PARAMETER NewChild + Assumes one wishes to create a new child item. + The parent path will be resolved and must validate true. + The final leaf will be treated as a leaf item that does not exist yet. + + .EXAMPLE + PS C:\> Resolve-ALPath -Path report.log -Provider FileSystem -NewChild -SingleItem + + Ensures the resolved path is a FileSystem path. + This will resolve to the current folder and the file report.log. + Will not ensure the file exists or doesn't exist. + If the current path is in a different provider, it will throw an exception. + + .EXAMPLE + PS C:\> Resolve-ALPath -Path ..\* + + This will resolve all items in the parent folder, whatever the current path or drive might be. +#> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] + [string[]] + $Path, + + [string] + $Provider, + + [switch] + $SingleItem, + + [switch] + $NewChild + ) + + process + { + foreach ($inputPath in $Path) + { + if ($inputPath -eq ".") + { + $inputPath = (Get-Location).Path + } + if ($NewChild) + { + $parent = Split-Path -Path $inputPath + $child = Split-Path -Path $inputPath -Leaf + + try + { + if (-not $parent) { $parentPath = Get-Location -ErrorAction Stop } + else { $parentPath = Resolve-Path $parent -ErrorAction Stop } + } + catch { throw "Failed to resolve path: $_" } + + if ($SingleItem -and (($parentPath | Measure-Object).Count -gt 1)) + { + throw "Could not resolve to only a single parent path!" + } + + if ($Provider -and ($parentPath.Provider.Name -ne $Provider)) + { + throw "Resolved provider is $($parentPath.Provider.Name) when it should be $($Provider)" + } + + foreach ($parentItem in $parentPath) + { + Join-Path $parentItem.ProviderPath $child + } + } + else + { + try { $resolvedPaths = Resolve-Path $inputPath -ErrorAction Stop } + catch { throw "Failed to resolve path" } + + if ($SingleItem -and (($resolvedPaths | Measure-Object).Count -gt 1)) + { + throw "Could not resolve to a single parent path!" + } + + if ($Provider -and ($resolvedPaths.Provider.Name -ne $Provider)) + { + throw "Resolved provider is $($resolvedPaths.Provider.Name) when it should be $($Provider)" + } + + $resolvedPaths.ProviderPath + } + } + } +} \ No newline at end of file diff --git a/AaronLocker/internal/functions/Resolve-ALPolicy.ps1 b/AaronLocker/internal/functions/Resolve-ALPolicy.ps1 new file mode 100644 index 0000000..6b9fc3d --- /dev/null +++ b/AaronLocker/internal/functions/Resolve-ALPolicy.ps1 @@ -0,0 +1,36 @@ +function Resolve-ALPolicy +{ +<# + .SYNOPSIS + Helper that resolves an AaronLocker policy name. + + .DESCRIPTION + Helper that resolves an AaronLocker policy name. + Use this to avoid hardcoding default values into every function. + Specifically, it avoids having to insert explicit calls to the configuration in every function, making it easier to later apply changes to that. + + .PARAMETER PolicyName + The name to resolve, can be empty string. + + .EXAMPLE + PS C:\> Resolve-ALPolicy -PolicyName $PolicyName + + Returns the resulting policy name to use. +#> + [OutputType([System.String])] + [CmdletBinding()] + param ( + [AllowEmptyString()] + [string] + $PolicyName + ) + + if (-not $PolicyName) { return $script:config.ActivePolicy } + + if ((Get-ALPolicy -PolicyName "*").Name -notcontains $PolicyName) + { + Write-ALError -Message "Policy $($PolicyName) not found! Known policies: $((Get-ALPolicy -PolicyName "*").Name -join ',')" -Terminate + } + + return (Get-ALPolicy -PolicyName $PolicyName).Name +} \ No newline at end of file diff --git a/AaronLocker/internal/functions/Test-AccessChk.ps1 b/AaronLocker/internal/functions/Test-AccessChk.ps1 new file mode 100644 index 0000000..0047139 --- /dev/null +++ b/AaronLocker/internal/functions/Test-AccessChk.ps1 @@ -0,0 +1,34 @@ +function Test-AccessChk +{ +<# + .SYNOPSIS + Checks, whether AccessChk.exe is present on the system. + + .DESCRIPTION + Checks, whether AccessChk.exe is present on the system. + + .EXAMPLE + PS C:\> Test-AchessChk + + Checks, whether AccessChk.exe is present on the system. +#> + [CmdletBinding()] + Param ( + + ) + + process + { + if ($script:config.PathAccessChk -and (Test-Path $script:config.PathAccessChk)) + { + return $true + } + if ($command = Get-Command AccessChk.exe -ErrorAction Ignore) + { + $script:config.PathAccessChk = $command.Source + Export-Configuration + return $true + } + return $false + } +} \ No newline at end of file diff --git a/AaronLocker/internal/functions/Update-PolicyFile.ps1 b/AaronLocker/internal/functions/Update-PolicyFile.ps1 new file mode 100644 index 0000000..458fab2 --- /dev/null +++ b/AaronLocker/internal/functions/Update-PolicyFile.ps1 @@ -0,0 +1,29 @@ +function Update-PolicyFile +{ +<# + .SYNOPSIS + Updates the file system state of a policy. + + .DESCRIPTION + Updates the file system state of a policy. + Used to ensure data is persisted across sessions. + + .PARAMETER PolicyName + The name of the policy to update + + .EXAMPLE + PS C:\> Update-PolicyFile -PolicyName OneDrive + + Updates the disk data of the OneDrive policy. +#> + [CmdletBinding()] + Param ( + [string] + $PolicyName + ) + + try { $PolicyName = Resolve-ALPolicy -PolicyName $PolicyName } + catch { Write-ALError -ErrorRecord $_ -Terminate } + + $script:_PolicyData[$PolicyName] | Export-Clixml -Path "$($script:_RulesFolder)\$($PolicyName).policy.clixml" +} \ No newline at end of file diff --git a/AaronLocker/internal/functions/Write-ALError.ps1 b/AaronLocker/internal/functions/Write-ALError.ps1 new file mode 100644 index 0000000..ecc7ead --- /dev/null +++ b/AaronLocker/internal/functions/Write-ALError.ps1 @@ -0,0 +1,86 @@ +function Write-ALError +{ +<# + .SYNOPSIS + Helper function to write an error. + + .DESCRIPTION + Helper function to write an error. + + .PARAMETER ErrorRecord + The error record to pass on. + + .PARAMETER Message + A custom message to insert. + + .PARAMETER Terminate + Make it a terminating exception + + .PARAMETER Continue + Call continue after writing an exception. + + .PARAMETER ContinueLabel + Call continue with a particular label. + + .EXAMPLE + PS C:\> Write-ALError -ErrorRecord $_ -Terminate + + Pass on the received exception and selfterminate. + + .EXAMPLE + PS C:\> Write-ALError -Message "Something broke" + + Write an error with the specified message. + + .EXAMPLE + PS C:\> Write-ALError -ErrorRecord $_ -Message "Something broke" + + Write an error, passing along the original record but overwriting the message +#> + [CmdletBinding()] + param ( + [System.Management.Automation.ErrorRecord] + $ErrorRecord, + + [string] + $Message, + + [switch] + $Terminate, + + [switch] + $Continue, + + [string] + $ContinueLabel + ) + + $cmdlet = Get-Variable -Name PSCmdlet -Scope 1 -ValueOnly + + if (-not $Message) + { + if ($Terminate) { $Cmdlet.ThrowTerminatingError($ErrorRecord) } + else { $cmdlet.WriteError($ErrorRecord) } + if ($ContinueLabel -and $Continue) { continue $ContinueLabel } + if ($Continue) { continue } + return + } + + if ($ErrorRecord) + { + $exception = New-Object System.Exception($Message, $ErrorRecord.Exception) + $newRecord = New-Object System.Management.Automation.ErrorRecord($exception, $ErrorRecord.FullyQualifiedErrorID, $ErrorRecord.CategoryInfo.Category, $ErrorRecord.TargetObject) + if ($Terminate) { $Cmdlet.ThrowTerminatingError($ErrorRecord) } + else { $cmdlet.WriteError($newRecord) } + if ($ContinueLabel -and $Continue) { continue $ContinueLabel } + if ($Continue) { continue } + return + } + + $exception = New-Object System.Exception($Message) + $newRecord = New-Object System.Management.Automation.ErrorRecord($exception, $Cmdlet.MyInvocation.InvocationName, "NotSpecified", $null) + if ($Terminate) { $Cmdlet.ThrowTerminatingError($ErrorRecord) } + else { $cmdlet.WriteError($newRecord) } + if ($ContinueLabel -and $Continue) { continue $ContinueLabel } + if ($Continue) { continue } +} \ No newline at end of file diff --git a/AaronLocker/internal/functions/readme.md b/AaronLocker/internal/functions/readme.md new file mode 100644 index 0000000..c7074e5 --- /dev/null +++ b/AaronLocker/internal/functions/readme.md @@ -0,0 +1,7 @@ +# Functions + +This is the folder where the internal functions go. + +Depending on the complexity of the module, it is recommended to subdivide them into subfolders. + +The module will pick up all .ps1 files recursively \ No newline at end of file diff --git a/AaronLocker/internal/scripts/postimport.ps1 b/AaronLocker/internal/scripts/postimport.ps1 new file mode 100644 index 0000000..9b7e362 --- /dev/null +++ b/AaronLocker/internal/scripts/postimport.ps1 @@ -0,0 +1,7 @@ +# Add all things you want to run after importing the main code + +# Load Variables +. Import-ModuleFile -Path "$ModuleRoot\internal\scripts\variables.ps1" + +# Register a scriptblock for use by Source File Rules +. Import-ModuleFile -Path "$ModuleRoot\internal\scripts\resolveFileRule.ps1" \ No newline at end of file diff --git a/AaronLocker/internal/scripts/preimport.ps1 b/AaronLocker/internal/scripts/preimport.ps1 new file mode 100644 index 0000000..5069cfb --- /dev/null +++ b/AaronLocker/internal/scripts/preimport.ps1 @@ -0,0 +1,4 @@ +# Add all things you want to run before importing the main code + +# Load Variables +. Import-ModuleFile -Path "$ModuleRoot\bin\types.ps1" \ No newline at end of file diff --git a/AaronLocker/internal/scripts/resolveFileRule.ps1 b/AaronLocker/internal/scripts/resolveFileRule.ps1 new file mode 100644 index 0000000..1e03d7f --- /dev/null +++ b/AaronLocker/internal/scripts/resolveFileRule.ps1 @@ -0,0 +1,23 @@ +<# +This policy is used by the SourcePathRule rule object's 'Resolve()' method call to resolve the source file to the intended rules. +Commands converting rule objects into rules should call that method on SourcePathRule objects, use that methods return values and discard the original object. +#> +[AaronLocker.SourcePathRule]::ResolutionScript = { + param ( + [AaronLocker.SourcePathRule] + $Rule + ) + $paramConvertToALPolicy = @{ + Path = $Rule.Path + Recurse = $Rule.Recurse + EnforceMinimumVersion = $Rule.EnforceMinimumVersion + RuleNamePrefix = $Rule.Label + } + + foreach ($ruleItem in (ConvertTo-ALPolicy @paramConvertToALPolicy)) + { + $ruleItem.Collection = $Rule.Collection + $ruleItem.Action = $Rule.Action + $ruleItem + } +} \ No newline at end of file diff --git a/AaronLocker/internal/scripts/variables.ps1 b/AaronLocker/internal/scripts/variables.ps1 new file mode 100644 index 0000000..009618b --- /dev/null +++ b/AaronLocker/internal/scripts/variables.ps1 @@ -0,0 +1,17 @@ +$script:_ConfigPath = "$($env:APPDATA)\WindowsPowerShell\AaronLocker\config.json" +$script:_RulesFolder = "$($env:APPDATA)\WindowsPowerShell\AaronLocker" +if (-not (Test-Path $script:_RulesFolder)) { $null = New-Item -Path $script:_RulesFolder -ItemType Directory -Force } +$script:config = Import-Configuration + +$script:_PolicyData = @{ } +foreach ($policyFile in (Get-ChildItem "$($script:_RulesFolder)\*.policy.clixml")) +{ + $policy = Import-Clixml -Path $policyFile.FullName + $script:_PolicyData[$policy.Name] = $policy +} +if (-not $script:_PolicyData[$script:config.ActivePolicy]) +{ + $script:_PolicyData[$script:config.ActivePolicy] = New-Object AaronLocker.Policy + $script:_PolicyData[$script:config.ActivePolicy].Name = $script:config.ActivePolicy + $script:_PolicyData[$script:config.ActivePolicy] | Export-Clixml -Path "$($script:_RulesFolder)\$($script:config.ActivePolicy).policy.clixml" +} \ No newline at end of file diff --git a/AaronLocker/legacy/functions/discovery/Search-ALDirectory.ps1 b/AaronLocker/legacy/functions/discovery/Search-ALDirectory.ps1 new file mode 100644 index 0000000..4b1ed3e --- /dev/null +++ b/AaronLocker/legacy/functions/discovery/Search-ALDirectory.ps1 @@ -0,0 +1,502 @@ +#TODO: Cleanup Path references once scan-caching issue is resolved satisfactorily. + + + +function Search-ALDirectory +{ +<# + .SYNOPSIS + Scan directories to identify files that might need additional AppLocker rules. + + .DESCRIPTION + Produces a list of files in various directories that might need additional AppLocker rules to allow them to execute. + Optionally, the script can list non-standard directories in the %SystemDrive% root directory. These directories might require additional scanning. + + The script searches specified directory hierarchies for MSIs and scripts (according to file extension), and EXE/DLL files regardless of extension. + That is, a file can be identified as a Portable Executable (PE) file (typically an EXE or DLL) even if it has a non-standard extension or no extension. + + Output columns include: + * IsSafeDir - indicates whether the file's parent directory is "safe" (not user-writable) or "unsafe" (user-writable); + * File type - EXE/DLL, MSI, or Script; + * File extension - the file's extension; + * File name - the file name without path information; + * File path - Full path to the file; + * Parent directory - The file's parent directory; + * Publisher name, Product name - signature and product name that can be used in publisher rules; + * CreationTime, LastAccessTime, LastWriteTime - the file's timestamps according to the file system. + + Directories that can be searched: + * WritableWindir - writable subdirectories of the %windir% directory, based on results of the last scan performed by Create-Policies.ps1; + * WritablePF - writable subdirectories of the %ProgramFiles% directories, based on results of the last scan performed by Create-Policies.ps1; + * SearchProgramData - the %ProgramData% directory hierarchy; + * SearchOneUserProfile - the current user's profile directory; + * SearchAllUserProfiles - the root directory of user profiles (C:\Users); + * DirsToSearch - one or more caller-specified, comma-separated directory paths. + + Note that results from this script do not necessarily require that rules be created: + this is just an indicator about files that *might* need rules, if the files need to be allowed. + + .PARAMETER WritableWindir + If this switch is specified, searches user-writable subdirectories under %windir% according to results of the last scan performed by Create-Policies.ps1. + + .PARAMETER WritablePF + If this switch is specified, searches user-writable subdirectories under the %ProgramFiles% directories according to results of the last scan performed by Create-Policies.ps1. + + .PARAMETER SearchProgramData + If this switch is specified, searches the %ProgramData% directory hierarchy, which can contain a mix of "safe" and "unsafe" directories. + + .PARAMETER SearchOneUserProfile + If this switch is specified, searches the user's profile directory. + + .PARAMETER SearchAllUserProfiles + If this switch is specified, searches from the root directory of all users' profiles (C:\Users) + + .PARAMETER DirsToSearch + Specifies one or more directories to search. + + .PARAMETER NoPEFiles + If this switch is specified, does not search for Portable Executable files (EXE/DLL files) + + .PARAMETER NoScripts + If this switch is specified, does not search for script files. + + .PARAMETER NoMSIs + If this switch is specified, does not search for MSI files. + + .PARAMETER DirectoryNamesOnly + If this switch is specified, reports the names and "safety" of directories that contain files of interest but no file information. + + .PARAMETER FindNonDefaultRootDirs + If this switch is specified, identifies non-standard directories in the %SystemDrive% root directory. + These directories often contain LOB applications. + This switch cannot be used with any other options. + + .EXAMPLE + PS C:\> Search-ALDirectory -SearchOneUserProfile -DirsToSearch H:\ + + Searches the user's profile directory and the H: drive. + + .NOTES + #TODO: Find a way to miss the .js false-positives, including but not only in browser caches. + #TODO: Skip .js in browser temp caches (IE on Win10: localappdata\Microsoft\Windows\INetCache) - possibly obviated by not looking at .js + #TODO: Maybe offer an option not to exclude .js; could be useful outside of user profiles? Maybe include .js for some directory types and not others. + #TODO: Distinguish between Exe and Dll files based on IMAGE_FILE_HEADER characteristics. +#> + [CmdletBinding()] + param ( + [parameter(ParameterSetName = "SearchDirectories")] + [switch] + $WritableWindir, + + [parameter(ParameterSetName = "SearchDirectories")] + [switch] + $WritablePF, + + [parameter(ParameterSetName = "SearchDirectories")] + [switch] + $SearchProgramData, + + [parameter(ParameterSetName = "SearchDirectories")] + [switch] + $SearchOneUserProfile, + + [parameter(ParameterSetName = "SearchDirectories")] + [switch] + $SearchAllUserProfiles, + + [parameter(ParameterSetName = "SearchDirectories")] + [String[]] + $DirsToSearch, + + [parameter(ParameterSetName = "SearchDirectories")] + [switch] + $NoPEFiles, + + [parameter(ParameterSetName = "SearchDirectories")] + [switch] + $NoScripts, + + [parameter(ParameterSetName = "SearchDirectories")] + [switch] + $NoMSIs, + + [parameter(ParameterSetName = "SearchDirectories")] + [switch] + $DirectoryNamesOnly, + + [parameter(ParameterSetName = "NonDefaultRootDirs")] + [switch] + $FindNonDefaultRootDirs + ) + + begin + { + Set-StrictMode -Version Latest + + if (-not $FindNonDefaultRootDirs) + { + if (-not (Test-AccessChk)) + { + throw @" +Scanning for writable subdirectories requires that Sysinternals AccessChk.exe be available. +Please download it and use Set-ALConfiguration -PathAccessChk "" to register its location. +"AccessChk.exe was not found. Exiting... +"@ + } + } + + #region Utility Functions + function Search-File + { + [CmdletBinding()] + param ( + [string] + $Directory, + + [string] + $Safety, + + [string[]] + $WritableDirs + ) + $doNoMore = $false + + Get-ChildItem -File $Directory -Force -ErrorAction SilentlyContinue -PipelineVariable file | ForEach-Object { + + # Work around Get-AppLockerFileInformation bug that vomits on zero-length input files + if ($_.Length -gt 0 -and -not $doNoMore) + { + $filetype = $null + if ((-not $NoScripts) -and ($file.Extension -in $scriptExtensions)) + { + $filetype = "Script" + } + elseif ((-not $NoMSIs) -and ($file.Extension -in $msiExtensions)) + { + $filetype = "MSI" + } + elseif ((-not $NoPEFiles) -and (Test-FileExecutable -File $file)) + { + $filetype = "EXE/DLL" + } + + # Output + if ($null -ne $filetype) + { + $fullname = $file.FullName + $fileext = $file.Extension + $filename = $file.Name + $parentDir = [System.IO.Path]::GetDirectoryName($fullname) + $pubName = $prodName = [String]::Empty + $alfi = Get-AppLockerFileInformation $file.FullName -ErrorAction SilentlyContinue -ErrorVariable alfiErr + # Diagnostics. Seeing sharing violations on some operations + if ($alfiErr.Count -gt 0) + { + Write-Host ($file.FullName + "`tLength = " + $file.Length.ToString()) -ForegroundColor Yellow -BackgroundColor Black + $alfiErr | ForEach-Object { Write-Host $_.Exception -ForegroundColor Red -BackgroundColor Black } + } + if ($null -ne $alfi) + { + $pub = $alfi.Publisher + if ($null -ne $pub) + { + $pubName = $pub.PublisherName + $prodName = $pub.ProductName + } + } + $safetyOut = $Safety + if ($Safety -eq $UnknownDir) + { + #$dbgInfo = $fullname + "`t" + $parentDir + if ($parentDir -in $WritableDirs) + { + #$dbgInfo = $UnsafeDir + "`t" + $dbgInfo + $safetyOut = $UnsafeDir + } + else + { + #$dbgInfo = ($SafeDir + "`t" + $dbgInfo) + $safetyOut = $SafeDir + } + #$dbgInfo + } + + if ($DirectoryNamesOnly) + { + [pscustomobject]@{ + PSTypeName = "AaronLocker.Detection.Directory" + IsSafeDir = $safetyOut + ParentDirectory = $parentDir + } + + # Found one file - don't need to continue inspection of files in this directory + $doNoMore = $true + } + else + { + [pscustomobject]@{ + PSTypeName = "AaronLocker.Detection.File" + IsSafeDir = $safetyOut + FileType = $filetype + FileExtension = $fileext + FileName = $filename + FilePath = $fullname + ParentDirectory = $parentDir + PublisherName = $pubName + ProductName = $prodName + CreationTime = $file.CreationTime + LastAccessTime = $file.LastAccessTime + LastWriteTime = $file.LastWriteTime + } + } + } + } + } + } + + function Search-Directory + { + [CmdletBinding()] + param ( + [string] + $Directory, + + [string] + $Safety, + + [string[]] + $WritableDirs + ) + Search-File -Directory $Directory -Safety $Safety -WritableDirs $WritableDirs + + Get-ChildItem -Directory $Directory -Force -ErrorAction SilentlyContinue | ForEach-Object { + $subdir = $_ + # Decide here whether to recurse into the subdirectory: + # * Skip junctions and symlinks (typically app-compat junctions). + # * Can add criteria here to skip browser caches, etc. + if (-not ($subdir.Attributes -band ([System.IO.FileAttributes]::ReparsePoint))) + { + Write-Verbose "... $($subdir.FullName)" + Search-Directory -Directory $subdir.FullName -Safety $Safety -WritableDirs $WritableDirs + } + else + { + Write-Verbose "SKIPPING $($subdir.FullName)" + } + } + } + + function Test-FileExecutable + { + <# + .SYNOPSIS + Inspect files for PE properties + + .DESCRIPTION + Inspect files for PE properties (on the cheap!) + If it's 64 bytes or more, and the first two are "MZ", we're calling it a PE file. + + .PARAMETER File + The file to inspect + + .EXAMPLE + PS C:\> Test-FileExecutable -File $file + + Checks whether $file is an executable + #> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [System.IO.FileInfo] + $File + ) + + if ($File.Length -lt 64) + { + return $false + } + + $mzHeader = Get-Content -LiteralPath $File.FullName -TotalCount 2 -Encoding Byte -ErrorAction SilentlyContinue + + # 0x4D = 'M', 0x5A = 'Z' + return $null -ne $mzHeader -and ($mzHeader[0] -eq 0x4D -and $mzHeader[1] -eq 0x5A) + } + #endregion Utility Functions + + # ".js", ### Too many false positives; these are almost always executed within programs that do not restrict .js. + $scriptExtensions = @( + ".bat", + ".cmd", + ".vbs", + ".wsf", + ".wsh", + ".ps1" + ) + $msiExtensions = @( + ".msi", + ".msp", + ".mst" + ) + } + + process + { + #region Find Non-Default root directories + ### ====================================================================== + ### The FindNonDefaultRootDirs is a standalone option that cannot be used with other switches. + ### It searches the SystemDrive root directory and enumerates non-default directory names. + if ($FindNonDefaultRootDirs) + { + $defaultRootDirs = @( + '$Recycle.Bin', + 'Config.Msi', + 'MSOTraceLite', + 'OneDriveTemp', + 'PerfLogs', + 'Program Files', + 'Program Files (x86)', + 'ProgramData', + 'Recovery', + 'System Volume Information', + 'Users', + 'Windows' + ) + + # Enumerate root-level directories whether hidden or not, but exclude junctions and symlinks. + # Output the ones that don't exist in a default Windows installation. + Get-ChildItem -Directory -Force "$($env:SystemDrive)\" | + Where-Object { -not ($_.Attributes -band ([System.IO.FileAttributes]::ReparsePoint)) -and ($_ -notin $defaultRootDirs) } | + Select-Object -ExpandProperty FullName + + return + } + #endregion Find Non-Default root directories + + #$rootDir = [System.IO.Path]::GetDirectoryName($MyInvocation.MyCommand.Path) + # Dot-source the config file. + #. $rootDir\Support\Config.ps1 + <# + Heathen Variables used: + $windirTxt + $PfTxt + $Pf86Txt + #> + + # Define some constants + Set-Variable UnsafeDir -option Constant -value "UnsafeDir" + Set-Variable SafeDir -option Constant -value "SafeDir" + Set-Variable UnknownDir -option Constant -value "UnknownDir" + + # Hashtable: key is path to inspect; value is indicator whether safe/unsafe + $dirsToInspect = @{ } + + # Writable directories under \Windows; known to be unsafe paths + if ($WritableWindir) + { + if (-not (Test-Path -Path $windirTxt)) + { + Write-Warning "$windirTxt does not exist yet. Run Create-Policies.ps1." + } + else + { + Get-Content $windirTxt | ForEach-Object { + $dirsToInspect.Add($_, $UnsafeDir) + } + } + } + + # Writable directories under ProgramFiles; known to be unsafe paths + if ($WritablePF) + { + if (-not (Test-Path -Path $PfTxt)) + { + Write-Warning "$PfTxt does not exist yet. Run Create-Policies.ps1." + } + elseif (-not (Test-Path -Path $Pf86Txt)) + { + Write-Warning "$Pf86Txt does not exist yet. Run Create-Policies.ps1." + } + else + { + Get-Content $PfTxt, $Pf86Txt | ForEach-Object { + $dirsToInspect.Add($_, $UnsafeDir) + } + } + } + + if ($SearchProgramData) + { + # Probably a mix of safe and unsafe paths + $dirsToInspect.Add($env:ProgramData, $UnknownDir) + } + + if ($SearchOneUserProfile) + { + #Assume all unsafe paths + #TODO: Skip browser-cache temp directories + $dirsToInspect.Add($env:USERPROFILE, $UnsafeDir) + } + + if ($SearchAllUserProfiles) + { + #Assume all unsafe paths + # No special folder or environment variable available. Get root directory from parent directory of user profile directory + $rootdir = [System.IO.Path]::GetDirectoryName($env:USERPROFILE) + #TODO: Skip browser-cache temp directories + # Skip app-compat juntions (most disallow FILE_LIST_DIRECTORY) + # Skip symlinks -- "All Users" is a symlinkd for \ProgramData but unlike most app-compat junctions it can be listed/traversed. + # This code prevents that. + Get-ChildItem -Force -Directory C:\Users | Where-Object { -not $_.Attributes -band ([System.IO.FileAttributes]::ReparsePoint) } | ForEach-Object { + $dirsToInspect.Add($_.FullName, $UnsafeDir) + } + } + + if ($DirsToSearch) + { + $DirsToSearch | ForEach-Object { $dirsToInspect.Add($_, $UnknownDir) } + } + + # Exclude known admins from analysis + $knownAdmins = $script:config.KnownAdmins + + # Capture into hash tables, separate file name, type, and parent path + $dirsToInspect.Keys | ForEach-Object { + + $dirToInspect = $_ + $safety = $dirsToInspect[$dirToInspect] + if ($safety -eq $UnknownDir) + { + Write-Host "about to inspect $dirToInspect for writable directories..." -ForegroundColor Cyan + $writableDirs = Search-WritableDirectory -RootDirectory $dirToInspect -KnownAdmins $knownAdmins + if ($null -eq $writableDirs) + { + $writableDirs = @() + } + } + else + { + $writableDirs = @() + } + + Write-Host "About to inspect $dirToInspect..." -ForegroundColor Cyan + Search-Directory -Directory $dirToInspect -Safety $safety -WritableDirs $writableDirs + } +<# Informational: + + Get-AppLockerFileInformation -Directory searches for these file extensions: + *.com + *.exe + *.dll + *.ocx + *.msi + *.msp + *.mst + *.bat + *.cmd + *.js + *.ps1 + *.vbs + *.appx +#> + } +} \ No newline at end of file diff --git a/AaronLocker/legacy/functions/export/Export-ALPolicyToExcel.ps1 b/AaronLocker/legacy/functions/export/Export-ALPolicyToExcel.ps1 new file mode 100644 index 0000000..923290c --- /dev/null +++ b/AaronLocker/legacy/functions/export/Export-ALPolicyToExcel.ps1 @@ -0,0 +1,132 @@ +function Export-ALPolicyToExcel +{ +<# + .SYNOPSIS + Turns AppLocker policy into a more human-readable Excel worksheet. + + .DESCRIPTION + The script gets AppLocker policy from one of four sources, imports it into a new Excel instance, and formats it. + + The four source options are: + * Current effective policy (default behavior -- use no parameters); + * Current local policy (use -Local switch); + * Exported AppLocker policy in an XML file (use -AppLockerXML parameter with file path); + * Output previously captured from ExportPolicy-ToCsv.ps1 (use -AppLockerCSV with file path); + + This script depends on ExportPolicy-ToCsv.ps1, which should be in the Support subdirectory. + It also depends on Microsoft Excel's being installed. + + The three command line options (-Local, -AppLockerXML, -AppLockerCSV) are mutually exclusive: only one can be used at a time. + + .PARAMETER Local + If this switch is specified, the script processes the computer's local AppLocker policy. + If no parameters are specified or this switch is set to -Local:$false, the script processes the computer's effective AppLocker policy. + + .PARAMETER AppLockerXML + If this parameter is specified, AppLocker policy is read from the specified exported XML policy file. + + .PARAMETER AppLockerCSV + If this parameter is specified, AppLocker policy is read from the specified CSV file previously created from ExportPolicy-ToCsv.ps1 output. + + .PARAMETER SaveWorkbook + If set, saves workbook to same directory as input file with same file name and default Excel file extension. + + .EXAMPLE + Export-ALPolicyToExcel + + Generates an Excel worksheet representing the computer's effective AppLocker policy. + + .EXAMPLE + Export-PolicyToCsv | Out-File .\AppLocker.csv; Export-ALPolicyToExcel -AppLockerCSV .\AppLocker.csv + + Generates an Excel worksheet representing AppLocker policy previously generated from ExportPolicy-ToCsv.ps1 output. + + .EXAMPLE + Get-AppLockerPolicy -Local -Xml | Out-File .\AppLocker.xml; Export-ALPolicyToExcel -AppLockerXML .\AppLocker.xml + + Generates an Excel worksheet representing AppLocker policy exported from a system into an XML file. + + .NOTES + #TODO: Add option to get AppLocker policy from AD GPO, if/when ExportPolicy-ToCsv.ps1 adds it. +#> + [CmdletBinding(DefaultParameterSetName = "LocalPolicy")] + param ( + [parameter(ParameterSetName = "LocalPolicy")] + [switch] + $Local, + + [parameter(ParameterSetName = "SavedXML")] + [String] + $AppLockerXML, + + [parameter(ParameterSetName = "SavedCSV")] + [String] + $AppLockerCSV, + + [parameter(ParameterSetName = "SavedXML")] + [parameter(ParameterSetName = "SavedCSV")] + [switch] + $SaveWorkbook + ) + + $OutputEncodingPrevious = $OutputEncoding + $OutputEncoding = [System.Text.ASCIIEncoding]::Unicode + + + $tabname = "AppLocker policy" + $filename = $tempfile = $xlFname = [String]::Empty + + $linebreakSeq = "^|^" + + if ($AppLockerCSV.Length -gt 0) + { + $filename = $AppLockerCSV + $tabname = [System.IO.Path]::GetFileName($AppLockerCSV) + if ($SaveWorkbook) + { + $xlFname = [System.IO.Path]::ChangeExtension($AppLockerCSV, ".xlsx") + } + } + else + { + $filename = $tempfile = [System.IO.Path]::GetTempFileName() + + if ($AppLockerXML.Length -gt 0) + { + Export-PolicyToCSV -AppLockerPolicyFile $AppLockerXML -linebreakSeq $linebreakSeq | Out-File $tempfile -Encoding unicode + $tabname = [System.IO.Path]::GetFileNameWithoutExtension($AppLockerXML) + if ($SaveWorkbook) + { + $xlFname = [System.IO.Path]::ChangeExtension($AppLockerXML, ".xlsx") + } + } + else + { + Export-PolicyToCSV -Local:$Local -linebreakSeq $linebreakSeq | Out-File $tempfile -Encoding unicode + if ($Local) + { + $tabname = "AppLocker policy - Local" + } + else + { + $tabname = "AppLocker policy - Effective" + } + } + } + + if ($xlFname.Length -gt 0) + { + # Ensure absolute path + if (-not ([System.IO.Path]::IsPathRooted($xlFname))) + { + $xlFname = [System.IO.Path]::Combine((Get-Location).Path, $xlFname) + } + } + + CreateExcelFromCsvFile $filename $tabname $linebreakSeq $xlFname + + # Delete the temp file + if ($tempfile.Length -gt 0) { Remove-Item $tempfile } + + $OutputEncoding = $OutputEncodingPrevious +} \ No newline at end of file diff --git a/AaronLocker/legacy/functions/policy/ConvertTo-ALPolicy.ps1 b/AaronLocker/legacy/functions/policy/ConvertTo-ALPolicy.ps1 new file mode 100644 index 0000000..7c33d77 --- /dev/null +++ b/AaronLocker/legacy/functions/policy/ConvertTo-ALPolicy.ps1 @@ -0,0 +1,275 @@ +function ConvertTo-ALPolicy +{ +<# + .SYNOPSIS + Builds tightly-scoped but forward-compatible AppLocker rules for files in user-writable directories. + + .DESCRIPTION + This script takes a list of one or more file system objects (files and/or directories) and generates rules to allow execution of the corresponding files. + + Rules generated with this script can be incorporated into comprehensive rule sets using New-ALPolicyScan. + + Publisher rules are generated where possible: + * Publisher rules restrict to a specific binary name, product name, and publisher, and (optionally) the identified version or above. + * Redundant rules are removed; if multiple versions of a specific file are found, the rule allows execution of the lowest-identified version or above. + Hash rules are generated when publisher rules cannot be created. + The script creates rule names and descriptions designed for readability in the Security Policy editor. The RuleNamePrefix option enables you to give each rule in the set a common prefix (e.g., "OneDrive") to make the source of the rule more apparent and so that related rules can be grouped alphabetically by name. + The rules' EnforcementMode is left NotConfigured. (New-ALPolicyScan takes care of setting EnforcementMode in the larger set.) + (Note that the New-AppLockerPolicy's -Optimize switch "overoptimizes," allowing any file name within a given publisher and product name. Not using that.) + + File system objects can be identified on the command line with -Path, or listed in a file (one object per line) referenced by -FileOfFileSystemObjects. + + This script determines whether each object is a file or a directory. For directories, this script enumerates and identifies EXE, DLL, and Script files based on file extension. Subdirectories are scanned if the -Recurse switch is specified on the command line. + + The intent of this script is to create fragments of policies that can be incorporated into a "master" policy in a modular way. + For example, create a file representing the rules needed to allow OneDrive to run, and separate files for LOB apps. + If/when the OneDrive rules need to be updated, they can be updated in isolation and those results incorporated into a new master set. + + .PARAMETER Path + An array of file paths and/or directory paths to scan. The array can be a comma-separated list of file system paths. + Either Path or InputFile must be specified. + + .PARAMETER InputFile + The name of a file containing a list of file paths and/or directory paths to scan; one path to a line. + Either Path or InputFile must be specified. + + .PARAMETER Recurse + If this switch is specified, scanning of directories includes subdirectories; otherwise, only files in the named directory are scanned. + + .PARAMETER EnforceMinimumVersion + If this switch is specified, generated publisher rules enforce minimum file version based on versions of the scanned files; otherwise rules do not enforce file versions + + .PARAMETER RuleNamePrefix + Optional: If specified, all rule names begin with the specified RuleNamePrefix. + + .EXAMPLE + ConvertTo-ALPolicy -Path $env:LOCALAPPDATA\Microsoft\OneDrive -Recurse -RuleNamePrefix OneDrive + + Scans the OneDrive directory and subdirectories in the current user's profile. + All generated rule names will begin with "OneDrive". + The generated rules are written to ..\WorkingFiles\OneDriveRules.xml. +#> + [CmdletBinding()] + param ( + [parameter(Mandatory = $true, ParameterSetName = "OnCommandLine", ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] + [Alias('FullName')] + [String[]] + $Path, + + [parameter(Mandatory = $true, ParameterSetName = "SpecifiedInFile")] + [String] + $InputFile, + + [switch] + $Recurse, + + [switch] + $EnforceMinimumVersion, + + [String] + $RuleNamePrefix + ) + + begin + { + #region Utility Function + function Add-Policy + { + [CmdletBinding()] + param ( + [Parameter(ValueFromPipeline = $true)] + $FileInformation, + + [System.Collections.Hashtable] + $Policies, + + [string] + $Prefix + ) + + process + { + foreach ($fileInformationObject in $FileInformation) + { + # Favor publisher rule; hash rule otherwise + $policy = New-AppLockerPolicy -FileInformation $fileInformationObject -RuleType Publisher, Hash + + foreach ($ruleCollection in $policy.RuleCollections) + { + $rtype = $ruleCollection.RuleCollectionType + foreach ($rule in $ruleCollection) + { + #region Publisher rule - file is signed and has required PE version information + if ($rule -is [Microsoft.Security.ApplicationId.PolicyManagement.PolicyModel.FilePublisherRule]) + { + $pubInfo = $rule.PublisherConditions + # Key on file name, product name, and publisher name; don't incorporate version number into the key + $key = $pubInfo.BinaryName + "|" + $pubInfo.ProductName + "|" + $pubInfo.PublisherName + # Build new rule name and description + $rule.Description = "Product: " + $pubInfo.ProductName + "`r`n" + "Publisher: " + $pubInfo.PublisherName + "`r`n" + "Original path: " + $fileInformationObject.Path.Path + $rule.Name = $Prefix + $pubInfo.BinaryName + $pubInfo.BinaryVersionRange.HighSection = $null + if ($EnforceMinimumVersion) + { + # Allow scanned version and above + $rule.Name += ", v" + $pubInfo.BinaryVersionRange.LowSection.ToString() + " and above" + } + else + { + $pubInfo.BinaryVersionRange.LowSection = $null + } + if (-not $Policies.ContainsKey($key)) + { + # Add this publisher rule to the collection + #DBG "PUBLISHER RULE (" + $rtype + "): ADDING " + $key + $Policies.Add($key, $policy) + } + elseif ($EnforceMinimumVersion) + { + # File already seen; see whether the newly-scanned file has a lower file version that needs to be allowed + $rulesPrev = $Policies[$key] + foreach ($rcPrev in $rulesPrev.RuleCollections) + { + foreach ($rulePrev in $rcPrev) + { + # Get the previously-scanned file version; compare to the new one + $verPrev = $rulePrev.PublisherConditions.BinaryVersionRange.LowSection + $verCurr = $pubInfo.BinaryVersionRange.LowSection + if ($verCurr.CompareTo($verPrev) -lt 0) + { + # The new one is a lower file version; replace the rule we had with the new one. + #DBG $pubInfo.BinaryName + " REPLACE WITH EARLIER VERSION, FROM " + $verPrev.ToString() + " TO " + $verCurr.ToString() + $Policies[$key] = $policy + } + else + { + #DBG $pubInfo.BinaryName + " KEEPING VERSION " + $verCurr.ToString() + " IN FAVOR OF " + $verPrev.ToString() + } + } + } + } + } + #endregion Publisher rule - file is signed and has required PE version information + + #region Hash Rule + elseif ($rule -is [Microsoft.Security.ApplicationId.PolicyManagement.PolicyModel.FileHashRule]) + { + # Hash rule - file is missing signature and/or PE version information + # Record the full path into the policy + $hashInfo = $rule.HashConditions.Hashes + # Key on file name and hash + $key = $hashInfo.SourceFileName + "|" + $hashInfo.HashDataString + if (-not $Policies.ContainsKey($key)) + { + $Policies[$key] = New-Object AaronLocker.HashRule -Property @{ + Label = "$($Prefix)$($rule.Name) - HASH RULE" + Name = "$($Prefix)$($rule.Name) - HASH RULE" + Description = "Identified in: $($fileInformationObject.Path.Path)" + } + # Default rule name is just the file name; append "HASH RULE" + # Set the rule description to the full path. + # If the same file appears in multiple locations, one path will be picked; it doesn't matter which + $rule.Name = $Prefix + $rule.Name + " - HASH RULE" + $rule.Description = "Identified in: " + $fileInformationObject.Path.Path + # Add this hash rule to the collection + #DBG "HASH RULE (" + $rtype + "): ADDING " + $key + $Policies.Add($key, $policy) + } + else + { + # Saw an identical file already + # "HASH RULE (" + $rtype + "): ALREADY HAVE " + $key + } + } + #endregion Hash Rule + } + } + } + } + } + #endregion Utility Function + + #region Handle Inputfile parameter + if ($InputFile) + { + # Test path of the file name, verify that it's a file + if ((Test-Path $InputFile) -and ((Get-Item $InputFile) -is [System.IO.FileInfo])) + { + $Path = Get-Content $InputFile + } + else + { + throw "Invalid file path: $InputFile" + } + } + #endregion Handle Inputfile parameter + + # If RuleNamePrefix specified, append ": " to it before incorporating into rule names + if ($RuleNamePrefix.Length -gt 0) + { + $RuleNamePrefix += ": " + } + + # Hash table of rules with redundant entries removed + $policies = @{ } + $countItems = 0 + + $paramGetAppLockerFileInformation = @{ + FileType = 'Exe', 'Dll', 'Script' + Recurse = $Recurse.ToBool() + } + } + process + { + #region Process all specified paths and build the policies dictionary + foreach ($fileSystempath in $Path) + { + $countItems = $countItems + 1 + + # E.g., in case of blank lines in input file + $fileSystempath = $fileSystempath.Trim() + if ($fileSystempath.Length -gt 0) + { + if (Test-Path $fileSystempath) + { + # Determine whether directory or file + $fspInfo = Get-Item $fileSystempath + if ($fspInfo -is [System.IO.DirectoryInfo]) + { + <# + Item is a directory; inspect directory (possibly with recursion) + Note: dependent on file extensions + Get-AppLockerFileInformation -Directory inspects files with these extensions: + .com, .exe, .dll, .ocx, .msi, .msp, .mst, .bat, .cmd, .js, .ps1, .vbs, .appx + But this script drops .msi, .msp, .mst, and .appx + #> + Get-AppLockerFileInformation -Directory $fileSystempath @paramGetAppLockerFileInformation | Add-Policy -Policies $policies -Prefix $RuleNamePrefix + } + elseif ($fspInfo -is [System.IO.FileInfo]) + { + # Item is a file; get applocker information for the file + Get-AppLockerFileInformation -Path $fileSystempath | Add-Policy -Policies $policies -Prefix $RuleNamePrefix + } + else + { + # Specified object exists and is not a file or a directory. + # Display a warning but continue. + Write-Warning -Message ("Unexpected object type for {0} : {1}" -f $fileSystempath, $fspInfo.GetType().FullName) + } + } + else + { + # Specified object does not exist. + # Display a warning but continue. + Write-Warning -Message "FILE SYSTEM OBJECT DOES NOT EXIST: $fileSystempath" + } + } + } + #endregion Process all specified paths and build the policies dictionary + } + end + { + if ($policies.Count -eq 0) { Write-Warning "No policies generated, no file found after scanning $($countItems) paths" } + $policies.Values + } +} \ No newline at end of file diff --git a/AaronLocker/legacy/functions/policy/Join-ALPolicy.ps1 b/AaronLocker/legacy/functions/policy/Join-ALPolicy.ps1 new file mode 100644 index 0000000..0ae1f67 --- /dev/null +++ b/AaronLocker/legacy/functions/policy/Join-ALPolicy.ps1 @@ -0,0 +1,41 @@ +function Join-ALPolicy +{ +<# + .SYNOPSIS + Merges multiple policies into a single combined policy. + + .DESCRIPTION + Merges multiple policies into a single combined policy. + + .PARAMETER Policy + The policy objects to combine. + Must be objects as returned by New-AppLockerPolicy. + + .EXAMPLE + PS C:\> $policies | Join-ALPolicy + + Combines all policies stored in $policies into a single one. +#> + [CmdletBinding()] + param ( + [Parameter(ValueFromPipeline = $true, Mandatory = $true)] + $Policy + ) + + begin + { + $policies = $null + } + process + { + foreach ($policyItem in $Policy) + { + if ($null -eq $policies) { $policies = $policyItem } + else { $policies.Merge($policyItem) } + } + } + end + { + $policies + } +} \ No newline at end of file diff --git a/AaronLocker/legacy/functions/policy/New-ALPolicyScan.ps1 b/AaronLocker/legacy/functions/policy/New-ALPolicyScan.ps1 new file mode 100644 index 0000000..d247b36 --- /dev/null +++ b/AaronLocker/legacy/functions/policy/New-ALPolicyScan.ps1 @@ -0,0 +1,794 @@ +function New-ALPolicyScan +{ +<# + .SYNOPSIS + Builds comprehensive and robust AppLocker "audit" and "enforce" rules to mitigate against users running unauthorized software, customizable through simple text files. Writes results to the Outputs subdirectory. + + TODO: Find and remove redundant rules. Report stripped rules to a separate log file. + + .DESCRIPTION + Create-Policies.ps1 generates comprehensive "audit" and "enforce" AppLocker rules to restrict non-admin code execution to "authorized" software, + in a way to minimize the need to update the rules. + Broadly speaking, "authorized" means that an administrator put it on the computer, OR created a rule specifically for that item. + * Supported operating systems include Windows 7 and newer, and Windows Server 2008 R2 and newer. + * Rules cover EXE, DLL, Script, and MSI; on Windows 8.1 and newer, rules also cover Packaged apps. + * Allows execution from the Windows and ProgramFiles directories, EXCEPT: + * Identifies user-writable subdirectories and disallows execution from those directories; + * Disallows execution of programs that run user-supplied code (e.g., mshta.exe); + * Disallows execution of programs that non-admins rarely need but that malware/ransomware authors are known to use (e.g., cipher.exe); + * Allows execution from identified "safe" paths (non-admins cannot write to them); + * Allows execution of specifically authorized code in user-writable ("unsafe") directories. + + Rule implementation: + AppLocker rule types include path rules, publisher rules, and hash rules. + Rules allowing execution from "safe" locations are implemented using path rules. + User-writable subdirectories of the Windows and ProgramFiles directories are identified using Sysinternals AccessChk.exe. Exceptions for those subdirectories are implemented within path rules. + Exceptions for "dangerous" programs (e.g., mshta.exe, cipher.exe) are generally implemented with publisher rules. + Rules allowing execution of EXE, DLL, and script files from user-writable directories are implemented with publisher rules when possible, and hash rules otherwise. The publisher rules can optionally specify the current version "and above;" publisher rules always allow files to be updated without needing to update the corresponding rules. + Publisher rules can also be created allowing execution of anything signed by a particular publisher, or a specific product by a particular publisher. + + Scanning for user-writable subdirectories of the Windows and ProgramFiles directories can be time-consuming. The script writes results to text files in an intermediate subdirectory. The script runs the scan if those files are not found OR if the -Rescan switch is specified. + It is STRONGLY recommended that the scanning be performed with administrative rights. + Once scans have been performed, scanned output can be copied to another machine and rules can be maintained without needing to rescan. + + Dependencies: + PowerShell v5.1 or higher (Windows Management Framework 5.1 or higher) + Current (or recent) version of Sysinternals AccessChk.exe, either in the Path or in the same directory as this script. + Scripts and support files included in this solution (some are in specific subdirectories). + + See external documentation for more information. + + .PARAMETER Rescan + If this switch is set, this script scans the Windows and ProgramFiles directories for user-writable subdirectories, and captures data about EXE files to blacklist. + If the results from a previous scan are found in the expected location and this switch is not specified, the script does not perform those scans. If those results are not found, the script performs the scan even if this switch is not set. + It is STRONGLY recommended that the scanning be performed with administrative rights. + + .PARAMETER ForUser + If scanning a system with an administrative account with a need to inspect another user's profile for "unsafe paths," specify that username with this optional parameter. E.g., if logged on and scanning with administrative account "abby-adm" but need to inspect $env:USERPROFILE belonging to "toby", use -ForUser toby. + + .PARAMETER Excel + If specified, also creates Excel spreadsheets representing the generated rules. + + .EXAMPLE + PS C:\> New-ALPolicyScan + + .LINK + Sysinternals AccessChk available here: + https://technet.microsoft.com/sysinternals/accesschk + https://download.sysinternals.com/files/AccessChk.zip +#> + [CmdletBinding()] + param ( + [switch] + $Rescan, + + [String] + $ForUser, + + [switch] + $Excel + ) + + $outputPath = $script:config.OutputPath + + #region Utility Functions + function Rename-Paths + { + <# + .SYNOPSIS + Helper function used to replace current username with another in paths. + + .DESCRIPTION + Helper function used to replace current username with another in paths. + + .PARAMETER Paths + The paths to replace the username in. + + .PARAMETER ForUsername + The username to insert into the path. + #> + [CmdletBinding()] + param ( + [Parameter(ValueFromPipeline = $true)] + [string[]] + $Paths, + + [string] + $ForUsername + ) + + process + { + foreach ($path in $Paths) + { + if ($path -match '\\\*$') { $path = [System.IO.Path]::GetFullPath($path.TrimEnd("*")) + "*" } + else { $path = [System.IO.Path]::GetFullPath($path) } + $path -replace "\\$env:USERNAME\\", "\$ForUsername\" -replace "\\$($env:USERNAME)$", "\$ForUsername" + } + } + } + #endregion Utility Functions + + #region Validation + # Only supported PowerShell version at this time: 5.1 + # PS Core v6.x doesn't include AppLocker cmdlets; string .Split() has new overloads that need to be dealt with. + # (At some point, may also need to check $PSVersionTable.PSEdition) + $psv = $PSVersionTable.PSVersion + if ($psv.Major -ne 5 -or $psv.Minor -ne 1) + { + $errMsg = "This script requires PowerShell v5.1.`nCurrent version = " + $PSVersionTable.PSVersion.ToString() + Write-Error $errMsg + return + } + + # Make sure this script is running in FullLanguage mode + if ($ExecutionContext.SessionState.LanguageMode -ne [System.Management.Automation.PSLanguageMode]::FullLanguage) + { + $errMsg = "This script must run in FullLanguage mode, but is running in " + $ExecutionContext.SessionState.LanguageMode.ToString() + Write-Error $errMsg + return + } + #endregion Validation + + + #$rootDir = [System.IO.Path]::GetDirectoryName($MyInvocation.MyCommand.Path) + + # Get configuration settings and global functions from .\Support\Config.ps1) + # Dot-source the config file. + #. $rootDir\Support\Config.ps1 + + # Create subdirectories if they don't exist (some have to exist because files are expected to be there). + if (-not (Test-Path -Path $customizationInputsDir)) { mkdir $customizationInputsDir | Out-Null } + if (-not (Test-Path -Path $mergeRulesDynamicDir)) { mkdir $mergeRulesDynamicDir | Out-Null } + if (-not (Test-Path -Path $mergeRulesStaticDir)) { mkdir $mergeRulesStaticDir | Out-Null } + if (-not (Test-Path -Path $outputsDir)) { mkdir $outputsDir | Out-Null } + if (-not (Test-Path -Path $supportDir)) { mkdir $supportDir | Out-Null } + if (-not (Test-Path -Path $scanResultsDir)) { mkdir $scanResultsDir | Out-Null } + + # Look for results from previous scan for user-writable directories under the Windows and ProgramFiles directories. + # If any of the files containing the filtered results are missing, force a rescan. + if (-not ((Test-Path -Path $windirTxt) -and (Test-Path -Path $PfTxt) -and (Test-Path -Path $Pf86Txt))) + { + $Rescan = $true + } + + #region Process Windir and ProgramFiles directories. + # If $Rescan enabled, enumerate user-writable directories under %windir% and the ProgramFiles directories + # (scans the '(x86)' one only if present; doesn't raise an error if not present). + # This must be done at least once. Note that it can be time-consuming. Admin rights are recommended. + # Scanning requires that Sysinternals AccessChk.exe be in the Path or in the script directory. If it isn't, + # this script writes an error message and quits. + # Outputs the list of all writable subdirectories to "*_Full.txt"; the rules are built using those results with redundant lines removed. + # The filtered lists can be hand-edited if absolutely necessary. + if ($Rescan) + { + # Scanning requires that AccessChk.exe be available. + # If accesschk.exe is in the rootdir, temporarily add the rootdir to the path. + # (Previous implementation invoked Get-Command to see whether accesschk.exe was in the path, and only if that failed looked for + # accesschk.exe in the rootdir. However, there was no good way to keep Get-Command from displaying a "Suggestion" message in that + # scenario.) + # Variable for restoring original Path, if necessary. + $origPath = "" + # Check for accesschk.exe in the rootdir. + if (Test-Path -Path $rootDir\AccessChk.exe) + { + # Found it in this script's directory. Temporarily prepend it to the path. + $origPath = $env:Path + $env:Path = "$rootDir;" + $origPath + } + # Otherwise, if AccessChk.exe not available in the path, write an error message and quit. + elseif ($null -eq (Get-Command AccessChk.exe -ErrorAction SilentlyContinue)) + { + $errMsg = "Scanning for writable subdirectories requires that Sysinternals AccessChk.exe be in the Path or in the same directory with this script.`n" + + "AccessChk.exe was not found. Exiting..." + Write-Error $errMsg + return + } + + # Enumerate user-writable subdirectories in protected directories. Capture grantees so they can be inspected afterwards. + Write-Host "Enumerating writable directories in $env:windir" -ForegroundColor Cyan + $knownAdmins = @() + $knownAdmins += & $ps1_KnownAdmins + Search-WritableDirectory -RootDirectory $env:windir -ShowGrantees -OutputXML -KnownAdmins $knownAdmins | Out-File -Encoding ASCII $windirFullXml + Write-Host "Enumerating writable directories in $env:ProgramFiles" -ForegroundColor Cyan + Search-WritableDirectory -RootDirectory $env:ProgramFiles -ShowGrantees -OutputXML -KnownAdmins $knownAdmins | Out-File -Encoding ASCII $PfFullXml + # The following applies only to 64-bit Windows; skip it on 32-bit and create an empty file + if ($null -ne ${env:ProgramFiles(x86)}) + { + Write-Host "Enumerating writable directories in ${env:ProgramFiles(x86)}" -ForegroundColor Cyan + Search-WritableDirectory -RootDirectory ${env:ProgramFiles(x86)} -ShowGrantees -OutputXML -KnownAdmins $knownAdmins | Out-File -Encoding ASCII $Pf86FullXml + } + else + { + # Create an empty file so the rest of the script doesn't have to take 32/64 into account. + New-Item $Pf86FullXml -ItemType File | Out-Null + } + # Restore original Path if it was altered for AccessChk.exe + if ($origPath.Length -gt 0) + { + $env:Path = $origPath + } + + # Function to remove redundancies from lists of user-writable directories enumerated in the supplied XML. + # Assumes that input is an XML listing user-writable directories. This script sorts the list of directory names alphabetically, + # and then removes any entries for which a parent directory has already been identified. + function RemoveRedundantLines([String]$fnameFullXml) + { + $x = [xml](Get-Content $fnameFullXml) + if ($null -ne $x) + { + $lastItem = "" + # Case-insensitive alphabetic sort of directory names + $x.root.dir.name | Sort-Object | ForEach-Object { + # First item in sorted list will be output. + # Anything that was output becomes $lastItem, lower-cased and ending with backslash. + # Anything that follows that matches $lastItem's full length (with backslash) must be a subdirectory - + # do not output that. + # When something doesn't match, it must be something other than a subdirectory of previous $lastItem. + # Write it out and make it $lastItem, lower-cased and ending with backslash. + $thisItem = $_ + if ($lastItem.Length -eq 0 -or !$thisItem.ToLower().StartsWith($lastItem)) + { + Write-Output $thisItem + $lastItem = $thisItem.ToLower() + if (!$lastItem.EndsWith("\")) { $lastItem += "\" } + } + } + } + } + + Write-Host "Removing redundancies in scan results" -ForegroundColor Cyan + RemoveRedundantLines $windirFullXml | Out-File -Encoding ASCII $windirTxt + RemoveRedundantLines $PfFullXml | Out-File -Encoding ASCII $PfTxt + RemoveRedundantLines $Pf86FullXml | Out-File -Encoding ASCII $Pf86Txt + } + #endregion Process Windir and ProgramFiles directories. + + #region Capture data for Exe files to blacklist if needed + if ($Rescan -or -not (Test-Path -Path $ExeBlacklistData)) + { + Write-Host "Processing EXE files to blacklist..." -ForegroundColor Cyan + # Get the EXE files to blacklist from the script that produces that list. + $exeFilesToBlacklist = (& $ps1_GetExeFilesToBlacklist) + # Create a hash collection for publisher information. Key on publisher name, product name, and binary name. + # Add to collection if equivalent is not already in the collection. + $pubCollection = @{ } + $exeFilesToBlacklist | ForEach-Object { + $pub = (Get-AppLockerFileInformation "$_").Publisher + if ($null -ne $pub) + { + $pubKey = ($pub.PublisherName + "|" + $pub.ProductName + "|" + $pub.BinaryName).ToLower() + if (!$pubCollection.ContainsKey($pubKey)) { $pubCollection.Add($pubKey, $pub) } + } + else + { + Write-Warning "UNABLE TO BUILD BLACKLIST RULE FOR $_" + } + } + + $pubCollection.Values | + Select-Object PublisherName, ProductName, BinaryName | + ConvertTo-Csv -NoTypeInformation | + Out-File $ExeBlacklistData -Encoding unicode + } + #endregion Capture data for Exe files to blacklist if needed + + #region Validate that scan-result files were created + if (-not ((Test-Path -Path $windirTxt) -and (Test-Path -Path $PfTxt) -and (Test-Path -Path $Pf86Txt))) + { + $errMsg = @" +One or more of the following files is missing: + $windirTxt + $PfTxt + $Pf86Txt +"@ + Write-Error $errMsg + return + } + + if (-not (Test-Path -Path $ExeBlacklistData)) + { + $errMsg = @" +The following file is missing: + $ExeBlacklistData +"@ + Write-Error $errMsg + return + } + #endregion Validate that scan-result files were created + + #region Process Windir and ProgramFiles directories. + # Read the lists of user-writable directories with redundancies removed. + $Wr_raw_windir = Get-Content $windirTxt + $Wr_raw_PF = Get-Content $PfTxt + $Wr_raw_PF86 = Get-Content $Pf86Txt + + # -------------------------------------------------------------------------------- + # Process names of directories, replacing hardcoded C:\, \Windows, etc., with AppLocker variables. + # Note that System32 and SysWOW64 map to the same variable names, as do the two ProgramFiles directories. + # Add trailing backslashes to the names (e.g., C:\Windows\System32\ ), so that if there happens to be + # a "C:\Windows\System32Extra" it won't match the System32 variable. + # Note that because of the trailing backslashes, if the top directories themselves are user-writable, + # they won't turn up in the list. That by itself would be a major problem, though. + $sSystem32 = "$env:windir\System32\".ToLower() + $sSysWow64 = "$env:windir\SysWOW64\".ToLower() + $sWindir = "$env:windir\".ToLower() + $sPF86 = "${env:ProgramFiles(x86)}\".ToLower() + $sPF = "$env:ProgramFiles\".ToLower() + + # Build arrays of processed directory names with duplicates removed. (E.g., System32\Com\dmp and + # SysWOW64\Com\dmp can both be covered with a single entry.) + $Wr_windir = @() + $Wr_PF = @() + + # For the Windows list, replace matching System32, SysWOW64, and Windows paths with corresponding + # AppLocker variables, then add to collection if not already present. + $Wr_raw_windir | ForEach-Object { + $dir = $_.ToLower() + if ($dir.StartsWith($sSystem32)) { $dir = "%SYSTEM32%\" + $dir.Substring($sSystem32.Length) } + elseif ($dir.StartsWith($sSysWow64)) { $dir = "%SYSTEM32%\" + $dir.Substring($sSysWow64.Length) } + elseif ($dir.StartsWith($sWindir)) { $dir = "%WINDIR%\" + $dir.Substring($sWindir.Length) } + # Don't add the rule twice if it appears in both System32 and SysWOW64, since both map to %SYSTEM32%. + if (!$Wr_windir.Contains($dir)) + { + $Wr_windir += $dir + } + } + + # For the two ProgramFiles lists, replace top directory with AppLocker variable, then add to collection + # if not already present. + $Wr_raw_PF86 | ForEach-Object { + $dir = $_.ToLower() + if ($dir.StartsWith($sPF86)) { $dir = "%PROGRAMFILES%\" + $dir.Substring($sPF86.Length) } + $Wr_PF += $dir + } + + $Wr_raw_PF | ForEach-Object { + $dir = $_.ToLower() + if ($dir.StartsWith($sPF)) { $dir = "%PROGRAMFILES%\" + $dir.Substring($sPF.Length) } + # Possibly already added same directory from PF86; don't add again + if (!$Wr_PF.Contains($dir)) + { + $Wr_PF += $dir + } + } + #endregion Process Windir and ProgramFiles directories. + + #################################################################################################### + # Load base AppLocker rules document + #################################################################################################### + + # -------------------------------------------------------------------------------- + # Build AppLocker rules starting with base document + $xDocument = [xml](Get-Content $defRulesXml) + + #region Incorporate data for EXE files to blacklist under Windir + # Incorporate the EXE blacklist into the document where the one PLACEHOLDER_WINDIR_EXEBLACKLIST + # placeholder is. + $xPlaceholder = $xDocument.SelectNodes("//PLACEHOLDER_WINDIR_EXEBLACKLIST")[0] + $xExcepts = $xPlaceholder.ParentNode + + $csvExeBlacklistData = (Get-Content $ExeBlacklistData | ConvertFrom-Csv) + $csvExeBlacklistData | ForEach-Object { + # Create a FilePublisherCondition element with the publisher attributes + $elem = $xDocument.CreateElement("FilePublisherCondition") + $elem.SetAttribute("PublisherName", $_.PublisherName) + $elem.SetAttribute("ProductName", $_.ProductName) + $elem.SetAttribute("BinaryName", $_.BinaryName) + # Set version number range to "any" + $elemVerRange = $xDocument.CreateElement("BinaryVersionRange") + $elemVerRange.SetAttribute("LowSection", "*") + $elemVerRange.SetAttribute("HighSection", "*") + # Add the version range to the publisher condition + $elem.AppendChild($elemVerRange) | Out-Null + # Add the publisher condition where the placeholder is + $xExcepts.AppendChild($elem) | Out-Null + } + # Remove the placeholder element + $xExcepts.RemoveChild($xPlaceholder) | Out-Null + + Write-Host "Processing additional safe paths to whitelist..." -ForegroundColor Cyan + # Get additional whitelisted paths from the script that produces that list and incorporate them into the document + $PathsToAllow = (& $ps1_GetSafePathsToAllow) + # Add "allow" for Everyone for Exe, Dll, and Script rules + $xRuleCollections = $xDocument.SelectNodes("//RuleCollection[@Type='Exe' or @Type='Script' or @Type='Dll']") + foreach ($xRuleCollection in $xRuleCollections) + { + $PathsToAllow | ForEach-Object { + # If path is an existing directory and doesn't have trailing "\*" appended, fix it so that it does. + # If path is a file, don't append \*. If the path ends with \*, no need for further validation. + # If it doesn't end with \* but Get-Item can't identify it as a file or a directory, write a warning and accept it as is. + $pathToAllow = $_ + if (-not $pathToAllow.EndsWith("\*")) + { + $pathItem = Get-Item $pathToAllow -ErrorAction SilentlyContinue + if ($pathItem -eq $null) + { + Write-Warning "Cannot verify path $pathItem; adding to rule set as is." + } + elseif ($pathItem -is [System.IO.DirectoryInfo]) + { + Write-Warning "Appending `"\*`" to rule for $pathToAllow" + $pathToAllow = [System.IO.Path]::Combine($pathToAllow, "*") + } + } + $elemRule = $xDocument.CreateElement("FilePathRule") + $elemRule.SetAttribute("Action", "Allow") + $elemRule.SetAttribute("UserOrGroupSid", "S-1-1-0") + $elemRule.SetAttribute("Id", [GUID]::NewGuid().Guid) + $elemRule.SetAttribute("Name", "Additional allowed path: " + $pathToAllow) + $elemRule.SetAttribute("Description", "Allows Everyone to execute from " + $pathToAllow) + $elemConditions = $xDocument.CreateElement("Conditions") + $elemCondition = $xDocument.CreateElement("FilePathCondition") + $elemCondition.SetAttribute("Path", $pathToAllow) + $elemConditions.AppendChild($elemCondition) | Out-Null + $elemRule.AppendChild($elemConditions) | Out-Null + $xRuleCollection.AppendChild($elemRule) | Out-Null + } + } + + # Incorporate path-exception rules for the user-writable directories under %windir% + # in the the EXE, DLL, and SCRIPT rules. + # Find the placeholders for Windows subdirectories, and add the path conditions there. + # Then remove the placeholders. + $xPlaceholders = $xDocument.SelectNodes("//PLACEHOLDER_WINDIR_WRITABLEDIRS") + foreach ($xPlaceholder in $xPlaceholders) + { + $xExcepts = $xPlaceholder.ParentNode + $Wr_windir | ForEach-Object { + $elem = $xDocument.CreateElement("FilePathCondition") + $elem.SetAttribute("Path", $_ + "\*") + $xExcepts.AppendChild($elem) | Out-Null + } + $xExcepts.RemoveChild($xPlaceholder) | Out-Null + } + + # Incorporate path-exception rules for the user-writable directories under %PF% + # in EXE, DLL, and SCRIPT rules. + # Find the placeholders for PF subdirectories, and add the path conditions there. + # Then remove the placeholders. + $xPlaceholders = $xDocument.SelectNodes("//PLACEHOLDER_PF_WRITABLEDIRS") + foreach ($xPlaceholder in $xPlaceholders) + { + $xExcepts = $xPlaceholder.ParentNode + $Wr_PF | ForEach-Object { + $elem = $xDocument.CreateElement("FilePathCondition") + $elem.SetAttribute("Path", $_ + "\*") + $xExcepts.AppendChild($elem) | Out-Null + } + $xExcepts.RemoveChild($xPlaceholder) | Out-Null + } + #endregion Incorporate data for EXE files to blacklist under Windir + + + #################################################################################################### + # Begin creating dynamically-generated rule fragments. Delete old ones first. + #################################################################################################### + + # Delete previous set of dynamically-generated rules first + Remove-Item ([System.IO.Path]::Combine($mergeRulesDynamicDir, "*.xml")) + + #region Create rules for trusted publishers + Write-Host "Creating rules for trusted publishers..." -ForegroundColor Cyan + + # Define an empty AppLocker policy to fill, with a blank publisher rule to use as a template. + $signerPolXml = [xml]@" + + + + + + + + + + + + + + +"@ + # Get the blank publisher rule. It will be cloned to make the real publisher rules, and then this blank will be deleted. + $fprTemplate = $signerPolXml.DocumentElement.SelectNodes("//FilePublisherRule")[0] + + # Run the script that produces the signer information to process. Should come in as a sequence of hashtables. + # Each hashtable must have a label, and either an exemplar or a publisher. + # fprRulesNotEmpty: Don't generate TrustedSigners.xml if it doesn't have any rules. + $fprRulesNotEmpty = $false + $signersToBuildRulesFor = (& $ps1_TrustedSigners) + $signersToBuildRulesFor | ForEach-Object { + $label = $_.label + if ($label -eq $null) + { + # Each hashtable must have a label. + Write-Warning -Message ("Invalid syntax in $ps1_TrustedSigners. No `"label`" specified.") + } + else + { + $publisher = $product = $binaryname = "" + $filename = "" + $good = $false + # Exemplar is a file signed by the publisher we want to trust. If the hashtable specifies "useProduct" = $true, + # the AppLocker rule allows anything signed by that publisher with the same ProductName. + if ($_.exemplar) + { + $filename = $_.exemplar + $alfi = Get-AppLockerFileInformation $filename + if ($alfi -eq $null) + { + Write-Warning -Message ("Cannot get AppLockerFileInformation for $filename") + } + elseif (!($alfi.Publisher.HasPublisherName)) + { + Write-Warning -Message ("Cannot get publisher information for $filename") + } + elseif ($_.useProduct -and !($alfi.Publisher.HasProductName)) + { + Write-Warning "Cannot get product name information for $filename" + } + else + { + # Get publisher to trust, and optionally ProductName. + $publisher = $alfi.Publisher.PublisherName + if ($_.useProduct) + { + $product = $alfi.Publisher.ProductName + } + $good = $true + } + } + else + { + # Otherwise, the hashtable must specify the exact publisher to trust (and optionally ProductName, BinaryName+collection). + $publisher = $_.PublisherName + $product = $_.ProductName + $binaryName = $_.BinaryName + $fileVersion = $_.FileVersion + $ruleCollection = $_.RuleCollection + if ($null -ne $publisher) + { + $good = $true + } + else + { + # Object isn't a hashtable, or doesn't have either exemplar or PublisherName. + Write-Warning -Message ("Invalid syntax in $ps1_TrustedSigners") + } + } + + if ($good) + { + $fprRulesNotEmpty = $true + + # Duplicate the blank publisher rule, and populate it with information gathered. + $fpr = $fprTemplate.Clone() + $fpr.Conditions.FilePublisherCondition.PublisherName = $publisher + + $fpr.Name = "$label`: Signer rule for $publisher" + if ($product.Length -gt 0) + { + $fpr.Conditions.FilePublisherCondition.ProductName = $product + $fpr.Name = "$label`: Signer/product rule for $publisher/$product" + if ($binaryName.Length -gt 0) + { + $fpr.Conditions.FilePublisherCondition.BinaryName = $binaryName + $fpr.Name = "$label`: Signer/product/file rule for $publisher/$product/$binaryName" + if ($fileVersion.Length -gt 0) + { + $fpr.Conditions.FilePublisherCondition.BinaryVersionRange.LowSection = $fileVersion + } + } + } + if ($filename.Length -gt 0) + { + $fpr.Description = "Information acquired from $filename" + } + else + { + $fpr.Description = "Information acquired from $fname_TrustedSigners" + } + Write-Host ("`t" + $fpr.Name) -ForegroundColor Cyan + + if ($publisher.ToLower().Contains("microsoft") -and $product.Length -eq 0 -and ($ruleCollection.Length -eq 0 -or $ruleCollection -eq "Exe")) + { + Write-Warning -Message ("Warning: Trusting all Microsoft-signed files is an overly-broad whitelisting strategy") + } + + if ($ruleCollection) + { + $node = $signerPolXml.SelectSingleNode("//RuleCollection[@Type='" + $ruleCollection + "']") + if ($node -eq $null) + { + Write-Warning ("Couldn't find RuleCollection Type = " + $ruleCollection + " (RuleCollection is case-sensitive)") + } + else + { + $fpr.Id = [string]([GUID]::NewGuid().Guid) + $node.AppendChild($fpr) | Out-Null + } + } + else + { + # Append a copy of the new publisher rule into each rule set with a different GUID in each. + $signerPolXml.SelectNodes("//RuleCollection") | ForEach-Object { + $fpr0 = $fpr.CloneNode($true) + + $fpr0.Id = [string]([GUID]::NewGuid().Guid) + $_.AppendChild($fpr0) | Out-Null + } + } + } + } + } + + # Don't generate the file if it doesn't contain any rules + if ($fprRulesNotEmpty) + { + # Delete the blank publisher rule from the rule set. + $fprTemplate.ParentNode.RemoveChild($fprTemplate) | Out-Null + + #$signerPolXml.OuterXml | clip + $outfile = [System.IO.Path]::Combine($mergeRulesDynamicDir, "TrustedSigners.xml") + # Save XML as Unicode + SaveXmlDocAsUnicode -xmlDoc $signerPolXml -xmlFilename $outfile + } + #endregion Create rules for trusted publishers + + #region Create custom hash rules + Write-Host "Creating extra hash rules ..." -ForegroundColor Cyan + + # Define an empty AppLocker policy to fill, with a blank hash rule to use as a template. + $hashRuleXml = [xml]@" + + + + + + + + + + + + + + +"@ + # Get the blank hash rule. It will be cloned to make the real hash rules. + $fhrTemplate = $hashRuleXml.DocumentElement.SelectNodes("//FileHashRule")[0] + # Remove the template rule from the main document + $fhrTemplate.ParentNode.RemoveChild($fhrTemplate) | Out-Null + # fhrRulesNotEmpty: Don't generate ExtraHashRules.xml if it doesn't have any rules. + $fhrRulesNotEmpty = $false + + # Run the script that produces the hash information to process. Should come in as a sequence of hashtables. + # Each hashtable must have the following properties: + # * RuleCollection (case-sensitive) + # * RuleName + # * RuleDesc + # * HashVal (must be SHA256 with "0x" and 64 hex digits) + # * FileName + Get-ALRuleHash | ForEach-Object { + + $fhr = $fhrTemplate.Clone() + $fhr.Id = [string]([GUID]::NewGuid().Guid) + $fhr.Name = $_.RuleName + $fhr.Description = $_.RuleDesc + $fhr.Conditions.FileHashCondition.FileHash.Data = $_.HashVal + $fhr.Conditions.FileHashCondition.FileHash.SourceFileName = $_.FileName + + $node = $hashRuleXml.SelectSingleNode("//RuleCollection[@Type='" + $_.RuleCollection + "']") + if ($node -eq $null) + { + Write-Warning ("Couldn't find RuleCollection Type = " + $_.RuleCollection + " (RuleCollection is case-sensitive)") + } + else + { + $node.AppendChild($fhr) | Out-Null + $fhrRulesNotEmpty = $true + } + } + + # Don't generate the file if it doesn't contain any rules + if ($fhrRulesNotEmpty) + { + $outfile = [System.IO.Path]::Combine($mergeRulesDynamicDir, "ExtraHashRules.xml") + # Save XML as Unicode + SaveXmlDocAsUnicode -xmlDoc $hashRuleXml -xmlFilename $outfile + } + #endregion Create custom hash rules + + #region Rules for files in user-writable directories + # -------------------------------------------------------------------------------- + # Build rules for files in writable directories identified in the "unsafe paths to build rules for" script. + # Uses BuildRulesForFilesInWritableDirectories.ps1. + # Writes results to the dynamic merge-rules directory, using the script-supplied labels as part of the file name. + # The files in the merge-rules directories will be merged into the main document later. + # (Doing this after the other files are created in the MergeRulesDynamicDir - file naming logic handles cases where + # file already exists from the other dynamically-generated files above, or if multiple items have the same label. + + if (!(Test-Path($ps1_UnsafePathsToBuildRulesFor))) + { + $errmsg = "Script file not found: $ps1_UnsafePathsToBuildRulesFor`nNo new rules generated for files in writable directories." + Write-Warning $errmsg + } + else + { + Write-Host "Creating rules for files in writable directories..." -ForegroundColor Cyan + Get-ALRulePath | ForEach-Object { + $label = $_.label + if ($ForUser) + { + $paths = Rename-Paths -Paths $_.paths -ForUsername $ForUser + } + else + { + $paths = $_.paths + } + $paramConvertToALPolicy = @{ + Path = $paths + Recurse = (-not ([bool]$_.noRecurse)) + EnforceMinimumVersion = ([bool]$_.enforceMinVersion) + RuleNamePrefix = $label + } + + $outfile = Join-Path $mergeRulesDynamicDir "$($label)Rules.xml" + # If it already exists, create a name that doesn't exist yet + $ixOutfile = 2 + while (Test-Path -Path $outfile) + { + $outfile = Join-Path $mergeRulesDynamicDir "$($label) ($($ixOutfile)) Rules.xml" + $ixOutfile++ + } + Write-Host ("Scanning $label`:", $paths) -Separator "`n`t" -ForegroundColor Cyan + + SaveAppLockerPolicyAsUnicodeXml -ALPolicy (ConvertTo-ALPolicy @paramConvertToALPolicy | Join-ALPolicy) -xmlFilename $outfile + } + } + #endregion Rules for files in user-writable directories + + #################################################################################################### + # Merging custom rules + #################################################################################################### + + # -------------------------------------------------------------------------------- + # Load the XML document with modifications into an AppLockerPolicy object + $masterPolicy = [Microsoft.Security.ApplicationId.PolicyManagement.PolicyModel.AppLockerPolicy]::FromXml($xDocument.OuterXml) + + Write-Host "Loading custom rule sets..." -ForegroundColor Cyan + # Merge any and all policy files found in the MergeRules directories, typically for authorized files in writable directories. + # Some may have been created in the previous step; others might have been dropped in from other sources. + Get-ChildItem $mergeRulesDynamicDir\*.xml, $mergeRulesStaticDir\*.xml | ForEach-Object { + $policyFileToMerge = $_ + Write-Host ("`tMerging " + $_.Directory.Name + "\" + $_.Name) -ForegroundColor Cyan + $policyToMerge = [Microsoft.Security.ApplicationId.PolicyManagement.PolicyModel.AppLockerPolicy]::Load($policyFileToMerge) + $masterPolicy.Merge($policyToMerge) + } + + #TODO: Optimize rules in rule collections here - combine/remove redundant/overlapping rules + + #region Generate final outputs + # Generate two versions of the rules file: one with rules enforced, and one with auditing only. + Write-Host "Creating final rule outputs..." -ForegroundColor Cyan + + # Generate the Enforced version + foreach ($ruleCollection in $masterPolicy.RuleCollections) + { + $ruleCollection.EnforcementMode = "Enabled" + } + SaveAppLockerPolicyAsUnicodeXml -ALPolicy $masterPolicy -xmlFilename $rulesFileEnforceNew + + # Generate the AuditOnly version + foreach ($ruleCollection in $masterPolicy.RuleCollections) + { + $ruleCollection.EnforcementMode = "AuditOnly" + } + SaveAppLockerPolicyAsUnicodeXml -ALPolicy $masterPolicy -xmlFilename $rulesFileAuditNew + + if ($Excel) + { + Export-ALPolicyToExcel -AppLockerXML $rulesFileEnforceNew -SaveWorkbook + Export-ALPolicyToExcel -AppLockerXML $rulesFileAuditNew -SaveWorkbook + } + #endregion Generate final outputs +} \ No newline at end of file diff --git a/AaronLocker/legacy/internal/functions/Export-PolicyToCsv.ps1 b/AaronLocker/legacy/internal/functions/Export-PolicyToCsv.ps1 new file mode 100644 index 0000000..d7c7460 --- /dev/null +++ b/AaronLocker/legacy/internal/functions/Export-PolicyToCsv.ps1 @@ -0,0 +1,193 @@ +function Export-PolicyToCsv +{ +<# + .SYNOPSIS + Turn AppLocker policy into more human-readable CSV. + + .DESCRIPTION + Script reads AppLocker policy from local policy, effective policy, or an XML file, and renders it as a tab-delimited CSV that can be pasted into Microsoft Excel, with easy sorting and filtering. + + If neither -AppLockerPolicyFile or -Local is specified, the script processes the current computer's effective policy. + + If -linebreakSeq is not specified, CRLF and LF sequences in attribute values are replaced with "^|^". The linebreak sequence can be replaced after importing results into Excel (in the Find/Replace dialog, replace the sequence with Ctrl+Shift+J). + + .PARAMETER AppLockerPolicyFile + If this optional string parameter is specified, AppLocker policy is read from the specified XML file. + + .PARAMETER Local + If this switch is specified, the script processes the current computer's local policy. + + .PARAMETER linebreakSeq + If this optional string parameter is specified, CRLF and LF sequences in attribute values are replaced with the specified sequence. "^|^" is the default. + + .EXAMPLE + ExportPolicy-ToCsv.ps1 | clip.exe + + Renders effective AppLocker policy to tab-delimited CSV and writes that output to the clipboard using the built-in Windows clip.exe utility. + Paste the output directly into an Excel spreadsheet, replace "^|^" with Ctrl+Shift+J, add filtering, freeze the top row, and autosize. + + .NOTES + #TODO: Add option to get AppLocker policy from AD GPO + E.g., + Get-AppLockerPolicy -Domain -LDAP "LDAP://DC13.Contoso.com/CN={31B2F340-016D-11D2-945F-00C04FB984F9},CN=Policies,CN=System,DC=Contoso,DC=com" + Figure out how to tie Get-GPO in with this... +#> + + [CmdletBinding()] + param ( + [String] + $AppLockerPolicyFile, + + [switch] + $Local, + + [parameter(Mandatory = $false)] + [String] + $linebreakSeq = "^|^" + ) + + + $tab = "`t" + + if ($AppLockerPolicyFile.Length -gt 0) + { + # Get policy from a file + $x = [xml](Get-Content $AppLockerPolicyFile) + } + elseif ($Local) + { + # Inspect local policy + $x = [xml](Get-AppLockerPolicy -Local -Xml) + } + else + { + # Inspect effecive policy + $x = [xml](Get-AppLockerPolicy -Effective -Xml) + } + + # CSV Headers + "FileType" + $tab + + "Enforce" + $tab + + "RuleType" + $tab + + "UserOrGroup" + $tab + + "Action" + $tab + + "RuleInfo" + $tab + + "Exceptions" + $tab + + "Name" + $tab + + "Description" + + + $x.AppLockerPolicy.RuleCollection | ForEach-Object { + $filetype = $_.Type + $enforce = $_.EnforcementMode + + if ($_.ChildNodes.Count -eq 0) + { + $filetype + $tab + + $enforce + $tab + + "N/A" + $tab + + "N/A" + $tab + + "N/A" + $tab + + "N/A" + $tab + + "N/A" + $tab + + "N/A" + $tab + + "N/A" + } + else + { + $_.ChildNodes | ForEach-Object { + + $childNode = $_ + switch ($childNode.LocalName) + { + + "FilePublisherRule" + { + $ruletype = "Publisher" + $condition = $childNode.Conditions.FilePublisherCondition + $ruleInfo = + "Publisher: " + $condition.PublisherName + $linebreakSeq + + "Product: " + $condition.ProductName + $linebreakSeq + + "BinaryName: " + $condition.BinaryName + $linebreakSeq + + "LowVersion: " + $condition.BinaryVersionRange.LowSection + $linebreakSeq + + "HighVersion: " + $condition.BinaryVersionRange.HighSection + } + + "FilePathRule" + { + $ruletype = "Path" + $ruleInfo = $childNode.Conditions.FilePathCondition.Path + } + + "FileHashRule" + { + $ruletype = "Hash" + $condition = $childNode.Conditions.FileHashCondition.FileHash + $ruleInfo = $condition.SourceFileName + "; length = " + $condition.SourceFileLength + } + + default { $ruletype = $_.LocalName; $condition = $ruleInfo = [string]::Empty; } + + } + + $exceptions = [string]::Empty + if ($null -ne $childNode.Exceptions) + { + # Output exceptions with a designated separator character sequence that can be replaced with line feeds in Excel + $arrExceptions = @() + if ($null -ne $childNode.Exceptions.FilePathCondition) + { + $arrExceptions += "[----- Path exceptions -----]" + $arrExceptions += ($childNode.Exceptions.FilePathCondition.Path | Sort-Object) + } + if ($null -ne $childNode.Exceptions.FilePublisherCondition) + { + $arrExceptions += "[----- Publisher exceptions -----]" + $arrExceptions += ($childNode.Exceptions.FilePublisherCondition | + ForEach-Object { + $s = $_.BinaryName + ": " + $_.PublisherName + "; " + $_.ProductName + $bvrLow = $_.BinaryVersionRange.LowSection + $bvrHigh = $_.BinaryVersionRange.HighSection + if ($bvrLow -ne "*" -or $bvrHigh -ne "*") { $s += "; ver " + $bvrLow + " to " + $bvrHigh } + $s + } | Sort-Object) + } + if ($null -ne $childNode.Exceptions.FileHashCondition) + { + $arrExceptions += "[----- Hash exceptions -----]" + $arrExceptions += ($childNode.Exceptions.FileHashCondition.FileHash | ForEach-Object { $_.SourceFileName + "; length = " + $_.SourceFileLength } | Sort-Object) + } + $exceptions = $arrExceptions -join $linebreakSeq + } + + # Replace CRLF with line-break replacement string; then replace any left-over LF characters with it. + $name = $_.Name.Replace("`r`n", $linebreakSeq).Replace("`n", $linebreakSeq) + $description = $_.Description.Replace("`r`n", $linebreakSeq).Replace("`n", $linebreakSeq) + # Get user/group name if possible; otherwise show SID. #was: $userOrGroup = $_.UserOrGroupSid + $oSID = New-Object System.Security.Principal.SecurityIdentifier($_.UserOrGroupSid) + $oUser = $null + try { $oUser = $oSID.Translate([System.Security.Principal.NTAccount]) } + catch { } + if ($null -ne $oUser) + { + $userOrGroup = $oUser.Value + } + else + { + $userOrGroup = $_.UserOrGroupSid + } + $action = $_.Action + + $filetype + $tab + + $enforce + $tab + + $ruletype + $tab + + $userOrGroup + $tab + + $action + $tab + + $ruleInfo + $tab + + $exceptions + $tab + + $name + $tab + + $description + } + } + } +} \ No newline at end of file diff --git a/AaronLocker/legacy/internal/functions/Search-WritableDirectory.ps1 b/AaronLocker/legacy/internal/functions/Search-WritableDirectory.ps1 new file mode 100644 index 0000000..4056790 --- /dev/null +++ b/AaronLocker/legacy/internal/functions/Search-WritableDirectory.ps1 @@ -0,0 +1,240 @@ +function Search-WritableDirectory +{ +<# + .SYNOPSIS + Enumerates "user-writable" subdirectories. + + .DESCRIPTION + Enumerates subdirectories that are writable by accounts other than a set of + known admin or admin-equivalent entities (including members of the local + Administrators group). The goal is to list user-writable directories in + which end user program execution should be disallowed via AppLocker. + You should run this script with administrative rights to avoid access- + denied errors. + + NOTE: Requires Sysinternals AccessChk.exe: + https://technet.microsoft.com/sysinternals/accesschk + https://download.sysinternals.com/files/AccessChk.zip + NOTE: Requires Windows PowerShell 5.1 or newer (relies on Get-LocalGroup and + Get-LocalGroupMember cmdlets). + + Note: this script does not discover user-writable files. A user-writable + file in a non-writable directory presents a similar risk, as a non-admin + can overwrite it with arbitrary content and execute it. + + .PARAMETER RootDirectory + The starting directory for the permission enumeration. + + .PARAMETER ShowGrantees + If set, output includes the names of the non-admin entities that have write permissions + + .PARAMETER DontFilterNTService + By default, this script ignores access granted to NT SERVICE\ accounts (SID beginning with S-1-5-80-). + If this switch is set, this script does not ignore that access, except for access granted to NT SERVICE\TrustedInstaller. + + .PARAMETER OutputXML + If set, output is formatted as XML. + + .PARAMETER KnownAdmins + Optional: additional list of known administrative users and groups. + + .EXAMPLE + PS C:\> Search-WritableDirectory -RootDirectory C:\Windows\System32 + + Output: + C:\Windows\System32\FxsTmp + C:\Windows\System32\Tasks + C:\Windows\System32\Com\dmp + C:\Windows\System32\Microsoft\Crypto\RSA\MachineKeys + C:\Windows\System32\spool\PRINTERS + C:\Windows\System32\spool\SERVERS + C:\Windows\System32\spool\drivers\color + C:\Windows\System32\Tasks\Microsoft IT Diagnostics Utility + C:\Windows\System32\Tasks\Microsoft IT VPN + C:\Windows\System32\Tasks\WPD + C:\Windows\System32\Tasks\Microsoft\Windows\RemoteApp and Desktop Connections Update + C:\Windows\System32\Tasks\Microsoft\Windows\SyncCenter + C:\Windows\System32\Tasks\Microsoft\Windows\WCM + C:\Windows\System32\Tasks\Microsoft\Windows\PLA\System + + .EXAMPLE + PS C:\> Search-WritableDirectory -RootDirectory C:\Windows\System32 -ShowGrantees + + Output: + C:\Windows\system32\FxsTmp + BUILTIN\Users + C:\Windows\system32\Tasks + NT AUTHORITY\Authenticated Users + C:\Windows\system32\Com\dmp + BUILTIN\Users + C:\Windows\system32\Microsoft\Crypto\RSA\MachineKeys + Everyone + C:\Windows\system32\spool\PRINTERS + BUILTIN\Users + C:\Windows\system32\spool\SERVERS + BUILTIN\Users + C:\Windows\system32\spool\drivers\color + BUILTIN\Users + C:\Windows\system32\Tasks\Microsoft IT Diagnostics Utility + NT AUTHORITY\Authenticated Users + C:\Windows\system32\Tasks\Microsoft IT VPN + NT AUTHORITY\Authenticated Users + C:\Windows\system32\Tasks\WPD + NT AUTHORITY\Authenticated Users + aaronmar5\aaronmaradmin + C:\Windows\system32\Tasks\Microsoft\Windows\RemoteApp and Desktop Connections Update + NT AUTHORITY\Authenticated Users + C:\Windows\system32\Tasks\Microsoft\Windows\SyncCenter + BUILTIN\Users + C:\Windows\system32\Tasks\Microsoft\Windows\WCM + BUILTIN\Users + C:\Windows\system32\Tasks\Microsoft\Windows\PLA\System + Everyone + + .EXAMPLE + PS C:\> $x = [xml](Search-WritableDirectory -RootDirectory C:\Windows\System32 -ShowGrantees -OutputXML) + PS C:\> $x.root.dir | Sort-Object name + + Output: + name Grantee + ---- ------- + C:\Windows\System32\Com\dmp BUILTIN\Users + C:\Windows\System32\FxsTmp BUILTIN\Users + C:\Windows\System32\Microsoft\Crypto\RSA\MachineKeys Everyone + C:\Windows\System32\spool\drivers\color BUILTIN\Users + C:\Windows\System32\spool\PRINTERS BUILTIN\Users + C:\Windows\System32\spool\SERVERS BUILTIN\Users + C:\Windows\System32\Tasks NT AUTHORITY\Authenticated Users + C:\Windows\System32\Tasks\Microsoft IT Diagnostics Utility NT AUTHORITY\Authenticated Users + C:\Windows\System32\Tasks\Microsoft IT VPN NT AUTHORITY\Authenticated Users + C:\Windows\System32\Tasks\Microsoft\Windows\PLA\System Everyone + C:\Windows\System32\Tasks\Microsoft\Windows\RemoteApp and Desktop Connection... NT AUTHORITY\Authenticated Users + C:\Windows\System32\Tasks\Microsoft\Windows\SyncCenter BUILTIN\Users + C:\Windows\System32\Tasks\Microsoft\Windows\WCM BUILTIN\Users + C:\Windows\System32\Tasks\WPD {NT AUTHORITY\Authenticated Users, vm-t2408\admin} + + .LINK + Sysinternals AccessChk available here: + https://technet.microsoft.com/sysinternals/accesschk + https://download.sysinternals.com/files/AccessChk.zip +#> + [CmdletBinding()] + param ( + [parameter(Mandatory = $true)] + [String] + $RootDirectory, + + [switch] + $ShowGrantees, + + [switch] + $DontFilterNTService, + + [switch] + $OutputXML, + + [String[]] + $KnownAdmins + ) + + if (-not (Test-AccessChk)) + { + $errMsg = @" +Scanning for writable subdirectories requires that Sysinternals AccessChk.exe be available. +Please download it and use Set-ALConfiguration -PathAccessChk "" to register its location. +"AccessChk.exe was not found. Exiting... +"@ + throw $errMsg + } + + # If RootDirectory has a trailing backslash, remove it (AccessChk doesn't handle it correctly). + if ($RootDirectory.EndsWith("\")) { $RootDirectory = $RootDirectory.Substring(0, $RootDirectory.Length - 1) } + + # Entities for which to ignore write permissions. + # TrustedInstaller is always ignored; other NT SERVICE\ accounts are filtered + # out later (too many to list and too many unknown). + # The Package SIDs below (S-1-15-2-*) are associated with microsoft.windows.fontdrvhost and + # are not a problem. AppContainers never grant additional access; they only reduce access. + $FilterOut0 = @" +S-1-3-0 +S-1-5-18 +S-1-5-19 +S-1-5-20 +S-1-5-32-544 +S-1-5-32-549 +S-1-5-32-550 +S-1-5-32-551 +S-1-5-32-577 +S-1-5-32-559 +S-1-5-32-568 +NT SERVICE\TrustedInstaller +S-1-15-2-1430448594-2639229838-973813799-439329657-1197984847-4069167804-1277922394 +S-1-15-2-95739096-486727260-2033287795-3853587803-1685597119-444378811-2746676523 +"@ + # Filter all the above plus caller-supplied "known admins" + $FilterOut = ($FilterOut0.Split("`n`r") + $KnownAdmins | Where-Object Length -gt 0) -join "," + # Add all members of the local Administrators group, as the Effective Permissions + # APIs consider them to be administrators also. + # For some reason, Get-LocalGroup/Get-LocalGroupMember aren't available on WMFv5.0 on Win7; + # Verify whether command exists before using it. The commands are available on Win7 in v5.1. + if ($null -ne (Get-Command Get-LocalGroupMember -ErrorAction SilentlyContinue)) + { + #TODO: Detect and handle case where this cmdlet fails - disconnected and the admins group contains domain SIDs that can't be resolved. + #FWIW, NET LOCALGROUP Administrators doesn't report these entries either. + #Also fails on AAD-joined, with unresolved SIDs beginning with S-1-12-1-... + Get-LocalGroupMember -SID S-1-5-32-544 -ErrorAction SilentlyContinue | ForEach-Object { $FilterOut += "," + $_.SID.Value } + } + + $currfile = "" + + if ($OutputXML) { "" } + + $bInElem = $false + + & $script:config.PathAccessChk /accepteula -nobanner -w -d -s -f $FilterOut $RootDirectory | ForEach-Object { + if ($_.StartsWith(" ") -or $_.Length -eq 0) + { + if ($_.StartsWith(" RW ") -or $_.StartsWith(" W ")) + { + $grantee = $_.Substring(5).Trim() + if ($DontFilterNTService -or (!$grantee.StartsWith("NT SERVICE\") -and !$grantee.StartsWith("S-1-5-80-"))) + { + if ($currfile.Length -gt 0) + { + if ($OutputXML) + { + # Path name has to be escaped for XML + "" + } + else + { + $currfile + } + $currfile = "" + $bInElem = $true + } + if ($ShowGrantees) + { + if ($OutputXML) + { + "" + $grantee + "" + } + else + { + " " + $grantee + } + } + } + } + } + else + { + if ($bInElem -and $OutputXML) { "" } + $currfile = $_ + $bInElem = $false + } + } + + if ($bInElem -and $OutputXML) { "" } + if ($OutputXML) { "" } +} \ No newline at end of file diff --git a/AaronLocker/legacy/internal/functions/legacy/AddNewWorksheet.ps1 b/AaronLocker/legacy/internal/functions/legacy/AddNewWorksheet.ps1 new file mode 100644 index 0000000..f88bffc --- /dev/null +++ b/AaronLocker/legacy/internal/functions/legacy/AddNewWorksheet.ps1 @@ -0,0 +1,31 @@ +function AddNewWorksheet +{ + [CmdletBinding()] + param ( + [string] + $tabname + ) + if ($null -eq $script:ExcelAppInstance) { return $null } + + if ($script:ExcelAppInstance.Workbooks.Count -eq 0) + { + $workbook = $script:ExcelAppInstance.Workbooks.Add(5) + $worksheet = $workbook.Sheets(1) + } + else + { + $workbook = $script:ExcelAppInstance.Workbooks[1] + $worksheet = $workbook.Worksheets.Add([System.Type]::Missing, $workbook.Worksheets[$workbook.Worksheets.Count]) + } + if ($tabname.Length -gt 0) + { + # Excel limits tab names to 31 characters + if ($tabname.Length -gt 31) + { + $tabname = $tabname.Substring(0, 31) + } + $worksheet.Name = $tabname + } + + $worksheet +} \ No newline at end of file diff --git a/AaronLocker/legacy/internal/functions/legacy/AddWorksheetFromCsvData.ps1 b/AaronLocker/legacy/internal/functions/legacy/AddWorksheetFromCsvData.ps1 new file mode 100644 index 0000000..0760f71 --- /dev/null +++ b/AaronLocker/legacy/internal/functions/legacy/AddWorksheetFromCsvData.ps1 @@ -0,0 +1,39 @@ +function AddWorksheetFromCsvData +{ + [CmdletBinding()] + param ( + [string[]] + $csv, + + [string] + $tabname, + + [string] + $CrLfEncoded + + ) + Write-Host "Preparing data for tab `"$tabname`"..." -ForegroundColor Cyan + + if ($null -eq $script:ExcelAppInstance) { return $null } + + if ($null -ne $csv) + { + $OutputEncodingPrevious = $OutputEncoding + $OutputEncoding = [System.Text.ASCIIEncoding]::Unicode + + $tempfile = [System.IO.Path]::GetTempFileName() + + $csv | Out-File $tempfile -Encoding unicode + + AddWorksheetFromCsvFile -filename $tempfile -tabname $tabname -CrLfEncoded $CrLfEncoded + + Remove-Item $tempfile + + $OutputEncoding = $OutputEncodingPrevious + } + else + { + $worksheet = AddNewWorksheet -tabname $tabname + $null = [System.Runtime.Interopservices.Marshal]::ReleaseComObject($worksheet) + } +} \ No newline at end of file diff --git a/AaronLocker/legacy/internal/functions/legacy/AddWorksheetFromCsvFile.ps1 b/AaronLocker/legacy/internal/functions/legacy/AddWorksheetFromCsvFile.ps1 new file mode 100644 index 0000000..11857d2 --- /dev/null +++ b/AaronLocker/legacy/internal/functions/legacy/AddWorksheetFromCsvFile.ps1 @@ -0,0 +1,83 @@ +function AddWorksheetFromCsvFile +{ + [CmdletBinding()] + param ( + [string] + $filename, + + [string] + $tabname, + + [string] + $CrLfEncoded + ) + + Write-Host "Populating tab `"$tabname`"..." -ForegroundColor Cyan + + if ($null -eq $script:ExcelAppInstance) { return $null } + + $worksheet = AddNewWorksheet -tabname $tabname + + ### Build the QueryTables.Add command + ### QueryTables does the same as when clicking "Data -> From Text" in Excel + $TxtConnector = ("TEXT;" + $filename) + $Connector = $worksheet.QueryTables.add($TxtConnector, $worksheet.Range("A1")) + $query = $worksheet.QueryTables.item($Connector.name) + $query.TextFileTabDelimiter = $true + + ### Execute & delete the import query + $null = $query.Refresh() + $query.Delete() + + if ($CrLfEncoded.Length -gt 0) + { + # Replace linebreak-replacement sequence in CSV with CRLF. + $null = $worksheet.UsedRange.Replace($CrLfEncoded, "`r`n") + } + + # Formatting: autofilter, font size, vertical alignment, freeze top row + $null = $worksheet.Cells.AutoFilter() + $worksheet.Cells.Font.Size = 9.5 + $worksheet.UsedRange.VerticalAlignment = -4160 # xlTop + $script:ExcelAppInstance.ActiveWindow.SplitColumn = 0 + $script:ExcelAppInstance.ActiveWindow.SplitRow = 1 + $script:ExcelAppInstance.ActiveWindow.FreezePanes = $true + $script:ExcelAppInstance.ActiveWindow.Zoom = 80 + + $null = $worksheet.Range("A2").Select() + + # Formatting: autosize column widths, then set maximum width (except on last column) + $maxWidth = 40 + $maxHeight = 120 + + $null = $worksheet.Cells.EntireColumn.AutoFit() + $ix = 1 + # Do this until the next to last column; don't set max width on the last column + while ($worksheet.Cells(1, $ix + 1).Text.Length -gt 0) + { + $cells = $worksheet.Cells(1, $ix) + #Write-Host ($cells.Text + "; " + $cells.ColumnWidth) + if ($cells.ColumnWidth -gt $maxWidth) { $cells.ColumnWidth = $maxWidth } + $ix++ + } + + # Formatting: autosize row heights, then set maximum height (if CrLf replacement on) + $null = $worksheet.Cells.EntireRow.AutoFit() + # If line breaks added, limit autofit row height to + if ($CrLfEncoded.Length -gt 0) + { + $ix = 1 + while ($worksheet.Cells($ix, 1).Text.Length -gt 0) + { + $cells = $worksheet.Cells($ix, 1) + #Write-Host ($ix.ToString() + "; " + $cells.RowHeight) + if ($cells.RowHeight -gt $maxHeight) { $cells.RowHeight = $maxHeight } + $ix++ + } + } + + # Release COM interface references + $null = [System.Runtime.Interopservices.Marshal]::ReleaseComObject($query) + $null = [System.Runtime.Interopservices.Marshal]::ReleaseComObject($Connector) + $null = [System.Runtime.Interopservices.Marshal]::ReleaseComObject($worksheet) +} \ No newline at end of file diff --git a/AaronLocker/legacy/internal/functions/legacy/AddWorksheetFromText.ps1 b/AaronLocker/legacy/internal/functions/legacy/AddWorksheetFromText.ps1 new file mode 100644 index 0000000..2c29c54 --- /dev/null +++ b/AaronLocker/legacy/internal/functions/legacy/AddWorksheetFromText.ps1 @@ -0,0 +1,37 @@ +function AddWorksheetFromText +{ + [CmdletBinding()] + param ( + [string[]] + $text, + + [string] + $tabname + ) + + Write-Host "Populating tab `"$tabname`"..." -ForegroundColor Cyan + + if ($null -eq $script:ExcelAppInstance) { return $null } + + $worksheet = AddNewWorksheet($tabname) + $worksheet.UsedRange.VerticalAlignment = -4160 # xlTop + + $row = [int]1 + foreach ($line in $text) + { + $iCol = [int][char]'A' + $lineparts = $line.Split("`t") + foreach ($part in $lineparts) + { + $cell = ([char]$iCol).ToString() + $row.ToString() + $worksheet.Range($cell).FormulaR1C1 = $part + $iCol++ + } + $row++ + } + + $null = $worksheet.Cells.EntireColumn.AutoFit() + + # Release COM interface references + $null = [System.Runtime.Interopservices.Marshal]::ReleaseComObject($worksheet) +} \ No newline at end of file diff --git a/AaronLocker/legacy/internal/functions/legacy/CreateExcelApplication.ps1 b/AaronLocker/legacy/internal/functions/legacy/CreateExcelApplication.ps1 new file mode 100644 index 0000000..19e4fd4 --- /dev/null +++ b/AaronLocker/legacy/internal/functions/legacy/CreateExcelApplication.ps1 @@ -0,0 +1,19 @@ +function CreateExcelApplication +{ + [CmdletBinding()] + param ( + + ) + Write-Host "Starting Excel..." -ForegroundColor Cyan + $script:ExcelAppInstance = New-Object -ComObject excel.application + if ($null -ne $script:ExcelAppInstance) + { + $script:ExcelAppInstance.Visible = $true + return $true + } + else + { + Write-Error "Apparently Excel is not installed. Can't create an Excel document without it. Exiting..." + return $false + } +} \ No newline at end of file diff --git a/AaronLocker/legacy/internal/functions/legacy/CreateExcelFromCsvFile.ps1 b/AaronLocker/legacy/internal/functions/legacy/CreateExcelFromCsvFile.ps1 new file mode 100644 index 0000000..3a49043 --- /dev/null +++ b/AaronLocker/legacy/internal/functions/legacy/CreateExcelFromCsvFile.ps1 @@ -0,0 +1,27 @@ +function CreateExcelFromCsvFile +{ + [CmdletBinding()] + param ( + [string] + $filename, + + [string] + $tabname, + + [string] + $CrLfEncoded, + + [string] + $saveAsName + ) + + if (CreateExcelApplication) + { + AddWorksheetFromCsvFile -filename $filename -tabname $tabname -CrLfEncoded $CrLfEncoded + if ($saveAsName.Length -gt 0) + { + SaveWorkbook -filename $saveAsName + } + ReleaseExcelApplication + } +} \ No newline at end of file diff --git a/AaronLocker/legacy/internal/functions/legacy/ReleaseExcelApplication.ps1 b/AaronLocker/legacy/internal/functions/legacy/ReleaseExcelApplication.ps1 new file mode 100644 index 0000000..57269bb --- /dev/null +++ b/AaronLocker/legacy/internal/functions/legacy/ReleaseExcelApplication.ps1 @@ -0,0 +1,11 @@ +function ReleaseExcelApplication +{ + [CmdletBinding()] + param ( + + ) + + Write-Host "Releasing Excel..." -ForegroundColor Cyan + $dummy = [System.Runtime.Interopservices.Marshal]::ReleaseComObject($script:ExcelAppInstance) + $script:ExcelAppInstance = $null +} \ No newline at end of file diff --git a/AaronLocker/legacy/internal/functions/legacy/SaveAppLockerPolicyAsUnicodeXml.ps1 b/AaronLocker/legacy/internal/functions/legacy/SaveAppLockerPolicyAsUnicodeXml.ps1 new file mode 100644 index 0000000..eada6f4 --- /dev/null +++ b/AaronLocker/legacy/internal/functions/legacy/SaveAppLockerPolicyAsUnicodeXml.ps1 @@ -0,0 +1,13 @@ +function SaveAppLockerPolicyAsUnicodeXml +{ + [CmdletBinding()] + param ( + [Microsoft.Security.ApplicationId.PolicyManagement.PolicyModel.AppLockerPolicy] + $ALPolicy, + + [string] + $xmlFilename + ) + + SaveXmlDocAsUnicode -xmlDoc ([xml]($ALPolicy.ToXml())) -xmlFilename $xmlFilename +} \ No newline at end of file diff --git a/AaronLocker/legacy/internal/functions/legacy/SaveWorkbook.ps1 b/AaronLocker/legacy/internal/functions/legacy/SaveWorkbook.ps1 new file mode 100644 index 0000000..29ac593 --- /dev/null +++ b/AaronLocker/legacy/internal/functions/legacy/SaveWorkbook.ps1 @@ -0,0 +1,13 @@ +function SaveWorkbook +{ + [CmdletBinding()] + param ( + [string] + $filename + ) + + Write-Host "Saving workbook as `"$filename`"..." -ForegroundColor Cyan + if ($null -eq $script:ExcelAppInstance) { return } + if ($script:ExcelAppInstance.Workbooks.Count -eq 0) { return } + $script:ExcelAppInstance.Workbooks[1].SaveAs($filename) +} \ No newline at end of file diff --git a/AaronLocker/legacy/internal/functions/legacy/SaveXmlDocAsUnicode.ps1 b/AaronLocker/legacy/internal/functions/legacy/SaveXmlDocAsUnicode.ps1 new file mode 100644 index 0000000..6753215 --- /dev/null +++ b/AaronLocker/legacy/internal/functions/legacy/SaveXmlDocAsUnicode.ps1 @@ -0,0 +1,35 @@ +function SaveXmlDocAsUnicode +{ +<# + .SYNOPSIS + Writes an xml document to file with unicode encoding + + .DESCRIPTION + Writes an xml document to file with unicode encoding + + .PARAMETER xmlDoc + The document to write. + + .PARAMETER xmlFilename + The path to write it to. + + .EXAMPLE + PS C:\> SaveXmlDocAsUnicode -xmlDoc $doc -xmlFilename "C:\temp\example.xml" + + Exports the specified document to C:\temp\example.xml +#> + [CmdletBinding()] + param ( + [System.Xml.XmlDocument] + $xmlDoc, + + [string] + $xmlFilename + ) + $xws = [System.Xml.XmlWriterSettings]::new() + $xws.Encoding = [System.Text.Encoding]::Unicode + $xws.Indent = $true + $xw = [System.Xml.XmlWriter]::Create($xmlFilename, $xws) + $xmlDoc.Save($xw) + $xw.Close() +} \ No newline at end of file diff --git a/AaronLocker/legacy/internal/functions/legacy/SelectFirstWorksheet.ps1 b/AaronLocker/legacy/internal/functions/legacy/SelectFirstWorksheet.ps1 new file mode 100644 index 0000000..427c5b9 --- /dev/null +++ b/AaronLocker/legacy/internal/functions/legacy/SelectFirstWorksheet.ps1 @@ -0,0 +1,10 @@ +function SelectFirstWorksheet +{ + [CmdletBinding()] + param ( + + ) + if ($null -eq $script:ExcelAppInstance) { return } + if ($script:ExcelAppInstance.Workbooks.Count -eq 0) { return } + $dummy = $script:ExcelAppInstance.Workbooks[1].Sheets(1).Select() +} \ No newline at end of file diff --git a/AaronLocker/readme.md b/AaronLocker/readme.md new file mode 100644 index 0000000..60bea84 --- /dev/null +++ b/AaronLocker/readme.md @@ -0,0 +1,17 @@ +# PSFModule guidance + +This is a finished module layout optimized for implementing the PSFramework. + +If you don't care to deal with the details, this is what you need to do to get started seeing results: + + - Add the functions you want to publish to `/functions/` + - Update the `FunctionsToExport` node in the module manifest (AaronLocker.psd1). All functions you want to publish should be in a list. + - Add internal helper functions the user should not see to `/internal/functions/` + + ## Path Warning + + > If you want your module to be compatible with Linux and MacOS, keep in mind that those OS are case sensitive for paths and files. + + `Import-ModuleFile` is preconfigured to resolve the path of the files specified, so it will reliably convert weird path notations the system can't handle. + Content imported through that command thus need not mind the path separator. + If you want to make sure your code too will survive OS-specific path notations, get used to using `Resolve-path` or the more powerful `Resolve-PSFPath`. \ No newline at end of file diff --git a/AaronLocker/snippets/CBH-Id.snippet b/AaronLocker/snippets/CBH-Id.snippet new file mode 100644 index 0000000..34f59e2 --- /dev/null +++ b/AaronLocker/snippets/CBH-Id.snippet @@ -0,0 +1,19 @@ + + +
+ CBH-Id + + Friedrich Weinmann + + Expansion + +
+ + + + + +
+
\ No newline at end of file diff --git a/AaronLocker/snippets/CBH-Identity.snippet b/AaronLocker/snippets/CBH-Identity.snippet new file mode 100644 index 0000000..fd9cca4 --- /dev/null +++ b/AaronLocker/snippets/CBH-Identity.snippet @@ -0,0 +1,19 @@ + + +
+ CBH-Identity + + Friedrich Weinmann + + Expansion + +
+ + + + + +
+
\ No newline at end of file diff --git a/AaronLocker/snippets/CBH-Policy.snippet b/AaronLocker/snippets/CBH-Policy.snippet new file mode 100644 index 0000000..6016047 --- /dev/null +++ b/AaronLocker/snippets/CBH-Policy.snippet @@ -0,0 +1,19 @@ + + +
+ CBH-Policy + + Friedrich Weinmann + + Expansion + +
+ + + + + +
+
\ No newline at end of file diff --git a/AaronLocker/snippets/CBH-PolicyName.snippet b/AaronLocker/snippets/CBH-PolicyName.snippet new file mode 100644 index 0000000..585a41a --- /dev/null +++ b/AaronLocker/snippets/CBH-PolicyName.snippet @@ -0,0 +1,18 @@ + + +
+ CBH-PolicyName + + Friedrich Weinmann + + Expansion + +
+ + + + + +
+
\ No newline at end of file diff --git a/AaronLocker/snippets/COD-ResolvePath.snippet b/AaronLocker/snippets/COD-ResolvePath.snippet new file mode 100644 index 0000000..7caa620 --- /dev/null +++ b/AaronLocker/snippets/COD-ResolvePath.snippet @@ -0,0 +1,18 @@ + + +
+ COD-ResolvePath + + Friedrich Weinmann + + Expansion + +
+ + + + + +
+
\ No newline at end of file diff --git a/AaronLocker/snippets/COD-ResolvePolicyName.snippet b/AaronLocker/snippets/COD-ResolvePolicyName.snippet new file mode 100644 index 0000000..729e9c0 --- /dev/null +++ b/AaronLocker/snippets/COD-ResolvePolicyName.snippet @@ -0,0 +1,18 @@ + + +
+ COD-ResolvePolicyName + + Friedrich Weinmann + + Expansion + +
+ + + + + +
+
\ No newline at end of file diff --git a/AaronLocker/snippets/FUN-Output.snippet b/AaronLocker/snippets/FUN-Output.snippet new file mode 100644 index 0000000..d7f65b2 --- /dev/null +++ b/AaronLocker/snippets/FUN-Output.snippet @@ -0,0 +1,66 @@ + + +
+ FUN-Output + + Friedrich Weinmann + + Expansion + +
+ + + + name + Name of the function + + name + + + + + + +
+
\ No newline at end of file diff --git a/AaronLocker/snippets/PAR-PolicyName.snippet b/AaronLocker/snippets/PAR-PolicyName.snippet new file mode 100644 index 0000000..b386f02 --- /dev/null +++ b/AaronLocker/snippets/PAR-PolicyName.snippet @@ -0,0 +1,18 @@ + + +
+ PAR-PolicyName + + Friedrich Weinmann + + Expansion + +
+ + + + + +
+
\ No newline at end of file diff --git a/AaronLocker/tests/functions/readme.md b/AaronLocker/tests/functions/readme.md new file mode 100644 index 0000000..f2b2ef0 --- /dev/null +++ b/AaronLocker/tests/functions/readme.md @@ -0,0 +1,7 @@ +# Description + +This is where the function tests go. + +Make sure to put them in folders reflecting the actual module structure. + +It is not necessary to differentiate between internal and public functions here. \ No newline at end of file diff --git a/AaronLocker/tests/general/FileIntegrity.Exceptions.ps1 b/AaronLocker/tests/general/FileIntegrity.Exceptions.ps1 new file mode 100644 index 0000000..8059fe2 --- /dev/null +++ b/AaronLocker/tests/general/FileIntegrity.Exceptions.ps1 @@ -0,0 +1,27 @@ +# List of forbidden commands +$global:BannedCommands = @( + 'Write-Host', + 'Write-Verbose', + 'Write-Warning', + 'Write-Error', + 'Write-Output', + 'Write-Information', + 'Write-Debug' +) + +<# + Contains list of exceptions for banned cmdlets. + Insert the file names of files that may contain them. + + Example: + "Write-Host" = @('Write-PSFHostColor.ps1','Write-PSFMessage.ps1') +#> +$global:MayContainCommand = @{ + "Write-Host" = @() + "Write-Verbose" = @() + "Write-Warning" = @() + "Write-Error" = @() + "Write-Output" = @() + "Write-Information" = @() + "Write-Debug" = @() +} \ No newline at end of file diff --git a/AaronLocker/tests/general/FileIntegrity.Tests.ps1 b/AaronLocker/tests/general/FileIntegrity.Tests.ps1 new file mode 100644 index 0000000..ada453e --- /dev/null +++ b/AaronLocker/tests/general/FileIntegrity.Tests.ps1 @@ -0,0 +1,90 @@ +$moduleRoot = (Resolve-Path "$PSScriptRoot\..\..").Path + +. "$PSScriptRoot\FileIntegrity.Exceptions.ps1" + +function Get-FileEncoding +{ +<# + .SYNOPSIS + Tests a file for encoding. + + .DESCRIPTION + Tests a file for encoding. + + .PARAMETER Path + The file to test +#> + [CmdletBinding()] + Param ( + [Parameter(Mandatory = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('FullName')] + [string] + $Path + ) + + [byte[]]$byte = get-content -Encoding byte -ReadCount 4 -TotalCount 4 -Path $Path + + if ($byte[0] -eq 0xef -and $byte[1] -eq 0xbb -and $byte[2] -eq 0xbf) { 'UTF8' } + elseif ($byte[0] -eq 0xfe -and $byte[1] -eq 0xff) { 'Unicode' } + elseif ($byte[0] -eq 0 -and $byte[1] -eq 0 -and $byte[2] -eq 0xfe -and $byte[3] -eq 0xff) { 'UTF32' } + elseif ($byte[0] -eq 0x2b -and $byte[1] -eq 0x2f -and $byte[2] -eq 0x76) { 'UTF7' } + else { 'Unknown' } +} + +Describe "Verifying integrity of module files" { + Context "Validating PS1 Script files" { + $allFiles = Get-ChildItem -Path $moduleRoot -Recurse -Filter "*.ps1" | Where-Object FullName -NotLike "$moduleRoot\tests\*" + + foreach ($file in $allFiles) + { + $name = $file.FullName.Replace("$moduleRoot\", '') + + It "[$name] Should have UTF8 encoding" { + Get-FileEncoding -Path $file.FullName | Should -Be 'UTF8' + } + + It "[$name] Should have no trailing space" { + ($file | Select-String "\s$" | Where-Object { $_.Line.Trim().Length -gt 0}).LineNumber | Should -BeNullOrEmpty + } + + $tokens = $null + $parseErrors = $null + $ast = [System.Management.Automation.Language.Parser]::ParseFile($file.FullName, [ref]$tokens, [ref]$parseErrors) + + It "[$name] Should have no syntax errors" { + $parseErrors | Should Be $Null + } + + foreach ($command in $global:BannedCommands) + { + if ($global:MayContainCommand["$command"] -notcontains $file.Name) + { + It "[$name] Should not use $command" { + $tokens | Where-Object Text -EQ $command | Should -BeNullOrEmpty + } + } + } + + It "[$name] Should not contain aliases" { + $tokens | Where-Object TokenFlags -eq CommandName | Where-Object { Test-Path "alias:\$($_.Text)" } | Measure-Object | Select-Object -ExpandProperty Count | Should -Be 0 + } + } + } + + Context "Validating help.txt help files" { + $allFiles = Get-ChildItem -Path $moduleRoot -Recurse -Filter "*.help.txt" | Where-Object FullName -NotLike "$moduleRoot\tests\*" + + foreach ($file in $allFiles) + { + $name = $file.FullName.Replace("$moduleRoot\", '') + + It "[$name] Should have UTF8 encoding" { + Get-FileEncoding -Path $file.FullName | Should -Be 'UTF8' + } + + It "[$name] Should have no trailing space" { + ($file | Select-String "\s$" | Where-Object { $_.Line.Trim().Length -gt 0 } | Measure-Object).Count | Should -Be 0 + } + } + } +} \ No newline at end of file diff --git a/AaronLocker/tests/general/Help.Exceptions.ps1 b/AaronLocker/tests/general/Help.Exceptions.ps1 new file mode 100644 index 0000000..f9c9bd7 --- /dev/null +++ b/AaronLocker/tests/general/Help.Exceptions.ps1 @@ -0,0 +1,26 @@ +# List of functions that should be ignored +$global:FunctionHelpTestExceptions = @( + +) + +<# + List of arrayed enumerations. These need to be treated differently. Add full name. + Example: + + "Sqlcollaborative.Dbatools.Connection.ManagementConnectionType[]" +#> +$global:HelpTestEnumeratedArrays = @( + +) + +<# + Some types on parameters just fail their validation no matter what. + For those it becomes possible to skip them, by adding them to this hashtable. + Add by following this convention: = @() + Example: + + "Get-DbaCmObject" = @("DoNotUse") +#> +$global:HelpTestSkipParameterType = @{ + +} diff --git a/AaronLocker/tests/general/Help.Tests.ps1 b/AaronLocker/tests/general/Help.Tests.ps1 new file mode 100644 index 0000000..c00bcf5 --- /dev/null +++ b/AaronLocker/tests/general/Help.Tests.ps1 @@ -0,0 +1,200 @@ +<# + .NOTES + The original test this is based upon was written by June Blender. + After several rounds of modifications it stands now as it is, but the honor remains hers. + + Thank you June, for all you have done! + + .DESCRIPTION + This test evaluates the help for all commands in a module. + + .PARAMETER SkipTest + Disables this test. + + .PARAMETER CommandPath + List of paths under which the script files are stored. + This test assumes that all functions have their own file that is named after themselves. + These paths are used to search for commands that should exist and be tested. + Will search recursively and accepts wildcards, make sure only functions are found + + .PARAMETER ModuleName + Name of the module to be tested. + The module must already be imported + + .PARAMETER ExceptionsFile + File in which exceptions and adjustments are configured. + In it there should be two arrays and a hashtable defined: + $global:FunctionHelpTestExceptions + $global:HelpTestEnumeratedArrays + $global:HelpTestSkipParameterType + These can be used to tweak the tests slightly in cases of need. + See the example file for explanations on each of these usage and effect. +#> +[CmdletBinding()] +Param ( + [switch] + $SkipTest, + + [string[]] + $CommandPath = @("$PSScriptRoot\..\..\functions", "$PSScriptRoot\..\..\internal\functions"), + + [string] + $ModuleName = "AaronLocker", + + [string] + $ExceptionsFile = "$PSScriptRoot\Help.Exceptions.ps1" +) +if ($SkipTest) { return } +. $ExceptionsFile + +$includedNames = (Get-ChildItem $CommandPath -Recurse -File | Where-Object Name -like "*.ps1").BaseName +$commands = Get-Command -Module (Get-Module $ModuleName) -CommandType Cmdlet, Function, Workflow | Where-Object Name -in $includedNames + +## When testing help, remember that help is cached at the beginning of each session. +## To test, restart session. + + +foreach ($command in $commands) { + $commandName = $command.Name + + # Skip all functions that are on the exclusions list + if ($global:FunctionHelpTestExceptions -contains $commandName) { continue } + + # The module-qualified command fails on Microsoft.PowerShell.Archive cmdlets + $Help = Get-Help $commandName -ErrorAction SilentlyContinue + $testhelperrors = 0 + $testhelpall = 0 + Describe "Test help for $commandName" { + + $testhelpall += 1 + if ($Help.Synopsis -like '*`[``]*') { + # If help is not found, synopsis in auto-generated help is the syntax diagram + It "should not be auto-generated" { + $Help.Synopsis | Should -Not -BeLike '*`[``]*' + } + $testhelperrors += 1 + } + + $testhelpall += 1 + if ([String]::IsNullOrEmpty($Help.Description.Text)) { + # Should be a description for every function + It "gets description for $commandName" { + $Help.Description | Should -Not -BeNullOrEmpty + } + $testhelperrors += 1 + } + + $testhelpall += 1 + if ([String]::IsNullOrEmpty(($Help.Examples.Example | Select-Object -First 1).Code)) { + # Should be at least one example + It "gets example code from $commandName" { + ($Help.Examples.Example | Select-Object -First 1).Code | Should -Not -BeNullOrEmpty + } + $testhelperrors += 1 + } + + $testhelpall += 1 + if ([String]::IsNullOrEmpty(($Help.Examples.Example.Remarks | Select-Object -First 1).Text)) { + # Should be at least one example description + It "gets example help from $commandName" { + ($Help.Examples.Example.Remarks | Select-Object -First 1).Text | Should -Not -BeNullOrEmpty + } + $testhelperrors += 1 + } + + if ($testhelperrors -eq 0) { + It "Ran silently $testhelpall tests" { + $testhelperrors | Should -be 0 + } + } + + $testparamsall = 0 + $testparamserrors = 0 + Context "Test parameter help for $commandName" { + + $Common = 'Debug', 'ErrorAction', 'ErrorVariable', 'InformationAction', 'InformationVariable', 'OutBuffer', 'OutVariable', + 'PipelineVariable', 'Verbose', 'WarningAction', 'WarningVariable' + + $parameters = $command.ParameterSets.Parameters | Sort-Object -Property Name -Unique | Where-Object Name -notin $common + $parameterNames = $parameters.Name + $HelpParameterNames = $Help.Parameters.Parameter.Name | Sort-Object -Unique + foreach ($parameter in $parameters) { + $parameterName = $parameter.Name + $parameterHelp = $Help.parameters.parameter | Where-Object Name -EQ $parameterName + + $testparamsall += 1 + if ([String]::IsNullOrEmpty($parameterHelp.Description.Text)) { + # Should be a description for every parameter + It "gets help for parameter: $parameterName : in $commandName" { + $parameterHelp.Description.Text | Should -Not -BeNullOrEmpty + } + $testparamserrors += 1 + } + + $testparamsall += 1 + $codeMandatory = $parameter.IsMandatory.toString() + if ($parameterHelp.Required -ne $codeMandatory) { + # Required value in Help should match IsMandatory property of parameter + It "help for $parameterName parameter in $commandName has correct Mandatory value" { + $parameterHelp.Required | Should -Be $codeMandatory + } + $testparamserrors += 1 + } + + if ($HelpTestSkipParameterType[$commandName] -contains $parameterName) { continue } + + $codeType = $parameter.ParameterType.Name + + $testparamsall += 1 + if ($parameter.ParameterType.IsEnum) { + # Enumerations often have issues with the typename not being reliably available + $names = $parameter.ParameterType::GetNames($parameter.ParameterType) + if ($parameterHelp.parameterValueGroup.parameterValue -ne $names) { + # Parameter type in Help should match code + It "help for $commandName has correct parameter type for $parameterName" { + $parameterHelp.parameterValueGroup.parameterValue | Should -be $names + } + $testparamserrors += 1 + } + } + elseif ($parameter.ParameterType.FullName -in $HelpTestEnumeratedArrays) { + # Enumerations often have issues with the typename not being reliably available + $names = [Enum]::GetNames($parameter.ParameterType.DeclaredMembers[0].ReturnType) + if ($parameterHelp.parameterValueGroup.parameterValue -ne $names) { + # Parameter type in Help should match code + It "help for $commandName has correct parameter type for $parameterName" { + $parameterHelp.parameterValueGroup.parameterValue | Should -be $names + } + $testparamserrors += 1 + } + } + else { + # To avoid calling Trim method on a null object. + $helpType = if ($parameterHelp.parameterValue) { $parameterHelp.parameterValue.Trim() } + if ($helpType -ne $codeType) { + # Parameter type in Help should match code + It "help for $commandName has correct parameter type for $parameterName" { + $helpType | Should -be $codeType + } + $testparamserrors += 1 + } + } + } + foreach ($helpParm in $HelpParameterNames) { + $testparamsall += 1 + if ($helpParm -notin $parameterNames) { + # Shouldn't find extra parameters in help. + It "finds help parameter in code: $helpParm" { + $helpParm -in $parameterNames | Should -Be $true + } + $testparamserrors += 1 + } + } + if ($testparamserrors -eq 0) { + It "Ran silently $testparamsall tests" { + $testparamserrors | Should -be 0 + } + } + } + } +} \ No newline at end of file diff --git a/AaronLocker/tests/general/Manifest.Tests.ps1 b/AaronLocker/tests/general/Manifest.Tests.ps1 new file mode 100644 index 0000000..9c47132 --- /dev/null +++ b/AaronLocker/tests/general/Manifest.Tests.ps1 @@ -0,0 +1,53 @@ +Describe "Validating the module manifest" { + $moduleRoot = (Resolve-Path "$PSScriptRoot\..\..").Path + $manifest = ((Get-Content "$moduleRoot\AaronLocker.psd1") -join "`n") | Invoke-Expression + [version]$moduleVersion = Get-Item "$moduleRoot\AaronLocker.psm1" | Select-String -Pattern '\$script:ModuleVersion = "(.*?)"' | ForEach-Object { $_.Matches[0].Groups[1].Value } + Context "Basic resources validation" { + $files = Get-ChildItem "$moduleRoot\functions" -Recurse -File -Filter "*.ps1" + It "Exports all functions in the public folder" { + + $functions = (Compare-Object -ReferenceObject $files.BaseName -DifferenceObject $manifest.FunctionsToExport | Where-Object SideIndicator -Like '<=').InputObject + $functions | Should -BeNullOrEmpty + } + It "Exports no function that isn't also present in the public folder" { + $functions = (Compare-Object -ReferenceObject $files.BaseName -DifferenceObject $manifest.FunctionsToExport | Where-Object SideIndicator -Like '=>').InputObject + $functions | Should -BeNullOrEmpty + } + + It "Exports none of its internal functions" { + $files = Get-ChildItem "$moduleRoot\internal\functions" -Recurse -File -Filter "*.ps1" + $files | Where-Object BaseName -In $manifest.FunctionsToExport | Should -BeNullOrEmpty + } + + It "Has the same version as the psm1 file" { + ([version]$manifest.ModuleVersion) | Should -Be $moduleVersion + } + } + + Context "Individual file validation" { + It "The root module file exists" { + Test-Path "$moduleRoot\$($manifest.RootModule)" | Should -Be $true + } + + foreach ($format in $manifest.FormatsToProcess) + { + It "The file $format should exist" { + Test-Path "$moduleRoot\$format" | Should -Be $true + } + } + + foreach ($type in $manifest.TypesToProcess) + { + It "The file $type should exist" { + Test-Path "$moduleRoot\$type" | Should -Be $true + } + } + + foreach ($assembly in $manifest.RequiredAssemblies) + { + It "The file $assembly should exist" { + Test-Path "$moduleRoot\$assembly" | Should -Be $true + } + } + } +} \ No newline at end of file diff --git a/AaronLocker/tests/general/PSScriptAnalyzer.Tests.ps1 b/AaronLocker/tests/general/PSScriptAnalyzer.Tests.ps1 new file mode 100644 index 0000000..b9aba06 --- /dev/null +++ b/AaronLocker/tests/general/PSScriptAnalyzer.Tests.ps1 @@ -0,0 +1,42 @@ +[CmdletBinding()] +Param ( + [switch] + $SkipTest, + + [string[]] + $CommandPath = @("$PSScriptRoot\..\..\functions", "$PSScriptRoot\..\..\internal\functions") +) + +if ($SkipTest) { return } + +$list = New-Object System.Collections.ArrayList + +Describe 'Invoking PSScriptAnalyzer against commandbase' { + $commandFiles = Get-ChildItem -Path $CommandPath -Recurse -Filter "*.ps1" + $scriptAnalyzerRules = Get-ScriptAnalyzerRule + + foreach ($file in $commandFiles) + { + Context "Analyzing $($file.BaseName)" { + $analysis = Invoke-ScriptAnalyzer -Path $file.FullName -ExcludeRule PSAvoidTrailingWhitespace, PSShouldProcess + + forEach ($rule in $scriptAnalyzerRules) + { + It "Should pass $rule" { + If ($analysis.RuleName -contains $rule) + { + $analysis | Where-Object RuleName -EQ $rule -outvariable failures | ForEach-Object { $list.Add($_) } + + 1 | Should Be 0 + } + else + { + 0 | Should Be 0 + } + } + } + } + } +} + +$list | Out-Default \ No newline at end of file diff --git a/AaronLocker/tests/pester.ps1 b/AaronLocker/tests/pester.ps1 new file mode 100644 index 0000000..466ecc4 --- /dev/null +++ b/AaronLocker/tests/pester.ps1 @@ -0,0 +1,91 @@ +param ( + $TestGeneral = $true, + + $TestFunctions = $true, + + [ValidateSet('None', 'Default', 'Passed', 'Failed', 'Pending', 'Skipped', 'Inconclusive', 'Describe', 'Context', 'Summary', 'Header', 'Fails', 'All')] + $Show = "None", + + $Include = "*", + + $Exclude = "" +) + +Write-PSFMessage -Level Important -Message "Starting Tests" + +Write-PSFMessage -Level Important -Message "Importing Module" + +Remove-Module AaronLocker -ErrorAction Ignore +Import-Module "$PSScriptRoot\..\AaronLocker.psd1" +Import-Module "$PSScriptRoot\..\AaronLocker.psm1" -Force + +Write-PSFMessage -Level Important -Message "Creating test result folder" +$null = New-Item -Path "$PSScriptRoot\..\.." -Name TestResults -ItemType Directory -Force + +$totalFailed = 0 +$totalRun = 0 + +$testresults = @() + +#region Run General Tests +Write-PSFMessage -Level Important -Message "Modules imported, proceeding with general tests" +foreach ($file in (Get-ChildItem "$PSScriptRoot\general" -Filter "*.Tests.ps1")) +{ + Write-PSFMessage -Level Significant -Message " Executing $($file.Name)" + $TestOuputFile = Join-Path "$PSScriptRoot\..\..\TestResults" "TEST-$($file.BaseName).xml" + $results = Invoke-Pester -Script $file.FullName -Show $Show -PassThru -OutputFile $TestOuputFile -OutputFormat NUnitXml + foreach ($result in $results) + { + $totalRun += $result.TotalCount + $totalFailed += $result.FailedCount + $result.TestResult | Where-Object { -not $_.Passed } | ForEach-Object { + $name = $_.Name + $testresults += [pscustomobject]@{ + Describe = $_.Describe + Context = $_.Context + Name = "It $name" + Result = $_.Result + Message = $_.FailureMessage + } + } + } +} +#endregion Run General Tests + +#region Test Commands +Write-PSFMessage -Level Important -Message "Proceeding with individual tests" +foreach ($file in (Get-ChildItem "$PSScriptRoot\functions" -Recurse -File -Filter "*Tests.ps1")) +{ + if ($file.Name -notlike $Include) { continue } + if ($file.Name -like $Exclude) { continue } + + Write-PSFMessage -Level Significant -Message " Executing $($file.Name)" + $TestOuputFile = Join-Path "$PSScriptRoot\..\..\TestResults" "TEST-$($file.BaseName).xml" + $results = Invoke-Pester -Script $file.FullName -Show $Show -PassThru -OutputFile $TestOuputFile -OutputFormat NUnitXml + foreach ($result in $results) + { + $totalRun += $result.TotalCount + $totalFailed += $result.FailedCount + $result.TestResult | Where-Object { -not $_.Passed } | ForEach-Object { + $name = $_.Name + $testresults += [pscustomobject]@{ + Describe = $_.Describe + Context = $_.Context + Name = "It $name" + Result = $_.Result + Message = $_.FailureMessage + } + } + } +} +#endregion Test Commands + +$testresults | Sort-Object Describe, Context, Name, Result, Message | Format-List + +if ($totalFailed -eq 0) { Write-PSFMessage -Level Critical -Message "All $totalRun tests executed without a single failure!" } +else { Write-PSFMessage -Level Critical -Message "$totalFailed tests out of $totalRun tests failed!" } + +if ($totalFailed -gt 0) +{ + throw "$totalFailed / $totalRun tests failed!" +} \ No newline at end of file diff --git a/AaronLocker/tests/readme.md b/AaronLocker/tests/readme.md new file mode 100644 index 0000000..43bb2fa --- /dev/null +++ b/AaronLocker/tests/readme.md @@ -0,0 +1,31 @@ +# Description + +This is the folder, where all the tests go. + +Those are subdivided in two categories: + + - General + - Function + +## General Tests + +General tests are function generic and test for general policies. + +These test scan answer questions such as: + + - Is my module following my style guides? + - Does any of my scripts have a syntax error? + - Do my scripts use commands I do not want them to use? + - Do my commands follow best practices? + - Do my commands have proper help? + +Basically, these allow a general module health check. + +These tests are already provided as part of the template. + +## Function Tests + +A healthy module should provide unit and integration tests for the commands & components it ships. +Only then can be guaranteed, that they will actually perform as promised. + +However, as each such test must be specific to the function it tests, there cannot be much in the way of templates. \ No newline at end of file diff --git a/AaronLocker/xml/AaronLocker.Format.ps1xml b/AaronLocker/xml/AaronLocker.Format.ps1xml new file mode 100644 index 0000000..d9c7f7e --- /dev/null +++ b/AaronLocker/xml/AaronLocker.Format.ps1xml @@ -0,0 +1,171 @@ + + + + + + AaronLocker.Detection.File + + AaronLocker.Detection.File + + + + + + + + + + + + + + + + + IsSafeDir + + + FileType + + + FileName + + + +-not [string]::IsNullOrEmpty($_.PublisherName) + + + + ParentDirectory + + + + + + + + + + AaronLocker.Rules.Hash + + AaronLocker.Rules.Hash + + + + + + + + + + + + + + + + RuleCollection + + + FileName + + + RuleName + + + RuleDesc + + + + + + + + + + AaronLocker.Rules.Path + + AaronLocker.Rules.Path + + + + + + + Right + + + + + + + + + NoRecurse + + + Label + + + Paths + + + + + + + + + + AaronLocker.Rules.Publisher + + AaronLocker.Rules.Publisher + + + + + + + Right + + + + + + + + + + + + + + + +$true -eq $_.Exemplar + + + + RuleCollection + + + Label + + + + +if ($_.PublisherName) { $_.PublisherName } +elseif ($_.Exemplar -and (Test-Path $_.Exemplar)) { (Get-AuthenticodeSignature -FilePath $_.Exemplar).Subject } + + + + + ProductName + + + + + + + + \ No newline at end of file diff --git a/AaronLocker/xml/AaronLocker.Types.ps1xml b/AaronLocker/xml/AaronLocker.Types.ps1xml new file mode 100644 index 0000000..2c46a9d --- /dev/null +++ b/AaronLocker/xml/AaronLocker.Types.ps1xml @@ -0,0 +1,212 @@ + + + + + Deserialized.AaronLocker.HashRule + + + PSStandardMembers + + + + TargetTypeForDeserialization + + + AaronLocker.HashRule + + + + + + + + AaronLocker.HashRule + + + SerializationData + + AaronLocker.SerializationTypeConverter + GetSerializationData + + + + + AaronLocker.SerializationTypeConverter + + + + + + Deserialized.AaronLocker.PathRule + + + PSStandardMembers + + + + TargetTypeForDeserialization + + + AaronLocker.PathRule + + + + + + + + AaronLocker.PathRule + + + SerializationData + + AaronLocker.SerializationTypeConverter + GetSerializationData + + + + + AaronLocker.SerializationTypeConverter + + + + + + Deserialized.AaronLocker.Policy + + + PSStandardMembers + + + + TargetTypeForDeserialization + + + AaronLocker.Policy + + + + + + + + AaronLocker.Policy + + + SerializationData + + AaronLocker.SerializationTypeConverter + GetSerializationData + + + + + AaronLocker.SerializationTypeConverter + + + + + + Deserialized.AaronLocker.PublisherRule + + + PSStandardMembers + + + + TargetTypeForDeserialization + + + AaronLocker.PublisherRule + + + + + + + + AaronLocker.PublisherRule + + + SerializationData + + AaronLocker.SerializationTypeConverter + GetSerializationData + + + + + AaronLocker.SerializationTypeConverter + + + + + + Deserialized.AaronLocker.RuleFailure + + + PSStandardMembers + + + + TargetTypeForDeserialization + + + AaronLocker.RuleFailure + + + + + + + + AaronLocker.RuleFailure + + + SerializationData + + AaronLocker.SerializationTypeConverter + GetSerializationData + + + + + AaronLocker.SerializationTypeConverter + + + + + + Deserialized.AaronLocker.SourcePathRule + + + PSStandardMembers + + + + TargetTypeForDeserialization + + + AaronLocker.SourcePathRule + + + + + + + + AaronLocker.SourcePathRule + + + SerializationData + + AaronLocker.SerializationTypeConverter + GetSerializationData + + + + + AaronLocker.SerializationTypeConverter + + + \ No newline at end of file diff --git a/AaronLocker/xml/readme.md b/AaronLocker/xml/readme.md new file mode 100644 index 0000000..9c2643f --- /dev/null +++ b/AaronLocker/xml/readme.md @@ -0,0 +1,43 @@ +# XML + +This is the folder where project XML files go, notably: + + - Format XML + - Type Extension XML + +External help files should _not_ be placed in this folder! + +## Notes on Files and Naming + +There should be only one format file and one type extension file per project, as importing them has a notable impact on import times. + + - The Format XML should be named `AaronLocker.Format.ps1xml` + - The Type Extension XML should be named `AaronLocker.Types.ps1xml` + +## Tools + +### New-PSMDFormatTableDefinition + +This function will take an input object and generate format xml for an auto-sized table. + +It provides a simple way to get started with formats. + +### Get-PSFTypeSerializationData + +``` +C# Warning! +This section is only interest if you're using C# together with PowerShell. +``` + +This function generates type extension XML that allows PowerShell to convert types written in C# to be written to file and restored from it without being 'Deserialized'. Also works for jobs or remoting, if both sides have the `PSFramework` module and type extension loaded. + +In order for a class to be eligible for this, it needs to conform to the following rules: + + - Have the `[Serializable]` attribute + - Be public + - Have an empty constructor + - Allow all public properties/fields to be set (even if setting it doesn't do anything) without throwing an exception. + +``` +non-public properties and fields will be lost in this process! +``` \ No newline at end of file diff --git a/AaronLocker/Compare-Policies.ps1 b/AaronLockerScriptBased/AaronLocker/Compare-Policies.ps1 similarity index 97% rename from AaronLocker/Compare-Policies.ps1 rename to AaronLockerScriptBased/AaronLocker/Compare-Policies.ps1 index 3ebfc3c..40f6c15 100644 --- a/AaronLocker/Compare-Policies.ps1 +++ b/AaronLockerScriptBased/AaronLocker/Compare-Policies.ps1 @@ -1,363 +1,363 @@ -<# -.SYNOPSIS -Compares two AppLocker policies. - -TODO: Add an option to get policies from AD GPO. - -.DESCRIPTION -Reads two AppLocker policy XML files, canonicalizes and compares the rule information and reports results as tab-delimited CSV, or optionally to an Excel workbook formatted for sorting and filtering. -Output columns are Compare, Rule, Reference, and Comparison. -The "Compare" column is one of the following values: - "==" if values are the same in both rule sets - "<->" if values are present in both rule sets but different - "<--" if the rule exists only in the reference rule set - "-->" if the rule exists only in the comparison rule set -The "Rule" column is either the name of a rule collection (Exe, Dll, Script, etc.) or information about a specific rule. -The "Reference" column shows data from the ReferencePolicyXML parameter. -The "Comparison" column shows data from the ComparisonPolicyXML parameter. - -Where the "Rule" column contains just the name of a rule collection, the Reference and Comparison columns indicate whether rules for that collection are "AuditOnly" or "Enabled" (enforced). - -Otherwise, the "Rule" column shows information about a specific rule, including: the file type (e.g., Dll, Exe); rule type (Publisher, Path, Hash); Allow or Deny; user/group SID; and rule-type-specific information. -For Publisher rules, the rule-specific information catenates the publisher, product, and binary name. (Product or binary name might be empty.) -For Path rules, the path is the rule-specific information. -For Hash rules, the source file name is the rule-specific information. - -The Reference and Comparison columns show more detailed rule-type-specific information about the rule from the Reference and Comparison rule sets: -For Publisher rules: the low and high version numbers that the rule applies to. If the Publisher rule includes exceptions, the raw XML is appended. -For Path rules: exceptions to the rule, sorted. -For Hash rules: the hash algorithm and value. - -When a rule set contains overlapping rules (e.g., two separate hashes allowed for the same file name), the detailed information is appended into the Reference or Comparison column. - -Note that when the -Excel switch is not used, line breaks within the CSV text fields are represented as "^|^". - - -.PARAMETER ReferencePolicyXML -Path to AppLocker policy XML file. -Use "local" to inspect local policy. -Use "effective" to inspect effective policy. - -.PARAMETER ComparisonPolicyXML -Path to AppLocker policy XML file. -Use "local" to inspect local policy. -Use "effective" to inspect effective policy. - -.PARAMETER DifferencesOnly -If this optional switch is specified, entries that are in both sets and are identical are not reported. - -.PARAMETER Excel -If this optional switch is specified, outputs to a formatted Excel rather than tab-delimited CSV text to the pipeline. Note that when the -Excel switch is not used, line breaks within the CSV text fields are represented as "^|^". - -.EXAMPLE -.\Compare-Policies.ps1 local effective -DifferencesOnly -Compare local policy against effective policy and report only the differences. -#> - - -param( - # path to XML file containing AppLocker policy - [parameter(Mandatory=$true)] - [String] - $ReferencePolicyXML, - - # path to XML file containing AppLocker policy - [parameter(Mandatory=$true)] - [String] - $ComparisonPolicyXML, - - # Don't report items that are the same in both sets. - [switch] - $DifferencesOnly, - - # Output to Excel - [switch] - $Excel -) - -$rootDir = [System.IO.Path]::GetDirectoryName($MyInvocation.MyCommand.Path) -# Get configuration settings and global functions from .\Support\Config.ps1) -# Dot-source the config file. -. $rootDir\Support\Config.ps1 - -$OutputEncodingPrevious = $OutputEncoding -$OutputEncoding = [System.Text.ASCIIEncoding]::Unicode - -$keySep = " | " -$linebreakSeq = "^|^" -$tab = "`t" - -$refname = $compname = [string]::Empty - -# Get reference policy from local policy, effective policy, or a named file -if ($ReferencePolicyXML.ToLower() -eq "local") -{ - $ReferencePolicy = [xml](Get-AppLockerPolicy -Local -Xml) - $refname = "Local Policy" -} -elseif ($ReferencePolicyXML.ToLower() -eq "effective") -{ - $ReferencePolicy = [xml](Get-AppLockerPolicy -Effective -Xml) - $refname = "Effective Policy" -} -else -{ - $ReferencePolicy = [xml](Get-Content $ReferencePolicyXML) - $refname = [System.IO.Path]::GetFileNameWithoutExtension($ReferencePolicyXML) -} - -# Get comparison policy from local policy, effective policy, or a named file -if ($ComparisonPolicyXML.ToLower() -eq "local") -{ - $ComparisonPolicy = [xml](Get-AppLockerPolicy -Local -Xml) - $compname = "Local Policy" -} -elseif ($ComparisonPolicyXML.ToLower() -eq "effective") -{ - $ComparisonPolicy = [xml](Get-AppLockerPolicy -Effective -Xml) - $compname = "Effective Policy" -} -else -{ - $ComparisonPolicy = [xml](Get-Content $ComparisonPolicyXML) - $compname = [System.IO.Path]::GetFileNameWithoutExtension($ComparisonPolicyXML) -} - - -# Create CSV headers -$csv = @() -$csv += "Compare" + $tab + "Rule" + $tab + "Reference ($refname)" + $tab + "Comparison ($compname)" + $tab + "Reference info" + $tab + "Comparison info" - - -function GetNodeKeyAndValue( $fType, $oNode, [ref] $oKey, [ref] $oValue ) -{ - $userOrGroup = $oNode.UserOrGroupSid - $action = $oNode.Action - $nameAndDescr = ($oNode.Name + $linebreakSeq + $oNode.Description).Replace("`r`n", $linebreakSeq).Replace("`n", $linebreakSeq) - $oValue.Value = @{ ruleDetail = ""; ruleDoco = $nameAndDescr; } - switch ( $oNode.LocalName ) - { - - "FilePublisherRule" - { - $ruletype = "Publisher" - $condition = $oNode.Conditions.FilePublisherCondition - $ruleInfo = $condition.PublisherName + $keySep + $condition.ProductName + $keySep + $condition.BinaryName - $oKey.Value = $fType + $keySep + $ruletype + $keySep + $action + $keySep + $userOrGroup + $keySep + $ruleInfo - $oValue.Value.ruleDetail = "Ver " + $condition.BinaryVersionRange.LowSection + " to " + $condition.BinaryVersionRange.HighSection - if ($oNode.Exceptions.InnerXml.Length -gt 0 ) - { - $oValue.Value.ruleDetail += ("; Exceptions: " + $oNode.Exceptions.InnerXml) - } - } - - "FilePathRule" - { - $ruletype = "Path" - $ruleInfo = $oNode.Conditions.FilePathCondition.Path - # Exceptions in canonical order. - #TODO: also report any hash rules if found within path rule exceptions - $exceptions = (($oNode.Exceptions.FilePathCondition.Path + $oNode.Exceptions.FilePublisherCondition.BinaryName) | Sort-Object) -join $linebreakSeq - $oKey.Value = $fType + $keySep + $ruletype + $keySep + $action + $keySep + $userOrGroup + $keySep + $ruleInfo - $oValue.Value.ruleDetail = $exceptions - } - - "FileHashRule" - { - $ruletype = "Hash" - $condition = $oNode.Conditions.FileHashCondition.FileHash - $ruleInfo = $condition.SourceFileName # + "; length = " + $condition.SourceFileLength - # $exceptions = "" # hash rules don't have exceptions - $oKey.Value = $fType + $keySep + $ruletype + $keySep + $action + $keySep + $userOrGroup + $keySep + $ruleInfo - $oValue.Value.ruleDetail = $condition.Type + " " + $condition.Data - } - - default { Write-Warning ("Unexpected/invalid rule type: " + $_.LocalName) } - - } -} - - -<# - $collections is a hashtable containing information about the rule collections (Exe, Dll, etc.), and whether each is "Audit" or "Enforce". - Key = filetype - Value = Two-element array, where the first element is the reference rule set's enforcement type and the second element is the comparison rule set's enforcement type. - - $rules is a hashtable containing information about AppLocker rules. - Key = information about rule, combining file type (Exe, Dll, Script, etc.), rule type (Publisher, Path, or Hash), and rule-specific information (see GetNodeKeyAndValue). - Value = Two-element array, where first element is rule information from the reference rule set, and the second element is from the comparison rule set. -#> -$collections = @{} -$rules = @{} - -<# -For both collections, key is a string, value is a two-element array, where element 0 is the reference data and element 1 is the comparison data -When adding a new item, create a new two-element array to set as the value, with either 0 or 1 containing data. -#> - - -$ReferencePolicy.AppLockerPolicy.RuleCollection | foreach { - $filetype = $_.Type - $enforce = $_.EnforcementMode - - <# - $collections is being newly populated here; value is two-element array in which the reference policy provides data for the first element, and - the second element is initially empty - #> - $collVal = @{ ruleDetail = $enforce; }, $null - $collections.Add($filetype, $collVal) - - if ($_.ChildNodes.Count -eq 0) - { - } - else - { - $_.ChildNodes | foreach { - - $childNode = $_ - $oKey = [ref]"" - $oValue = [ref]"" - GetNodeKeyAndValue $filetype $childNode $oKey $oValue - - # If the reference set already contains this key, see whether the value is a duplicate or a differing value - # If duplicate, ignore it. If it's different, append it to the existing value - if ($rules.ContainsKey($oKey.Value)) - { - $existingVal = $rules[$oKey.Value][0] - if ($existingVal.ruleDetail -ne $oValue.Value.ruleDetail) - { - $rules[$oKey.Value][0].ruleDetail += ($linebreakSeq + $oValue.Value.ruleDetail) - } - if ($existingVal.ruleDoco -ne $oValue.Value.ruleDoco) - { - $rules[$oKey.Value][0].ruleDoco += ($linebreakSeq + $oValue.Value.ruleDoco) - } - } - else - { - $ruleVal = $oValue.Value, $null - $rules.Add($oKey.Value, $ruleVal) - } - } - } -} - -$ComparisonPolicy.AppLockerPolicy.RuleCollection | foreach { - $filetype = $_.Type - $enforce = $_.EnforcementMode - - # If $collections already has this file type, add to the existing value array; otherwise create a new entry with a new two-element array, populating the second element of that array - if ($collections.ContainsKey($filetype)) - { - $collections[$filetype][1] = @{ ruleDetail = $enforce; } - } - else - { - $collVal = $null, @{ ruleDetail = $enforce } - $collections.Add($filetype, $collVal) - } - - # Then do child nodes... - if ($_.ChildNodes.Count -eq 0) - { - } - else - { - $_.ChildNodes | foreach { - - $childNode = $_ - $oKey = [ref]"" - $oValue = [ref]"" - GetNodeKeyAndValue $filetype $childNode $oKey $oValue - - if ($rules.ContainsKey($oKey.Value)) - { - # If there's already data in the second element, see whether it's a duplicate. If it's a duplicate, ignore; if it's a differing value, append it to the existing value - $existingVal = $rules[$oKey.Value][1] - if ($existingVal -eq $null) - { - $rules[$oKey.Value][1] = $oValue.Value - } - else - { - if ($existingVal.ruleDetail -ne $oValue.Value.ruleDetail) - { - $rules[$oKey.Value][1].ruleDetail += ($linebreakSeq + $oValue.Value.ruleDetail) - } - if ($existingVal.ruleDoco -ne $oValue.Value.ruleDoco) - { - $rules[$oKey.Value][1].ruleDoco += ($linebreakSeq + $oValue.Value.ruleDoco) - } - } - } - else - { - $ruleVal = $null, $oValue.Value - $rules.Add($oKey.Value, $ruleVal) - } - } - } - -} - - -function ShowKeyValCompare($key, $val) -{ - # Assume that if the key is present, then one or both of val0 and val1 is present - if ($null -eq $val[0]) - { - "-->" + $tab + $key + $tab + "" + $tab + $val[1].ruleDetail + $tab + "" + $tab + $val[1].ruleDoco - } - elseif ($null -eq $val[1]) - { - "<--" + $tab + $key + $tab + $val[0].ruleDetail + $tab + "" + $tab + $val[0].ruleDoco + $tab - } - else - { # Canonicalize/sort before performing comparison so that the same items in a different order doesn't report as a difference - # TODO: re-sort ruleDoco so that its items still correspond to the sorted ruleDetail - not just a simple alpha sort though. - $val0RuleDetail = ($val[0].ruleDetail.Replace($linebreakSeq, "`n").Split("`n") | Sort-Object) -join $linebreakSeq - $val1RuleDetail = ($val[1].ruleDetail.Replace($linebreakSeq, "`n").Split("`n") | Sort-Object) -join $linebreakSeq - if ($val0RuleDetail -eq $val1RuleDetail) - { - if (!$DifferencesOnly) - { - "==" + $tab + $key + $tab + $val0RuleDetail + $tab + $val1RuleDetail + $tab + $val[0].ruleDoco + $tab + $val[1].ruleDoco - } - } - else - { - "<->" + $tab + $key + $tab + $val0RuleDetail + $tab + $val1RuleDetail + $tab + $val[0].ruleDoco + $tab + $val[1].ruleDoco - } - } -} - - -# Output everything in order - -$csv += ( - $collections.Keys | Sort-Object | foreach { - ShowKeyValCompare $_ $collections[$_] - } - ) -$csv += ( - $rules.Keys | Sort-Object | foreach { - ShowKeyValCompare $_ $rules[$_] - } - ) - -if ($Excel) -{ - if (CreateExcelApplication) - { - AddWorksheetFromCsvData -csv $csv -tabname "$refname vs $compname" -CrLfEncoded $linebreakSeq - ReleaseExcelApplication - } -} -else -{ - # Just output the CSV raw - $csv -} - -$OutputEncoding = $OutputEncodingPrevious - +<# +.SYNOPSIS +Compares two AppLocker policies. + +TODO: Add an option to get policies from AD GPO. + +.DESCRIPTION +Reads two AppLocker policy XML files, canonicalizes and compares the rule information and reports results as tab-delimited CSV, or optionally to an Excel workbook formatted for sorting and filtering. +Output columns are Compare, Rule, Reference, and Comparison. +The "Compare" column is one of the following values: + "==" if values are the same in both rule sets + "<->" if values are present in both rule sets but different + "<--" if the rule exists only in the reference rule set + "-->" if the rule exists only in the comparison rule set +The "Rule" column is either the name of a rule collection (Exe, Dll, Script, etc.) or information about a specific rule. +The "Reference" column shows data from the ReferencePolicyXML parameter. +The "Comparison" column shows data from the ComparisonPolicyXML parameter. + +Where the "Rule" column contains just the name of a rule collection, the Reference and Comparison columns indicate whether rules for that collection are "AuditOnly" or "Enabled" (enforced). + +Otherwise, the "Rule" column shows information about a specific rule, including: the file type (e.g., Dll, Exe); rule type (Publisher, Path, Hash); Allow or Deny; user/group SID; and rule-type-specific information. +For Publisher rules, the rule-specific information catenates the publisher, product, and binary name. (Product or binary name might be empty.) +For Path rules, the path is the rule-specific information. +For Hash rules, the source file name is the rule-specific information. + +The Reference and Comparison columns show more detailed rule-type-specific information about the rule from the Reference and Comparison rule sets: +For Publisher rules: the low and high version numbers that the rule applies to. If the Publisher rule includes exceptions, the raw XML is appended. +For Path rules: exceptions to the rule, sorted. +For Hash rules: the hash algorithm and value. + +When a rule set contains overlapping rules (e.g., two separate hashes allowed for the same file name), the detailed information is appended into the Reference or Comparison column. + +Note that when the -Excel switch is not used, line breaks within the CSV text fields are represented as "^|^". + + +.PARAMETER ReferencePolicyXML +Path to AppLocker policy XML file. +Use "local" to inspect local policy. +Use "effective" to inspect effective policy. + +.PARAMETER ComparisonPolicyXML +Path to AppLocker policy XML file. +Use "local" to inspect local policy. +Use "effective" to inspect effective policy. + +.PARAMETER DifferencesOnly +If this optional switch is specified, entries that are in both sets and are identical are not reported. + +.PARAMETER Excel +If this optional switch is specified, outputs to a formatted Excel rather than tab-delimited CSV text to the pipeline. Note that when the -Excel switch is not used, line breaks within the CSV text fields are represented as "^|^". + +.EXAMPLE +.\Compare-Policies.ps1 local effective -DifferencesOnly +Compare local policy against effective policy and report only the differences. +#> + + +param( + # path to XML file containing AppLocker policy + [parameter(Mandatory=$true)] + [String] + $ReferencePolicyXML, + + # path to XML file containing AppLocker policy + [parameter(Mandatory=$true)] + [String] + $ComparisonPolicyXML, + + # Don't report items that are the same in both sets. + [switch] + $DifferencesOnly, + + # Output to Excel + [switch] + $Excel +) + +$rootDir = [System.IO.Path]::GetDirectoryName($MyInvocation.MyCommand.Path) +# Get configuration settings and global functions from .\Support\Config.ps1) +# Dot-source the config file. +. $rootDir\Support\Config.ps1 + +$OutputEncodingPrevious = $OutputEncoding +$OutputEncoding = [System.Text.ASCIIEncoding]::Unicode + +$keySep = " | " +$linebreakSeq = "^|^" +$tab = "`t" + +$refname = $compname = [string]::Empty + +# Get reference policy from local policy, effective policy, or a named file +if ($ReferencePolicyXML.ToLower() -eq "local") +{ + $ReferencePolicy = [xml](Get-AppLockerPolicy -Local -Xml) + $refname = "Local Policy" +} +elseif ($ReferencePolicyXML.ToLower() -eq "effective") +{ + $ReferencePolicy = [xml](Get-AppLockerPolicy -Effective -Xml) + $refname = "Effective Policy" +} +else +{ + $ReferencePolicy = [xml](Get-Content $ReferencePolicyXML) + $refname = [System.IO.Path]::GetFileNameWithoutExtension($ReferencePolicyXML) +} + +# Get comparison policy from local policy, effective policy, or a named file +if ($ComparisonPolicyXML.ToLower() -eq "local") +{ + $ComparisonPolicy = [xml](Get-AppLockerPolicy -Local -Xml) + $compname = "Local Policy" +} +elseif ($ComparisonPolicyXML.ToLower() -eq "effective") +{ + $ComparisonPolicy = [xml](Get-AppLockerPolicy -Effective -Xml) + $compname = "Effective Policy" +} +else +{ + $ComparisonPolicy = [xml](Get-Content $ComparisonPolicyXML) + $compname = [System.IO.Path]::GetFileNameWithoutExtension($ComparisonPolicyXML) +} + + +# Create CSV headers +$csv = @() +$csv += "Compare" + $tab + "Rule" + $tab + "Reference ($refname)" + $tab + "Comparison ($compname)" + $tab + "Reference info" + $tab + "Comparison info" + + +function GetNodeKeyAndValue( $fType, $oNode, [ref] $oKey, [ref] $oValue ) +{ + $userOrGroup = $oNode.UserOrGroupSid + $action = $oNode.Action + $nameAndDescr = ($oNode.Name + $linebreakSeq + $oNode.Description).Replace("`r`n", $linebreakSeq).Replace("`n", $linebreakSeq) + $oValue.Value = @{ ruleDetail = ""; ruleDoco = $nameAndDescr; } + switch ( $oNode.LocalName ) + { + + "FilePublisherRule" + { + $ruletype = "Publisher" + $condition = $oNode.Conditions.FilePublisherCondition + $ruleInfo = $condition.PublisherName + $keySep + $condition.ProductName + $keySep + $condition.BinaryName + $oKey.Value = $fType + $keySep + $ruletype + $keySep + $action + $keySep + $userOrGroup + $keySep + $ruleInfo + $oValue.Value.ruleDetail = "Ver " + $condition.BinaryVersionRange.LowSection + " to " + $condition.BinaryVersionRange.HighSection + if ($oNode.Exceptions.InnerXml.Length -gt 0 ) + { + $oValue.Value.ruleDetail += ("; Exceptions: " + $oNode.Exceptions.InnerXml) + } + } + + "FilePathRule" + { + $ruletype = "Path" + $ruleInfo = $oNode.Conditions.FilePathCondition.Path + # Exceptions in canonical order. + #TODO: also report any hash rules if found within path rule exceptions + $exceptions = (($oNode.Exceptions.FilePathCondition.Path + $oNode.Exceptions.FilePublisherCondition.BinaryName) | Sort-Object) -join $linebreakSeq + $oKey.Value = $fType + $keySep + $ruletype + $keySep + $action + $keySep + $userOrGroup + $keySep + $ruleInfo + $oValue.Value.ruleDetail = $exceptions + } + + "FileHashRule" + { + $ruletype = "Hash" + $condition = $oNode.Conditions.FileHashCondition.FileHash + $ruleInfo = $condition.SourceFileName # + "; length = " + $condition.SourceFileLength + # $exceptions = "" # hash rules don't have exceptions + $oKey.Value = $fType + $keySep + $ruletype + $keySep + $action + $keySep + $userOrGroup + $keySep + $ruleInfo + $oValue.Value.ruleDetail = $condition.Type + " " + $condition.Data + } + + default { Write-Warning ("Unexpected/invalid rule type: " + $_.LocalName) } + + } +} + + +<# + $collections is a hashtable containing information about the rule collections (Exe, Dll, etc.), and whether each is "Audit" or "Enforce". + Key = filetype + Value = Two-element array, where the first element is the reference rule set's enforcement type and the second element is the comparison rule set's enforcement type. + + $rules is a hashtable containing information about AppLocker rules. + Key = information about rule, combining file type (Exe, Dll, Script, etc.), rule type (Publisher, Path, or Hash), and rule-specific information (see GetNodeKeyAndValue). + Value = Two-element array, where first element is rule information from the reference rule set, and the second element is from the comparison rule set. +#> +$collections = @{} +$rules = @{} + +<# +For both collections, key is a string, value is a two-element array, where element 0 is the reference data and element 1 is the comparison data +When adding a new item, create a new two-element array to set as the value, with either 0 or 1 containing data. +#> + + +$ReferencePolicy.AppLockerPolicy.RuleCollection | foreach { + $filetype = $_.Type + $enforce = $_.EnforcementMode + + <# + $collections is being newly populated here; value is two-element array in which the reference policy provides data for the first element, and + the second element is initially empty + #> + $collVal = @{ ruleDetail = $enforce; }, $null + $collections.Add($filetype, $collVal) + + if ($_.ChildNodes.Count -eq 0) + { + } + else + { + $_.ChildNodes | foreach { + + $childNode = $_ + $oKey = [ref]"" + $oValue = [ref]"" + GetNodeKeyAndValue $filetype $childNode $oKey $oValue + + # If the reference set already contains this key, see whether the value is a duplicate or a differing value + # If duplicate, ignore it. If it's different, append it to the existing value + if ($rules.ContainsKey($oKey.Value)) + { + $existingVal = $rules[$oKey.Value][0] + if ($existingVal.ruleDetail -ne $oValue.Value.ruleDetail) + { + $rules[$oKey.Value][0].ruleDetail += ($linebreakSeq + $oValue.Value.ruleDetail) + } + if ($existingVal.ruleDoco -ne $oValue.Value.ruleDoco) + { + $rules[$oKey.Value][0].ruleDoco += ($linebreakSeq + $oValue.Value.ruleDoco) + } + } + else + { + $ruleVal = $oValue.Value, $null + $rules.Add($oKey.Value, $ruleVal) + } + } + } +} + +$ComparisonPolicy.AppLockerPolicy.RuleCollection | foreach { + $filetype = $_.Type + $enforce = $_.EnforcementMode + + # If $collections already has this file type, add to the existing value array; otherwise create a new entry with a new two-element array, populating the second element of that array + if ($collections.ContainsKey($filetype)) + { + $collections[$filetype][1] = @{ ruleDetail = $enforce; } + } + else + { + $collVal = $null, @{ ruleDetail = $enforce } + $collections.Add($filetype, $collVal) + } + + # Then do child nodes... + if ($_.ChildNodes.Count -eq 0) + { + } + else + { + $_.ChildNodes | foreach { + + $childNode = $_ + $oKey = [ref]"" + $oValue = [ref]"" + GetNodeKeyAndValue $filetype $childNode $oKey $oValue + + if ($rules.ContainsKey($oKey.Value)) + { + # If there's already data in the second element, see whether it's a duplicate. If it's a duplicate, ignore; if it's a differing value, append it to the existing value + $existingVal = $rules[$oKey.Value][1] + if ($existingVal -eq $null) + { + $rules[$oKey.Value][1] = $oValue.Value + } + else + { + if ($existingVal.ruleDetail -ne $oValue.Value.ruleDetail) + { + $rules[$oKey.Value][1].ruleDetail += ($linebreakSeq + $oValue.Value.ruleDetail) + } + if ($existingVal.ruleDoco -ne $oValue.Value.ruleDoco) + { + $rules[$oKey.Value][1].ruleDoco += ($linebreakSeq + $oValue.Value.ruleDoco) + } + } + } + else + { + $ruleVal = $null, $oValue.Value + $rules.Add($oKey.Value, $ruleVal) + } + } + } + +} + + +function ShowKeyValCompare($key, $val) +{ + # Assume that if the key is present, then one or both of val0 and val1 is present + if ($null -eq $val[0]) + { + "-->" + $tab + $key + $tab + "" + $tab + $val[1].ruleDetail + $tab + "" + $tab + $val[1].ruleDoco + } + elseif ($null -eq $val[1]) + { + "<--" + $tab + $key + $tab + $val[0].ruleDetail + $tab + "" + $tab + $val[0].ruleDoco + $tab + } + else + { # Canonicalize/sort before performing comparison so that the same items in a different order doesn't report as a difference + # TODO: re-sort ruleDoco so that its items still correspond to the sorted ruleDetail - not just a simple alpha sort though. + $val0RuleDetail = ($val[0].ruleDetail.Replace($linebreakSeq, "`n").Split("`n") | Sort-Object) -join $linebreakSeq + $val1RuleDetail = ($val[1].ruleDetail.Replace($linebreakSeq, "`n").Split("`n") | Sort-Object) -join $linebreakSeq + if ($val0RuleDetail -eq $val1RuleDetail) + { + if (!$DifferencesOnly) + { + "==" + $tab + $key + $tab + $val0RuleDetail + $tab + $val1RuleDetail + $tab + $val[0].ruleDoco + $tab + $val[1].ruleDoco + } + } + else + { + "<->" + $tab + $key + $tab + $val0RuleDetail + $tab + $val1RuleDetail + $tab + $val[0].ruleDoco + $tab + $val[1].ruleDoco + } + } +} + + +# Output everything in order + +$csv += ( + $collections.Keys | Sort-Object | foreach { + ShowKeyValCompare $_ $collections[$_] + } + ) +$csv += ( + $rules.Keys | Sort-Object | foreach { + ShowKeyValCompare $_ $rules[$_] + } + ) + +if ($Excel) +{ + if (CreateExcelApplication) + { + AddWorksheetFromCsvData -csv $csv -tabname "$refname vs $compname" -CrLfEncoded $linebreakSeq + ReleaseExcelApplication + } +} +else +{ + # Just output the CSV raw + $csv +} + +$OutputEncoding = $OutputEncodingPrevious + diff --git a/AaronLocker/Create-Policies.ps1 b/AaronLockerScriptBased/AaronLocker/Create-Policies.ps1 similarity index 97% rename from AaronLocker/Create-Policies.ps1 rename to AaronLockerScriptBased/AaronLocker/Create-Policies.ps1 index dcdb0d0..a44db4d 100644 --- a/AaronLocker/Create-Policies.ps1 +++ b/AaronLockerScriptBased/AaronLocker/Create-Policies.ps1 @@ -1,864 +1,864 @@ -<# -.SYNOPSIS -Builds comprehensive and robust AppLocker "audit" and "enforce" rules to mitigate against users running unauthorized software, customizable through simple text files. Writes results to the Outputs subdirectory. - -TODO: Find and remove redundant rules. Report stripped rules to a separate log file. - -.DESCRIPTION -Create-Policies.ps1 generates comprehensive "audit" and "enforce" AppLocker rules to restrict non-admin code execution to "authorized" software, -in a way to minimize the need to update the rules. -Broadly speaking, "authorized" means that an administrator put it on the computer, OR created a rule specifically for that item. -* Supported operating systems include Windows 7 and newer, and Windows Server 2008 R2 and newer. -* Rules cover EXE, DLL, Script, and MSI; on Windows 8.1 and newer, rules also cover Packaged apps. -* Allows execution from the Windows and ProgramFiles directories, EXCEPT: - * Identifies user-writable subdirectories and disallows execution from those directories; - * Disallows execution of programs that run user-supplied code (e.g., mshta.exe); - * Disallows execution of programs that non-admins rarely need but that malware/ransomware authors are known to use (e.g., cipher.exe); -* Allows execution from identified "safe" paths (non-admins cannot write to them); -* Allows execution of specifically authorized code in user-writable ("unsafe") directories. - -Rule implementation: -AppLocker rule types include path rules, publisher rules, and hash rules. -Rules allowing execution from "safe" locations are implemented using path rules. -User-writable subdirectories of the Windows and ProgramFiles directories are identified using Sysinternals AccessChk.exe. Exceptions for those subdirectories are implemented within path rules. -Exceptions for "dangerous" programs (e.g., mshta.exe, cipher.exe) are generally implemented with publisher rules. -Rules allowing execution of EXE, DLL, and script files from user-writable directories are implemented with publisher rules when possible, and hash rules otherwise. The publisher rules can optionally specify the current version "and above;" publisher rules always allow files to be updated without needing to update the corresponding rules. -Publisher rules can also be created allowing execution of anything signed by a particular publisher, or a specific product by a particular publisher. - -Scanning for user-writable subdirectories of the Windows and ProgramFiles directories can be time-consuming. The script writes results to text files in an intermediate subdirectory. The script runs the scan if those files are not found OR if the -Rescan switch is specified. -It is STRONGLY recommended that the scanning be performed with administrative rights. -Once scans have been performed, scanned output can be copied to another machine and rules can be maintained without needing to rescan. - -Dependencies: -PowerShell v5.1 or higher (Windows Management Framework 5.1 or higher) -Current (or recent) version of Sysinternals AccessChk.exe, either in the Path or in the same directory as this script. -Scripts and support files included in this solution (some are in specific subdirectories). - -See external documentation for more information. - -.LINK -Sysinternals AccessChk is available here: - https://technet.microsoft.com/sysinternals/accesschk - https://download.sysinternals.com/files/AccessChk.zip - https://live.sysinternals.com/accesschk.exe -or run .\Support\DownloadAccesschk.ps1, which downloads AccessChk.exe to the main AaronLocker directory. - -.PARAMETER Rescan -If this switch is set, this script scans the Windows and ProgramFiles directories for user-writable subdirectories, and captures data about EXE files to blacklist. -If the results from a previous scan are found in the expected location and this switch is not specified, the script does not perform those scans. If those results are not found, the script performs the scan even if this switch is not set. -It is STRONGLY recommended that the scanning be performed with administrative rights. - -.PARAMETER ForUser -If scanning a system with an administrative account with a need to inspect another user's profile for "unsafe paths," specify that username with this optional parameter. E.g., if logged on and scanning with administrative account "abby-adm" but need to inspect $env:USERPROFILE belonging to "toby", use -ForUser toby. - -.PARAMETER Excel -If specified, also creates Excel spreadsheets representing the generated rules. -#> - - - -#################################################################################################### -# Parameters -#################################################################################################### - -param( - # If set, forces rescans for user-writable directories under Windows and ProgramFiles - [switch] - $Rescan = $false, - - # If set, replaces current user name with another in "unsafe paths" - [parameter(Mandatory=$false)] - [String] - $ForUser, - - # If specified, also creates Excel spreadsheets representing the generated rules. - [switch] - $Excel -) - -#################################################################################################### -# Initialize -#################################################################################################### - -# -------------------------------------------------------------------------------- -# Only supported PowerShell version at this time: 5.1 -# PS Core v6.x doesn't include AppLocker cmdlets; string .Split() has new overloads that need to be dealt with. -# (At some point, may also need to check $PSVersionTable.PSEdition) -$psv = $PSVersionTable.PSVersion -if ($psv.Major -ne 5 -or $psv.Minor -ne 1) -{ - $errMsg = "This script requires PowerShell v5.1.`nCurrent version = " + $PSVersionTable.PSVersion.ToString() - Write-Error $errMsg - return -} - -# Make sure this script is running in FullLanguage mode -if ($ExecutionContext.SessionState.LanguageMode -ne [System.Management.Automation.PSLanguageMode]::FullLanguage) -{ - $errMsg = "This script must run in FullLanguage mode, but is running in " + $ExecutionContext.SessionState.LanguageMode.ToString() - Write-Error $errMsg - return -} - -# -------------------------------------------------------------------------------- - -$rootDir = [System.IO.Path]::GetDirectoryName($MyInvocation.MyCommand.Path) - -# Get configuration settings and global functions from .\Support\Config.ps1) -# Dot-source the config file. -. $rootDir\Support\Config.ps1 - -# Create subdirectories if they don't exist (some have to exist because files are expected to be there). -if (!(Test-Path($customizationInputsDir))) { mkdir $customizationInputsDir | Out-Null } -if (!(Test-Path($mergeRulesDynamicDir))) { mkdir $mergeRulesDynamicDir | Out-Null } -if (!(Test-Path($mergeRulesStaticDir))) { mkdir $mergeRulesStaticDir | Out-Null } -if (!(Test-Path($outputsDir))) { mkdir $outputsDir | Out-Null } -if (!(Test-Path($supportDir))) { mkdir $supportDir | Out-Null } -if (!(Test-Path($scanResultsDir))) { mkdir $scanResultsDir | Out-Null } - -# Look for results from previous scan for user-writable directories under the Windows and ProgramFiles directories. -# If any of the files containing the filtered results are missing, force a rescan. -if ( ! ( (Test-Path($windirTxt)) -and (Test-Path($PfTxt)) -and (Test-Path($Pf86Txt)) ) ) -{ - $Rescan = $true -} - -#################################################################################################### -# Scan Windir and ProgramFiles directories if needed -#################################################################################################### - -# -------------------------------------------------------------------------------- -# If $Rescan enabled, enumerate user-writable directories under %windir% and the ProgramFiles directories -# (scans the '(x86)' one only if present; doesn't raise an error if not present). -# This must be done at least once. Note that it can be time-consuming. Admin rights are recommended. -# Scanning requires that Sysinternals AccessChk.exe be in the Path or in the script directory. If it isn't, -# this script writes an error message and quits. -# Outputs the list of all writable subdirectories to "*_Full.txt"; the rules are built using those results with redundant lines removed. -# The filtered lists can be hand-edited if absolutely necessary. -if ($Rescan) -{ - # Scanning requires that AccessChk.exe be available. - # If accesschk.exe is in the rootdir, temporarily add the rootdir to the path. - # (Previous implementation invoked Get-Command to see whether accesschk.exe was in the path, and only if that failed looked for - # accesschk.exe in the rootdir. However, there was no good way to keep Get-Command from displaying a "Suggestion" message in that - # scenario.) - # Variable for restoring original Path, if necessary. - $origPath = "" - # Check for accesschk.exe in the rootdir. - if (Test-Path -Path $rootDir\AccessChk.exe) - { - # Found it in this script's directory. Temporarily prepend it to the path. - $origPath = $env:Path - $env:Path = "$rootDir;" + $origPath - } - # Otherwise, if AccessChk.exe not available in the path, write an error message and quit. - elseif ($null -eq (Get-Command AccessChk.exe -ErrorAction SilentlyContinue)) - { - $errMsg = "Scanning for writable subdirectories requires that Sysinternals AccessChk.exe be in the Path or in the same directory with this script.`n" + - "AccessChk.exe was not found.`n" + - "(See .\Support\DownloadAccesschk.ps1 for help.)`n" + - "Exiting..." - Write-Error $errMsg - return - } - - # Enumerate user-writable subdirectories in protected directories. Capture grantees so they can be inspected afterwards. - Write-Host "Enumerating writable directories in $env:windir" -ForegroundColor Cyan - $knownAdmins = @() - $knownAdmins += & $ps1_KnownAdmins - & $ps1_EnumWritableDirs -RootDirectory $env:windir -ShowGrantees -OutputXML -KnownAdmins $knownAdmins | Out-File -Encoding ASCII $windirFullXml - Write-Host "Enumerating writable directories in $env:ProgramFiles" -ForegroundColor Cyan - & $ps1_EnumWritableDirs -RootDirectory $env:ProgramFiles -ShowGrantees -OutputXML -KnownAdmins $knownAdmins | Out-File -Encoding ASCII $PfFullXml - # The following applies only to 64-bit Windows; skip it on 32-bit and create an empty file - if ($null -ne ${env:ProgramFiles(x86)}) - { - Write-Host "Enumerating writable directories in ${env:ProgramFiles(x86)}" -ForegroundColor Cyan - & $ps1_EnumWritableDirs -RootDirectory ${env:ProgramFiles(x86)} -ShowGrantees -OutputXML -KnownAdmins $knownAdmins | Out-File -Encoding ASCII $Pf86FullXml - } - else - { - # Create an empty file so the rest of the script doesn't have to take 32/64 into account. - New-Item $Pf86FullXml -ItemType File | Out-Null - } - # Restore original Path if it was altered for AccessChk.exe - if ($origPath.Length -gt 0) - { - $env:Path = $origPath - } - - # If a directory grants these permissions, the grantee can write an alternate data stream to the directory - # and execute it - $ADSWriteAndExecPerms = - [System.Security.AccessControl.FileSystemRights]::CreateFiles + - [System.Security.AccessControl.FileSystemRights]::CreateDirectories + - [System.Security.AccessControl.FileSystemRights]::WriteExtendedAttributes + - [System.Security.AccessControl.FileSystemRights]::WriteAttributes + - [System.Security.AccessControl.FileSystemRights]::ReadData + - [System.Security.AccessControl.FileSystemRights]::ExecuteFile - $InheritOnly = - [System.Security.AccessControl.PropagationFlags]::InheritOnly; - - # Function to determine whether a non-admin can create/modify an alternate data stream (ADS) on the directory - function HasWritableADS([System.Xml.XmlElement] $dirItem) - { - # Write-Verbose ($dirItem.name + ", " + $dirItem.Grantee) - $totalRights = [System.Security.AccessControl.FileSystemRights]0; - $acl = Get-Acl -LiteralPath $dirItem.Name - foreach( $grantee in $dirItem.Grantee ) - { - # Write-Verbose $grantee - foreach ( $ace in $acl.Access ) - { - # Write-Verbose ($ace.FileSystemRights.ToString() + " | " + $ace.PropagationFlags.ToString()) - # ACE applies to identified non-admin entity and isn't marked InheritOnly - if (($ace.IdentityReference.Value -eq $grantee) -and (($ace.PropagationFlags -band $InheritOnly) -eq 0)) - { - # Sum them up - $totalRights = $totalRights -bor $ace.FileSystemRights - } - } - } - # Write-Verbose "totalRights = $totalRights" - return (($totalRights -band $ADSWriteAndExecPerms) -eq $ADSWriteAndExecPerms) - } - - # Function to remove redundancies from lists of user-writable directories enumerated in the supplied XML. - # Assumes that input is an XML listing user-writable directories. This script sorts the list of directory names alphabetically, - # and then removes any entries for which a parent directory has already been identified. - # WHILE WE'RE AT IT, when we identify the top-parent writable directories, determine whether the directory allows a non-admin - # to add an Alternate Data Stream. If so, output a line to exclude execution from any ADS on the directory. - function RemoveRedundantLinesAndIdentifyWritableADS([String] $fnameFullXml) - { - $x = [xml](Get-Content $fnameFullXml) - if ($null -ne $x) - { - $lastItem = "" - # Case-insensitive alphabetic sort of directory names - $x.root.dir | Sort-Object name | foreach { - # First item in sorted list will be output. - # Anything that was output becomes $lastItem, lower-cased and ending with backslash. - # Anything that follows that matches $lastItem's full length (with backslash) must be a subdirectory - - # do not output that. - # When something doesn't match, it must be something other than a subdirectory of previous $lastItem. - # Write it out and make it $lastItem, lower-cased and ending with backslash. - $thisItem = $_ - if ($lastItem.Length -eq 0 -or !$thisItem.name.ToLower().StartsWith($lastItem)) - { - # Write output that serves as an exclusion for everything in this directory (including subdirectories) - Write-Output ($thisItem.name + "\*") - if (HasWritableADS($thisItem)) - { - # Write output that serves as an exclusion for any potential ADSes of this directory - Write-Output ($thisItem.name + ":*") - #Write-Verbose ("Writable ADS: " + $thisItem.name) - #Write-Verbose ("----------------------------") - } - $lastItem = $thisItem.name.ToLower() - if (!$lastItem.EndsWith("\")) { $lastItem += "\" } - } - } - } - } - - Write-Host "Removing redundancies in scan results" -ForegroundColor Cyan - RemoveRedundantLinesAndIdentifyWritableADS $windirFullXml | Out-File -Encoding ASCII $windirTxt - RemoveRedundantLinesAndIdentifyWritableADS $PfFullXml | Out-File -Encoding ASCII $PfTxt - RemoveRedundantLinesAndIdentifyWritableADS $Pf86FullXml | Out-File -Encoding ASCII $Pf86Txt -} - -#################################################################################################### -# Capture data for Exe files to blacklist if needed -#################################################################################################### -if ( $Rescan -or !(Test-Path($ExeBlacklistData) ) ) -{ - Write-Host "Processing EXE files to blacklist..." -ForegroundColor Cyan - # Get the EXE files to blacklist from the script that produces that list. - $exeFilesToBlacklist = (& $ps1_GetExeFilesToBlacklist) - # Create a hash collection for publisher information. Key on publisher name, product name, and binary name. - # Add to collection if equivalent is not already in the collection. - $pubCollection = @{} - $exeFilesToBlacklist | foreach { - $pub = (Get-AppLockerFileInformation "$_").Publisher - if ($null -ne $pub) - { - $pubKey = ($pub.PublisherName + "|" + $pub.ProductName + "|" + $pub.BinaryName).ToLower() - if (!$pubCollection.ContainsKey($pubKey)) { $pubCollection.Add($pubKey, $pub) } - } - else - { - Write-Warning "UNABLE TO BUILD BLACKLIST RULE FOR $_" - } - } - - $pubCollection.Values | - Select-Object PublisherName, ProductName, BinaryName | - ConvertTo-Csv -NoTypeInformation | - Out-File $ExeBlacklistData -Encoding unicode -} - -#################################################################################################### -# Validate that scan-result files were created -#################################################################################################### - -if ( ! ( (Test-Path($windirTxt)) -and (Test-Path($PfTxt)) -and (Test-Path($Pf86Txt)) ) ) -{ - $errMsg = "One or more of the following files is missing:`n" + - "`t" + $windirTxt + "`n" + - "`t" + $PfTxt + "`n" + - "`t" + $Pf86Txt +"`n" - Write-Error $errMsg - return -} - -if ( ! (Test-Path($ExeBlacklistData)) ) -{ - $errMsg = "The following file is missing:`n" + - "`t" + $ExeBlacklistData +"`n" - Write-Error $errMsg - return -} - -#################################################################################################### -# Process Windir and ProgramFiles directories. -#################################################################################################### - -# -------------------------------------------------------------------------------- -# Read the lists of user-writable directories with redundancies removed. -$Wr_raw_windir = (Get-Content $windirTxt) -$Wr_raw_PF = (Get-Content $PfTxt) -$Wr_raw_PF86 = (Get-Content $Pf86Txt) - -# -------------------------------------------------------------------------------- -# Process names of directories, replacing hardcoded C:\, \Windows, etc., with AppLocker variables. -# Note that System32 and SysWOW64 map to the same variable names, as do the two ProgramFiles directories. -# Add trailing backslashes to the names (e.g., C:\Windows\System32\ ), so that if there happens to be -# a "C:\Windows\System32Extra" it won't match the System32 variable. -# Note that because of the trailing backslashes, if the top directories themselves are user-writable, -# they won't turn up in the list. That by itself would be a major problem, though. -$sSystem32 = "$env:windir\System32\".ToLower() -$sSysWow64 = "$env:windir\SysWOW64\".ToLower() -$sWindir = "$env:windir\".ToLower() -$sPF86 = "${env:ProgramFiles(x86)}\".ToLower() -$sPF = "$env:ProgramFiles\".ToLower() - -# Build arrays of processed directory names with duplicates removed. (E.g., System32\Com\dmp and -# SysWOW64\Com\dmp can both be covered with a single entry.) -$Wr_windir = @() -$Wr_PF = @() - -# For the Windows list, replace matching System32, SysWOW64, and Windows paths with corresponding -# AppLocker variables, then add to collection if not already present. -$Wr_raw_windir | foreach { - $dir = $_.ToLower() - if ($dir.StartsWith($sSystem32)) { $dir = "%SYSTEM32%\" + $dir.Substring($sSystem32.Length) } - elseif ($dir.StartsWith($sSysWow64)) { $dir = "%SYSTEM32%\" + $dir.Substring($sSysWow64.Length) } - elseif ($dir.StartsWith($sWindir)) { $dir = "%WINDIR%\" + $dir.Substring($sWindir.Length) } - # Don't add the rule twice if it appears in both System32 and SysWOW64, since both map to %SYSTEM32%. - if (!$Wr_windir.Contains($dir)) - { - $Wr_windir += $dir - } -} - -# For the two ProgramFiles lists, replace top directory with AppLocker variable, then add to collection -# if not already present. -$Wr_raw_PF86 | foreach { - $dir = $_.ToLower() - if ($dir.StartsWith($sPF86)) { $dir = "%PROGRAMFILES%\" + $dir.Substring($sPF86.Length) } - $Wr_PF += $dir -} - -$Wr_raw_PF | foreach { - $dir = $_.ToLower() - if ($dir.StartsWith($sPF)) { $dir = "%PROGRAMFILES%\" + $dir.Substring($sPF.Length) } - # Possibly already added same directory from PF86; don't add again - if (!$Wr_PF.Contains($dir)) - { - $Wr_PF += $dir - } -} - -#################################################################################################### -# Load base AppLocker rules document -#################################################################################################### - -# -------------------------------------------------------------------------------- -# Build AppLocker rules starting with base document -$xDocument = [xml](Get-Content $defRulesXml) - -#################################################################################################### -# Incorporate data for EXE files to blacklist under Windir -#################################################################################################### - -# Incorporate the EXE blacklist into the document where the one PLACEHOLDER_WINDIR_EXEBLACKLIST -# placeholder is. -$xPlaceholder = $xDocument.SelectNodes("//PLACEHOLDER_WINDIR_EXEBLACKLIST")[0] -$xExcepts = $xPlaceholder.ParentNode - -$csvExeBlacklistData = (Get-Content $ExeBlacklistData | ConvertFrom-Csv) -$csvExeBlacklistData | foreach { - # Create a FilePublisherCondition element with the publisher attributes - $elem = $xDocument.CreateElement("FilePublisherCondition") - $elem.SetAttribute("PublisherName", $_.PublisherName) - $elem.SetAttribute("ProductName", $_.ProductName) - $elem.SetAttribute("BinaryName", $_.BinaryName) - # Set version number range to "any" - $elemVerRange = $xDocument.CreateElement("BinaryVersionRange") - $elemVerRange.SetAttribute("LowSection", "*") - $elemVerRange.SetAttribute("HighSection", "*") - # Add the version range to the publisher condition - $elem.AppendChild($elemVerRange) | Out-Null - # Add the publisher condition where the placeholder is - $xExcepts.AppendChild($elem) | Out-Null -} -# Remove the placeholder element -$xExcepts.RemoveChild($xPlaceholder) | Out-Null - -Write-Host "Processing additional safe paths to whitelist..." -ForegroundColor Cyan -# Get additional whitelisted paths from the script that produces that list and incorporate them into the document -$PathsToAllow = (& $ps1_GetSafePathsToAllow) -# Add "allow" for Everyone for Exe, Dll, and Script rules -$xRuleCollections = $xDocument.SelectNodes("//RuleCollection[@Type='Exe' or @Type='Script' or @Type='Dll']") -foreach($xRuleCollection in $xRuleCollections) -{ - $PathsToAllow | foreach { - # If path is an existing directory and doesn't have trailing "\*" appended, fix it so that it does. - # If path is a file, don't append \*. If the path ends with \*, no need for further validation. - # If it doesn't end with \* but Get-Item can't identify it as a file or a directory, write a warning and accept it as is. - $pathToAllow = $_ - if (!$pathToAllow.EndsWith("\*")) - { - $pathItem = Get-Item $pathToAllow -Force -ErrorAction SilentlyContinue - if ($pathItem -eq $null) - { - Write-Warning "Cannot verify path $pathItem; adding to rule set as is." - } - elseif ($pathItem -is [System.IO.DirectoryInfo]) - { - Write-Warning "Appending `"\*`" to rule for $pathToAllow" - $pathToAllow = [System.IO.Path]::Combine($pathToAllow, "*") - } - } - $elemRule = $xDocument.CreateElement("FilePathRule") - $elemRule.SetAttribute("Action", "Allow") - $elemRule.SetAttribute("UserOrGroupSid", "S-1-1-0") - $elemRule.SetAttribute("Id", [GUID]::NewGuid().Guid) - $elemRule.SetAttribute("Name", "Additional allowed path: " + $pathToAllow) - $elemRule.SetAttribute("Description", "Allows Everyone to execute from " + $pathToAllow) - $elemConditions = $xDocument.CreateElement("Conditions") - $elemCondition = $xDocument.CreateElement("FilePathCondition") - $elemCondition.SetAttribute("Path", $pathToAllow) - $elemConditions.AppendChild($elemCondition) | Out-Null - $elemRule.AppendChild($elemConditions) | Out-Null - $xRuleCollection.AppendChild($elemRule) | Out-Null - } -} - -# Incorporate path-exception rules for the user-writable directories under %windir% -# in the the EXE, DLL, and SCRIPT rules. -# Find the placeholders for Windows subdirectories, and add the path conditions there. -# Then remove the placeholders. -$xPlaceholders = $xDocument.SelectNodes("//PLACEHOLDER_WINDIR_WRITABLEDIRS") -foreach($xPlaceholder in $xPlaceholders) -{ - $xExcepts = $xPlaceholder.ParentNode - $Wr_windir | foreach { - $elem = $xDocument.CreateElement("FilePathCondition") - $elem.SetAttribute("Path", $_) - $xExcepts.AppendChild($elem) | Out-Null - } - $xExcepts.RemoveChild($xPlaceholder) | Out-Null -} - -# Incorporate path-exception rules for the user-writable directories under %PF% -# in EXE, DLL, and SCRIPT rules. -# Find the placeholders for PF subdirectories, and add the path conditions there. -# Then remove the placeholders. -$xPlaceholders = $xDocument.SelectNodes("//PLACEHOLDER_PF_WRITABLEDIRS") -foreach($xPlaceholder in $xPlaceholders) -{ - $xExcepts = $xPlaceholder.ParentNode - $Wr_PF | foreach { - $elem = $xDocument.CreateElement("FilePathCondition") - $elem.SetAttribute("Path", $_) - $xExcepts.AppendChild($elem) | Out-Null - } - $xExcepts.RemoveChild($xPlaceholder) | Out-Null -} - - -#################################################################################################### -# Begin creating dynamically-generated rule fragments. Delete old ones first. -#################################################################################################### - -# Delete previous set of dynamically-generated rules first -Remove-Item ([System.IO.Path]::Combine($mergeRulesDynamicDir, "*.xml")) - - -#################################################################################################### -# Create rules for trusted publishers -#################################################################################################### -Write-Host "Creating rules for trusted publishers..." -ForegroundColor Cyan - -# Define an empty AppLocker policy to fill, with a blank publisher rule to use as a template. -$signerPolXml = [xml]@" - - - - - - - - - - - - - - -"@ -# Get the blank publisher rule. It will be cloned to make the real publisher rules, and then this blank will be deleted. -$fprTemplate = $signerPolXml.DocumentElement.SelectNodes("//FilePublisherRule")[0] - -# Run the script that produces the signer information to process. Should come in as a sequence of hashtables. -# Each hashtable must have a label, and either an exemplar or a publisher. -# fprRulesNotEmpty: Don't generate TrustedSigners.xml if it doesn't have any rules. -$fprRulesNotEmpty = $false -$signersToBuildRulesFor = (& $ps1_TrustedSigners) -$signersToBuildRulesFor | foreach { - $label = $_.label - if ($label -eq $null) - { - # Each hashtable must have a label. - Write-Warning -Message ("Invalid syntax in $ps1_TrustedSigners. No `"label`" specified.") - } - else - { - $publisher = $product = $binaryname = "" - $filename = "" - $good = $false - # Exemplar is a file signed by the publisher we want to trust. If the hashtable specifies "useProduct" = $true, - # the AppLocker rule allows anything signed by that publisher with the same ProductName. - if ($_.exemplar) - { - $filename = $_.exemplar - $alfi = Get-AppLockerFileInformation $filename - if ($alfi -eq $null) - { - Write-Warning -Message ("Cannot get AppLockerFileInformation for $filename") - } - elseif (!($alfi.Publisher.HasPublisherName)) - { - Write-Warning -Message ("Cannot get publisher information for $filename") - } - elseif ($_.useProduct -and !($alfi.Publisher.HasProductName)) - { - Write-Warning "Cannot get product name information for $filename" - } - else - { - # Get publisher to trust, and optionally ProductName. - $publisher = $alfi.Publisher.PublisherName - if ($_.useProduct) - { - $product = $alfi.Publisher.ProductName - } - $good = $true - } - } - else - { - # Otherwise, the hashtable must specify the exact publisher to trust (and optionally ProductName, BinaryName+collection). - $publisher = $_.PublisherName - $product = $_.ProductName - $binaryName = $_.BinaryName - $fileVersion = $_.FileVersion - $ruleCollection = $_.RuleCollection - if ($null -ne $publisher) - { - $good = $true - } - else - { - # Object isn't a hashtable, or doesn't have either exemplar or PublisherName. - Write-Warning -Message ("Invalid syntax in $ps1_TrustedSigners") - } - } - - if ($good) - { - $fprRulesNotEmpty = $true - - # Duplicate the blank publisher rule, and populate it with information gathered. - $fpr = $fprTemplate.Clone() - $fpr.Conditions.FilePublisherCondition.PublisherName = $publisher - - $fpr.Name = "$label`: Signer rule for $publisher" - if ($product.Length -gt 0) - { - $fpr.Conditions.FilePublisherCondition.ProductName = $product - $fpr.Name = "$label`: Signer/product rule for $publisher/$product" - if ($binaryName.Length -gt 0) - { - $fpr.Conditions.FilePublisherCondition.BinaryName = $binaryName - $fpr.Name = "$label`: Signer/product/file rule for $publisher/$product/$binaryName" - if ($fileVersion.Length -gt 0) - { - $fpr.Conditions.FilePublisherCondition.BinaryVersionRange.LowSection = $fileVersion - } - } - } - if ($filename.Length -gt 0) - { - $fpr.Description = "Information acquired from $filename" - } - else - { - $fpr.Description = "Information acquired from $fname_TrustedSigners" - } - Write-Host ("`t" + $fpr.Name) -ForegroundColor Cyan - - if ($publisher.ToLower().Contains("microsoft") -and $product.Length -eq 0 -and ($ruleCollection.Length -eq 0 -or $ruleCollection -eq "Exe")) - { - Write-Warning -Message ("Warning: Trusting all Microsoft-signed files is an overly-broad whitelisting strategy") - } - - if ($ruleCollection) - { - $node = $signerPolXml.SelectSingleNode("//RuleCollection[@Type='" + $ruleCollection + "']") - if ($node -eq $null) - { - Write-Warning ("Couldn't find RuleCollection Type = " + $ruleCollection + " (RuleCollection is case-sensitive)") - } - else - { - $fpr.Id = [string]([GUID]::NewGuid().Guid) - $node.AppendChild($fpr) | Out-Null - } - } - else - { - # Append a copy of the new publisher rule into each rule set with a different GUID in each. - $signerPolXml.SelectNodes("//RuleCollection") | foreach { - $fpr0 = $fpr.CloneNode($true) - - $fpr0.Id = [string]([GUID]::NewGuid().Guid) - $_.AppendChild($fpr0) | Out-Null - } - } - } - } -} - -# Don't generate the file if it doesn't contain any rules -if ($fprRulesNotEmpty) -{ - # Delete the blank publisher rule from the rule set. - $fprTemplate.ParentNode.RemoveChild($fprTemplate) | Out-Null - - #$signerPolXml.OuterXml | clip - $outfile = [System.IO.Path]::Combine($mergeRulesDynamicDir, "TrustedSigners.xml") - # Save XML as Unicode - SaveXmlDocAsUnicode -xmlDoc $signerPolXml -xmlFilename $outfile -} - -#################################################################################################### -# Create custom hash rules -#################################################################################################### -Write-Host "Creating extra hash rules ..." -ForegroundColor Cyan - -# Define an empty AppLocker policy to fill, with a blank hash rule to use as a template. -$hashRuleXml = [xml]@" - - - - - - - - - - - - - - -"@ -# Get the blank hash rule. It will be cloned to make the real hash rules. -$fhrTemplate = $hashRuleXml.DocumentElement.SelectNodes("//FileHashRule")[0] -# Remove the template rule from the main document -$fhrTemplate.ParentNode.RemoveChild($fhrTemplate) | Out-Null -# fhrRulesNotEmpty: Don't generate ExtraHashRules.xml if it doesn't have any rules. -$fhrRulesNotEmpty = $false - -# Run the script that produces the hash information to process. Should come in as a sequence of hashtables. -# Each hashtable must have the following properties: -# * RuleCollection (case-sensitive) -# * RuleName -# * RuleDesc -# * HashVal (must be SHA256 with "0x" and 64 hex digits) -# * FileName -$hashRuleData = (& $ps1_HashRuleData) - -$hashRuleData | foreach { - - $fhr = $fhrTemplate.Clone() - $fhr.Id = [string]([GUID]::NewGuid().Guid) - $fhr.Name = $_.RuleName - $fhr.Description = $_.RuleDesc - $fhr.Conditions.FileHashCondition.FileHash.Data = $_.HashVal - $fhr.Conditions.FileHashCondition.FileHash.SourceFileName = $_.FileName - - $node = $hashRuleXml.SelectSingleNode("//RuleCollection[@Type='" + $_.RuleCollection + "']") - if ($node -eq $null) - { - Write-Warning ("Couldn't find RuleCollection Type = " + $_.RuleCollection + " (RuleCollection is case-sensitive)") - } - else - { - $node.AppendChild($fhr) | Out-Null - $fhrRulesNotEmpty = $true - } -} - -# Don't generate the file if it doesn't contain any rules -if ($fhrRulesNotEmpty) -{ - $outfile = [System.IO.Path]::Combine($mergeRulesDynamicDir, "ExtraHashRules.xml") - # Save XML as Unicode - SaveXmlDocAsUnicode -xmlDoc $hashRuleXml -xmlFilename $outfile -} - -#################################################################################################### -# Rules for files in user-writable directories -#################################################################################################### - -# -------------------------------------------------------------------------------- -# Helper function used to replace current username with another in paths. -function RenamePaths($paths, $forUsername) -{ - # Warning: if $forUsername is "Users" that will be a problem. - $forUsername = "\" + $forUsername - # Look for username bracketed by backslashes, or at end of the path. - $CurrentName = "\" + $env:USERNAME.ToLower() + "\" - $CurrentNameFinal = "\" + $env:USERNAME.ToLower() - - $paths | ForEach-Object { - $origTargetDir = $_ - # Temporarily remove trailing \* if present; can't GetFullPath with that. - if ($origTargetDir.EndsWith("\*")) - { - $bAppend = "\*" - $targetDir = $origTargetDir.Substring(0, $origTargetDir.Length - 2) - } - else - { - $bAppend = "" - $targetDir = $origTargetDir - } - # GetFullPath in case the provided name is 8.3-shortened. - $targetDir = [System.IO.Path]::GetFullPath($targetDir).ToLower() - if ($targetDir.Contains($CurrentName) -or $targetDir.EndsWith($CurrentNameFinal)) - { - $targetDir.Replace($CurrentNameFinal, $forUsername) + $bAppend - } - else - { - $origTargetDir - } - } -} - -# -------------------------------------------------------------------------------- -# Build rules for files in writable directories identified in the "unsafe paths to build rules for" script. -# Uses BuildRulesForFilesInWritableDirectories.ps1. -# Writes results to the dynamic merge-rules directory, using the script-supplied labels as part of the file name. -# The files in the merge-rules directories will be merged into the main document later. -# (Doing this after the other files are created in the MergeRulesDynamicDir - file naming logic handles cases where -# file already exists from the other dynamically-generated files above, or if multiple items have the same label. - -if ( !(Test-Path($ps1_UnsafePathsToBuildRulesFor)) ) -{ - $errmsg = "Script file not found: $ps1_UnsafePathsToBuildRulesFor`nNo new rules generated for files in writable directories." - Write-Warning $errmsg -} -else -{ - Write-Host "Creating rules for files in writable directories..." -ForegroundColor Cyan - $UnsafePathsToBuildRulesFor = (& $ps1_UnsafePathsToBuildRulesFor) - $UnsafePathsToBuildRulesFor | foreach { - $label = $_.label - if ($ForUser) - { - $paths = RenamePaths -paths $_.paths -forUsername $ForUser - } - else - { - $paths = $_.paths - } - $recurse = $true; - if ($null -ne $_.noRecurse) { $recurse = !$_.noRecurse } - $enforceMinFileVersion = $true - if ($null -ne $_.enforceMinVersion) { $enforceMinFileVersion = $_.enforceMinVersion } - $outfile = [System.IO.Path]::Combine($mergeRulesDynamicDir, $label + " Rules.xml") - # If it already exists, create a name that doesn't exist yet - $ixOutfile = [int]2 - while (Test-Path($outfile)) - { - $outfile = [System.IO.Path]::Combine($mergeRulesDynamicDir, $label + " (" + $ixOutfile.ToString() + ") Rules.xml") - $ixOutfile++ - } - Write-Host ("Scanning $label`:", $paths) -Separator "`n`t" -ForegroundColor Cyan - & $ps1_BuildRulesForFilesInWritableDirectories -FileSystemPaths $paths -RecurseDirectories: $recurse -EnforceMinimumVersion: $enforceMinFileVersion -RuleNamePrefix $label -OutputFileName $outfile - } -} - -#################################################################################################### -# Merging custom rules -#################################################################################################### - -# -------------------------------------------------------------------------------- -# Load the XML document with modifications into an AppLockerPolicy object -$masterPolicy = [Microsoft.Security.ApplicationId.PolicyManagement.PolicyModel.AppLockerPolicy]::FromXml($xDocument.OuterXml) - -Write-Host "Loading custom rule sets..." -ForegroundColor Cyan -# Merge any and all policy files found in the MergeRules directories, typically for authorized files in writable directories. -# Some may have been created in the previous step; others might have been dropped in from other sources. -Get-ChildItem $mergeRulesDynamicDir\*.xml, $mergeRulesStaticDir\*.xml | foreach { - $policyFileToMerge = $_ - Write-Host ("`tMerging " + $_.Directory.Name + "\" + $_.Name) -ForegroundColor Cyan - $policyToMerge = [Microsoft.Security.ApplicationId.PolicyManagement.PolicyModel.AppLockerPolicy]::Load($policyFileToMerge) - $masterPolicy.Merge($policyToMerge) -} - -#TODO: Optimize rules in rule collections here - combine/remove redundant/overlapping rules - -#################################################################################################### -# Generate final outputs -#################################################################################################### - -# Generate two versions of the rules file: one with rules enforced, and one with auditing only. - -Write-Host "Creating final rule outputs..." -ForegroundColor Cyan - -# Generate the Enforced version -foreach( $ruleCollection in $masterPolicy.RuleCollections) -{ - $ruleCollection.EnforcementMode = "Enabled" -} -SaveAppLockerPolicyAsUnicodeXml -ALPolicy $masterPolicy -xmlFilename $rulesFileEnforceNew - -# Generate the AuditOnly version -foreach( $ruleCollection in $masterPolicy.RuleCollections) -{ - $ruleCollection.EnforcementMode = "AuditOnly" -} -SaveAppLockerPolicyAsUnicodeXml -ALPolicy $masterPolicy -xmlFilename $rulesFileAuditNew - -if ($Excel) -{ - & $ps1_ExportPolicyToExcel -AppLockerXML $rulesFileEnforceNew -SaveWorkbook - & $ps1_ExportPolicyToExcel -AppLockerXML $rulesFileAuditNew -SaveWorkbook -} - -# -------------------------------------------------------------------------------- +<# +.SYNOPSIS +Builds comprehensive and robust AppLocker "audit" and "enforce" rules to mitigate against users running unauthorized software, customizable through simple text files. Writes results to the Outputs subdirectory. + +TODO: Find and remove redundant rules. Report stripped rules to a separate log file. + +.DESCRIPTION +Create-Policies.ps1 generates comprehensive "audit" and "enforce" AppLocker rules to restrict non-admin code execution to "authorized" software, +in a way to minimize the need to update the rules. +Broadly speaking, "authorized" means that an administrator put it on the computer, OR created a rule specifically for that item. +* Supported operating systems include Windows 7 and newer, and Windows Server 2008 R2 and newer. +* Rules cover EXE, DLL, Script, and MSI; on Windows 8.1 and newer, rules also cover Packaged apps. +* Allows execution from the Windows and ProgramFiles directories, EXCEPT: + * Identifies user-writable subdirectories and disallows execution from those directories; + * Disallows execution of programs that run user-supplied code (e.g., mshta.exe); + * Disallows execution of programs that non-admins rarely need but that malware/ransomware authors are known to use (e.g., cipher.exe); +* Allows execution from identified "safe" paths (non-admins cannot write to them); +* Allows execution of specifically authorized code in user-writable ("unsafe") directories. + +Rule implementation: +AppLocker rule types include path rules, publisher rules, and hash rules. +Rules allowing execution from "safe" locations are implemented using path rules. +User-writable subdirectories of the Windows and ProgramFiles directories are identified using Sysinternals AccessChk.exe. Exceptions for those subdirectories are implemented within path rules. +Exceptions for "dangerous" programs (e.g., mshta.exe, cipher.exe) are generally implemented with publisher rules. +Rules allowing execution of EXE, DLL, and script files from user-writable directories are implemented with publisher rules when possible, and hash rules otherwise. The publisher rules can optionally specify the current version "and above;" publisher rules always allow files to be updated without needing to update the corresponding rules. +Publisher rules can also be created allowing execution of anything signed by a particular publisher, or a specific product by a particular publisher. + +Scanning for user-writable subdirectories of the Windows and ProgramFiles directories can be time-consuming. The script writes results to text files in an intermediate subdirectory. The script runs the scan if those files are not found OR if the -Rescan switch is specified. +It is STRONGLY recommended that the scanning be performed with administrative rights. +Once scans have been performed, scanned output can be copied to another machine and rules can be maintained without needing to rescan. + +Dependencies: +PowerShell v5.1 or higher (Windows Management Framework 5.1 or higher) +Current (or recent) version of Sysinternals AccessChk.exe, either in the Path or in the same directory as this script. +Scripts and support files included in this solution (some are in specific subdirectories). + +See external documentation for more information. + +.LINK +Sysinternals AccessChk is available here: + https://technet.microsoft.com/sysinternals/accesschk + https://download.sysinternals.com/files/AccessChk.zip + https://live.sysinternals.com/accesschk.exe +or run .\Support\DownloadAccesschk.ps1, which downloads AccessChk.exe to the main AaronLocker directory. + +.PARAMETER Rescan +If this switch is set, this script scans the Windows and ProgramFiles directories for user-writable subdirectories, and captures data about EXE files to blacklist. +If the results from a previous scan are found in the expected location and this switch is not specified, the script does not perform those scans. If those results are not found, the script performs the scan even if this switch is not set. +It is STRONGLY recommended that the scanning be performed with administrative rights. + +.PARAMETER ForUser +If scanning a system with an administrative account with a need to inspect another user's profile for "unsafe paths," specify that username with this optional parameter. E.g., if logged on and scanning with administrative account "abby-adm" but need to inspect $env:USERPROFILE belonging to "toby", use -ForUser toby. + +.PARAMETER Excel +If specified, also creates Excel spreadsheets representing the generated rules. +#> + + + +#################################################################################################### +# Parameters +#################################################################################################### + +param( + # If set, forces rescans for user-writable directories under Windows and ProgramFiles + [switch] + $Rescan = $false, + + # If set, replaces current user name with another in "unsafe paths" + [parameter(Mandatory=$false)] + [String] + $ForUser, + + # If specified, also creates Excel spreadsheets representing the generated rules. + [switch] + $Excel +) + +#################################################################################################### +# Initialize +#################################################################################################### + +# -------------------------------------------------------------------------------- +# Only supported PowerShell version at this time: 5.1 +# PS Core v6.x doesn't include AppLocker cmdlets; string .Split() has new overloads that need to be dealt with. +# (At some point, may also need to check $PSVersionTable.PSEdition) +$psv = $PSVersionTable.PSVersion +if ($psv.Major -ne 5 -or $psv.Minor -ne 1) +{ + $errMsg = "This script requires PowerShell v5.1.`nCurrent version = " + $PSVersionTable.PSVersion.ToString() + Write-Error $errMsg + return +} + +# Make sure this script is running in FullLanguage mode +if ($ExecutionContext.SessionState.LanguageMode -ne [System.Management.Automation.PSLanguageMode]::FullLanguage) +{ + $errMsg = "This script must run in FullLanguage mode, but is running in " + $ExecutionContext.SessionState.LanguageMode.ToString() + Write-Error $errMsg + return +} + +# -------------------------------------------------------------------------------- + +$rootDir = [System.IO.Path]::GetDirectoryName($MyInvocation.MyCommand.Path) + +# Get configuration settings and global functions from .\Support\Config.ps1) +# Dot-source the config file. +. $rootDir\Support\Config.ps1 + +# Create subdirectories if they don't exist (some have to exist because files are expected to be there). +if (!(Test-Path($customizationInputsDir))) { mkdir $customizationInputsDir | Out-Null } +if (!(Test-Path($mergeRulesDynamicDir))) { mkdir $mergeRulesDynamicDir | Out-Null } +if (!(Test-Path($mergeRulesStaticDir))) { mkdir $mergeRulesStaticDir | Out-Null } +if (!(Test-Path($outputsDir))) { mkdir $outputsDir | Out-Null } +if (!(Test-Path($supportDir))) { mkdir $supportDir | Out-Null } +if (!(Test-Path($scanResultsDir))) { mkdir $scanResultsDir | Out-Null } + +# Look for results from previous scan for user-writable directories under the Windows and ProgramFiles directories. +# If any of the files containing the filtered results are missing, force a rescan. +if ( ! ( (Test-Path($windirTxt)) -and (Test-Path($PfTxt)) -and (Test-Path($Pf86Txt)) ) ) +{ + $Rescan = $true +} + +#################################################################################################### +# Scan Windir and ProgramFiles directories if needed +#################################################################################################### + +# -------------------------------------------------------------------------------- +# If $Rescan enabled, enumerate user-writable directories under %windir% and the ProgramFiles directories +# (scans the '(x86)' one only if present; doesn't raise an error if not present). +# This must be done at least once. Note that it can be time-consuming. Admin rights are recommended. +# Scanning requires that Sysinternals AccessChk.exe be in the Path or in the script directory. If it isn't, +# this script writes an error message and quits. +# Outputs the list of all writable subdirectories to "*_Full.txt"; the rules are built using those results with redundant lines removed. +# The filtered lists can be hand-edited if absolutely necessary. +if ($Rescan) +{ + # Scanning requires that AccessChk.exe be available. + # If accesschk.exe is in the rootdir, temporarily add the rootdir to the path. + # (Previous implementation invoked Get-Command to see whether accesschk.exe was in the path, and only if that failed looked for + # accesschk.exe in the rootdir. However, there was no good way to keep Get-Command from displaying a "Suggestion" message in that + # scenario.) + # Variable for restoring original Path, if necessary. + $origPath = "" + # Check for accesschk.exe in the rootdir. + if (Test-Path -Path $rootDir\AccessChk.exe) + { + # Found it in this script's directory. Temporarily prepend it to the path. + $origPath = $env:Path + $env:Path = "$rootDir;" + $origPath + } + # Otherwise, if AccessChk.exe not available in the path, write an error message and quit. + elseif ($null -eq (Get-Command AccessChk.exe -ErrorAction SilentlyContinue)) + { + $errMsg = "Scanning for writable subdirectories requires that Sysinternals AccessChk.exe be in the Path or in the same directory with this script.`n" + + "AccessChk.exe was not found.`n" + + "(See .\Support\DownloadAccesschk.ps1 for help.)`n" + + "Exiting..." + Write-Error $errMsg + return + } + + # Enumerate user-writable subdirectories in protected directories. Capture grantees so they can be inspected afterwards. + Write-Host "Enumerating writable directories in $env:windir" -ForegroundColor Cyan + $knownAdmins = @() + $knownAdmins += & $ps1_KnownAdmins + & $ps1_EnumWritableDirs -RootDirectory $env:windir -ShowGrantees -OutputXML -KnownAdmins $knownAdmins | Out-File -Encoding ASCII $windirFullXml + Write-Host "Enumerating writable directories in $env:ProgramFiles" -ForegroundColor Cyan + & $ps1_EnumWritableDirs -RootDirectory $env:ProgramFiles -ShowGrantees -OutputXML -KnownAdmins $knownAdmins | Out-File -Encoding ASCII $PfFullXml + # The following applies only to 64-bit Windows; skip it on 32-bit and create an empty file + if ($null -ne ${env:ProgramFiles(x86)}) + { + Write-Host "Enumerating writable directories in ${env:ProgramFiles(x86)}" -ForegroundColor Cyan + & $ps1_EnumWritableDirs -RootDirectory ${env:ProgramFiles(x86)} -ShowGrantees -OutputXML -KnownAdmins $knownAdmins | Out-File -Encoding ASCII $Pf86FullXml + } + else + { + # Create an empty file so the rest of the script doesn't have to take 32/64 into account. + New-Item $Pf86FullXml -ItemType File | Out-Null + } + # Restore original Path if it was altered for AccessChk.exe + if ($origPath.Length -gt 0) + { + $env:Path = $origPath + } + + # If a directory grants these permissions, the grantee can write an alternate data stream to the directory + # and execute it + $ADSWriteAndExecPerms = + [System.Security.AccessControl.FileSystemRights]::CreateFiles + + [System.Security.AccessControl.FileSystemRights]::CreateDirectories + + [System.Security.AccessControl.FileSystemRights]::WriteExtendedAttributes + + [System.Security.AccessControl.FileSystemRights]::WriteAttributes + + [System.Security.AccessControl.FileSystemRights]::ReadData + + [System.Security.AccessControl.FileSystemRights]::ExecuteFile + $InheritOnly = + [System.Security.AccessControl.PropagationFlags]::InheritOnly; + + # Function to determine whether a non-admin can create/modify an alternate data stream (ADS) on the directory + function HasWritableADS([System.Xml.XmlElement] $dirItem) + { + # Write-Verbose ($dirItem.name + ", " + $dirItem.Grantee) + $totalRights = [System.Security.AccessControl.FileSystemRights]0; + $acl = Get-Acl -LiteralPath $dirItem.Name + foreach( $grantee in $dirItem.Grantee ) + { + # Write-Verbose $grantee + foreach ( $ace in $acl.Access ) + { + # Write-Verbose ($ace.FileSystemRights.ToString() + " | " + $ace.PropagationFlags.ToString()) + # ACE applies to identified non-admin entity and isn't marked InheritOnly + if (($ace.IdentityReference.Value -eq $grantee) -and (($ace.PropagationFlags -band $InheritOnly) -eq 0)) + { + # Sum them up + $totalRights = $totalRights -bor $ace.FileSystemRights + } + } + } + # Write-Verbose "totalRights = $totalRights" + return (($totalRights -band $ADSWriteAndExecPerms) -eq $ADSWriteAndExecPerms) + } + + # Function to remove redundancies from lists of user-writable directories enumerated in the supplied XML. + # Assumes that input is an XML listing user-writable directories. This script sorts the list of directory names alphabetically, + # and then removes any entries for which a parent directory has already been identified. + # WHILE WE'RE AT IT, when we identify the top-parent writable directories, determine whether the directory allows a non-admin + # to add an Alternate Data Stream. If so, output a line to exclude execution from any ADS on the directory. + function RemoveRedundantLinesAndIdentifyWritableADS([String] $fnameFullXml) + { + $x = [xml](Get-Content $fnameFullXml) + if ($null -ne $x) + { + $lastItem = "" + # Case-insensitive alphabetic sort of directory names + $x.root.dir | Sort-Object name | foreach { + # First item in sorted list will be output. + # Anything that was output becomes $lastItem, lower-cased and ending with backslash. + # Anything that follows that matches $lastItem's full length (with backslash) must be a subdirectory - + # do not output that. + # When something doesn't match, it must be something other than a subdirectory of previous $lastItem. + # Write it out and make it $lastItem, lower-cased and ending with backslash. + $thisItem = $_ + if ($lastItem.Length -eq 0 -or !$thisItem.name.ToLower().StartsWith($lastItem)) + { + # Write output that serves as an exclusion for everything in this directory (including subdirectories) + Write-Output ($thisItem.name + "\*") + if (HasWritableADS($thisItem)) + { + # Write output that serves as an exclusion for any potential ADSes of this directory + Write-Output ($thisItem.name + ":*") + #Write-Verbose ("Writable ADS: " + $thisItem.name) + #Write-Verbose ("----------------------------") + } + $lastItem = $thisItem.name.ToLower() + if (!$lastItem.EndsWith("\")) { $lastItem += "\" } + } + } + } + } + + Write-Host "Removing redundancies in scan results" -ForegroundColor Cyan + RemoveRedundantLinesAndIdentifyWritableADS $windirFullXml | Out-File -Encoding ASCII $windirTxt + RemoveRedundantLinesAndIdentifyWritableADS $PfFullXml | Out-File -Encoding ASCII $PfTxt + RemoveRedundantLinesAndIdentifyWritableADS $Pf86FullXml | Out-File -Encoding ASCII $Pf86Txt +} + +#################################################################################################### +# Capture data for Exe files to blacklist if needed +#################################################################################################### +if ( $Rescan -or !(Test-Path($ExeBlacklistData) ) ) +{ + Write-Host "Processing EXE files to blacklist..." -ForegroundColor Cyan + # Get the EXE files to blacklist from the script that produces that list. + $exeFilesToBlacklist = (& $ps1_GetExeFilesToBlacklist) + # Create a hash collection for publisher information. Key on publisher name, product name, and binary name. + # Add to collection if equivalent is not already in the collection. + $pubCollection = @{} + $exeFilesToBlacklist | foreach { + $pub = (Get-AppLockerFileInformation "$_").Publisher + if ($null -ne $pub) + { + $pubKey = ($pub.PublisherName + "|" + $pub.ProductName + "|" + $pub.BinaryName).ToLower() + if (!$pubCollection.ContainsKey($pubKey)) { $pubCollection.Add($pubKey, $pub) } + } + else + { + Write-Warning "UNABLE TO BUILD BLACKLIST RULE FOR $_" + } + } + + $pubCollection.Values | + Select-Object PublisherName, ProductName, BinaryName | + ConvertTo-Csv -NoTypeInformation | + Out-File $ExeBlacklistData -Encoding unicode +} + +#################################################################################################### +# Validate that scan-result files were created +#################################################################################################### + +if ( ! ( (Test-Path($windirTxt)) -and (Test-Path($PfTxt)) -and (Test-Path($Pf86Txt)) ) ) +{ + $errMsg = "One or more of the following files is missing:`n" + + "`t" + $windirTxt + "`n" + + "`t" + $PfTxt + "`n" + + "`t" + $Pf86Txt +"`n" + Write-Error $errMsg + return +} + +if ( ! (Test-Path($ExeBlacklistData)) ) +{ + $errMsg = "The following file is missing:`n" + + "`t" + $ExeBlacklistData +"`n" + Write-Error $errMsg + return +} + +#################################################################################################### +# Process Windir and ProgramFiles directories. +#################################################################################################### + +# -------------------------------------------------------------------------------- +# Read the lists of user-writable directories with redundancies removed. +$Wr_raw_windir = (Get-Content $windirTxt) +$Wr_raw_PF = (Get-Content $PfTxt) +$Wr_raw_PF86 = (Get-Content $Pf86Txt) + +# -------------------------------------------------------------------------------- +# Process names of directories, replacing hardcoded C:\, \Windows, etc., with AppLocker variables. +# Note that System32 and SysWOW64 map to the same variable names, as do the two ProgramFiles directories. +# Add trailing backslashes to the names (e.g., C:\Windows\System32\ ), so that if there happens to be +# a "C:\Windows\System32Extra" it won't match the System32 variable. +# Note that because of the trailing backslashes, if the top directories themselves are user-writable, +# they won't turn up in the list. That by itself would be a major problem, though. +$sSystem32 = "$env:windir\System32\".ToLower() +$sSysWow64 = "$env:windir\SysWOW64\".ToLower() +$sWindir = "$env:windir\".ToLower() +$sPF86 = "${env:ProgramFiles(x86)}\".ToLower() +$sPF = "$env:ProgramFiles\".ToLower() + +# Build arrays of processed directory names with duplicates removed. (E.g., System32\Com\dmp and +# SysWOW64\Com\dmp can both be covered with a single entry.) +$Wr_windir = @() +$Wr_PF = @() + +# For the Windows list, replace matching System32, SysWOW64, and Windows paths with corresponding +# AppLocker variables, then add to collection if not already present. +$Wr_raw_windir | foreach { + $dir = $_.ToLower() + if ($dir.StartsWith($sSystem32)) { $dir = "%SYSTEM32%\" + $dir.Substring($sSystem32.Length) } + elseif ($dir.StartsWith($sSysWow64)) { $dir = "%SYSTEM32%\" + $dir.Substring($sSysWow64.Length) } + elseif ($dir.StartsWith($sWindir)) { $dir = "%WINDIR%\" + $dir.Substring($sWindir.Length) } + # Don't add the rule twice if it appears in both System32 and SysWOW64, since both map to %SYSTEM32%. + if (!$Wr_windir.Contains($dir)) + { + $Wr_windir += $dir + } +} + +# For the two ProgramFiles lists, replace top directory with AppLocker variable, then add to collection +# if not already present. +$Wr_raw_PF86 | foreach { + $dir = $_.ToLower() + if ($dir.StartsWith($sPF86)) { $dir = "%PROGRAMFILES%\" + $dir.Substring($sPF86.Length) } + $Wr_PF += $dir +} + +$Wr_raw_PF | foreach { + $dir = $_.ToLower() + if ($dir.StartsWith($sPF)) { $dir = "%PROGRAMFILES%\" + $dir.Substring($sPF.Length) } + # Possibly already added same directory from PF86; don't add again + if (!$Wr_PF.Contains($dir)) + { + $Wr_PF += $dir + } +} + +#################################################################################################### +# Load base AppLocker rules document +#################################################################################################### + +# -------------------------------------------------------------------------------- +# Build AppLocker rules starting with base document +$xDocument = [xml](Get-Content $defRulesXml) + +#################################################################################################### +# Incorporate data for EXE files to blacklist under Windir +#################################################################################################### + +# Incorporate the EXE blacklist into the document where the one PLACEHOLDER_WINDIR_EXEBLACKLIST +# placeholder is. +$xPlaceholder = $xDocument.SelectNodes("//PLACEHOLDER_WINDIR_EXEBLACKLIST")[0] +$xExcepts = $xPlaceholder.ParentNode + +$csvExeBlacklistData = (Get-Content $ExeBlacklistData | ConvertFrom-Csv) +$csvExeBlacklistData | foreach { + # Create a FilePublisherCondition element with the publisher attributes + $elem = $xDocument.CreateElement("FilePublisherCondition") + $elem.SetAttribute("PublisherName", $_.PublisherName) + $elem.SetAttribute("ProductName", $_.ProductName) + $elem.SetAttribute("BinaryName", $_.BinaryName) + # Set version number range to "any" + $elemVerRange = $xDocument.CreateElement("BinaryVersionRange") + $elemVerRange.SetAttribute("LowSection", "*") + $elemVerRange.SetAttribute("HighSection", "*") + # Add the version range to the publisher condition + $elem.AppendChild($elemVerRange) | Out-Null + # Add the publisher condition where the placeholder is + $xExcepts.AppendChild($elem) | Out-Null +} +# Remove the placeholder element +$xExcepts.RemoveChild($xPlaceholder) | Out-Null + +Write-Host "Processing additional safe paths to whitelist..." -ForegroundColor Cyan +# Get additional whitelisted paths from the script that produces that list and incorporate them into the document +$PathsToAllow = (& $ps1_GetSafePathsToAllow) +# Add "allow" for Everyone for Exe, Dll, and Script rules +$xRuleCollections = $xDocument.SelectNodes("//RuleCollection[@Type='Exe' or @Type='Script' or @Type='Dll']") +foreach($xRuleCollection in $xRuleCollections) +{ + $PathsToAllow | foreach { + # If path is an existing directory and doesn't have trailing "\*" appended, fix it so that it does. + # If path is a file, don't append \*. If the path ends with \*, no need for further validation. + # If it doesn't end with \* but Get-Item can't identify it as a file or a directory, write a warning and accept it as is. + $pathToAllow = $_ + if (!$pathToAllow.EndsWith("\*")) + { + $pathItem = Get-Item $pathToAllow -Force -ErrorAction SilentlyContinue + if ($pathItem -eq $null) + { + Write-Warning "Cannot verify path $pathItem; adding to rule set as is." + } + elseif ($pathItem -is [System.IO.DirectoryInfo]) + { + Write-Warning "Appending `"\*`" to rule for $pathToAllow" + $pathToAllow = [System.IO.Path]::Combine($pathToAllow, "*") + } + } + $elemRule = $xDocument.CreateElement("FilePathRule") + $elemRule.SetAttribute("Action", "Allow") + $elemRule.SetAttribute("UserOrGroupSid", "S-1-1-0") + $elemRule.SetAttribute("Id", [GUID]::NewGuid().Guid) + $elemRule.SetAttribute("Name", "Additional allowed path: " + $pathToAllow) + $elemRule.SetAttribute("Description", "Allows Everyone to execute from " + $pathToAllow) + $elemConditions = $xDocument.CreateElement("Conditions") + $elemCondition = $xDocument.CreateElement("FilePathCondition") + $elemCondition.SetAttribute("Path", $pathToAllow) + $elemConditions.AppendChild($elemCondition) | Out-Null + $elemRule.AppendChild($elemConditions) | Out-Null + $xRuleCollection.AppendChild($elemRule) | Out-Null + } +} + +# Incorporate path-exception rules for the user-writable directories under %windir% +# in the the EXE, DLL, and SCRIPT rules. +# Find the placeholders for Windows subdirectories, and add the path conditions there. +# Then remove the placeholders. +$xPlaceholders = $xDocument.SelectNodes("//PLACEHOLDER_WINDIR_WRITABLEDIRS") +foreach($xPlaceholder in $xPlaceholders) +{ + $xExcepts = $xPlaceholder.ParentNode + $Wr_windir | foreach { + $elem = $xDocument.CreateElement("FilePathCondition") + $elem.SetAttribute("Path", $_) + $xExcepts.AppendChild($elem) | Out-Null + } + $xExcepts.RemoveChild($xPlaceholder) | Out-Null +} + +# Incorporate path-exception rules for the user-writable directories under %PF% +# in EXE, DLL, and SCRIPT rules. +# Find the placeholders for PF subdirectories, and add the path conditions there. +# Then remove the placeholders. +$xPlaceholders = $xDocument.SelectNodes("//PLACEHOLDER_PF_WRITABLEDIRS") +foreach($xPlaceholder in $xPlaceholders) +{ + $xExcepts = $xPlaceholder.ParentNode + $Wr_PF | foreach { + $elem = $xDocument.CreateElement("FilePathCondition") + $elem.SetAttribute("Path", $_) + $xExcepts.AppendChild($elem) | Out-Null + } + $xExcepts.RemoveChild($xPlaceholder) | Out-Null +} + + +#################################################################################################### +# Begin creating dynamically-generated rule fragments. Delete old ones first. +#################################################################################################### + +# Delete previous set of dynamically-generated rules first +Remove-Item ([System.IO.Path]::Combine($mergeRulesDynamicDir, "*.xml")) + + +#################################################################################################### +# Create rules for trusted publishers +#################################################################################################### +Write-Host "Creating rules for trusted publishers..." -ForegroundColor Cyan + +# Define an empty AppLocker policy to fill, with a blank publisher rule to use as a template. +$signerPolXml = [xml]@" + + + + + + + + + + + + + + +"@ +# Get the blank publisher rule. It will be cloned to make the real publisher rules, and then this blank will be deleted. +$fprTemplate = $signerPolXml.DocumentElement.SelectNodes("//FilePublisherRule")[0] + +# Run the script that produces the signer information to process. Should come in as a sequence of hashtables. +# Each hashtable must have a label, and either an exemplar or a publisher. +# fprRulesNotEmpty: Don't generate TrustedSigners.xml if it doesn't have any rules. +$fprRulesNotEmpty = $false +$signersToBuildRulesFor = (& $ps1_TrustedSigners) +$signersToBuildRulesFor | foreach { + $label = $_.label + if ($label -eq $null) + { + # Each hashtable must have a label. + Write-Warning -Message ("Invalid syntax in $ps1_TrustedSigners. No `"label`" specified.") + } + else + { + $publisher = $product = $binaryname = "" + $filename = "" + $good = $false + # Exemplar is a file signed by the publisher we want to trust. If the hashtable specifies "useProduct" = $true, + # the AppLocker rule allows anything signed by that publisher with the same ProductName. + if ($_.exemplar) + { + $filename = $_.exemplar + $alfi = Get-AppLockerFileInformation $filename + if ($alfi -eq $null) + { + Write-Warning -Message ("Cannot get AppLockerFileInformation for $filename") + } + elseif (!($alfi.Publisher.HasPublisherName)) + { + Write-Warning -Message ("Cannot get publisher information for $filename") + } + elseif ($_.useProduct -and !($alfi.Publisher.HasProductName)) + { + Write-Warning "Cannot get product name information for $filename" + } + else + { + # Get publisher to trust, and optionally ProductName. + $publisher = $alfi.Publisher.PublisherName + if ($_.useProduct) + { + $product = $alfi.Publisher.ProductName + } + $good = $true + } + } + else + { + # Otherwise, the hashtable must specify the exact publisher to trust (and optionally ProductName, BinaryName+collection). + $publisher = $_.PublisherName + $product = $_.ProductName + $binaryName = $_.BinaryName + $fileVersion = $_.FileVersion + $ruleCollection = $_.RuleCollection + if ($null -ne $publisher) + { + $good = $true + } + else + { + # Object isn't a hashtable, or doesn't have either exemplar or PublisherName. + Write-Warning -Message ("Invalid syntax in $ps1_TrustedSigners") + } + } + + if ($good) + { + $fprRulesNotEmpty = $true + + # Duplicate the blank publisher rule, and populate it with information gathered. + $fpr = $fprTemplate.Clone() + $fpr.Conditions.FilePublisherCondition.PublisherName = $publisher + + $fpr.Name = "$label`: Signer rule for $publisher" + if ($product.Length -gt 0) + { + $fpr.Conditions.FilePublisherCondition.ProductName = $product + $fpr.Name = "$label`: Signer/product rule for $publisher/$product" + if ($binaryName.Length -gt 0) + { + $fpr.Conditions.FilePublisherCondition.BinaryName = $binaryName + $fpr.Name = "$label`: Signer/product/file rule for $publisher/$product/$binaryName" + if ($fileVersion.Length -gt 0) + { + $fpr.Conditions.FilePublisherCondition.BinaryVersionRange.LowSection = $fileVersion + } + } + } + if ($filename.Length -gt 0) + { + $fpr.Description = "Information acquired from $filename" + } + else + { + $fpr.Description = "Information acquired from $fname_TrustedSigners" + } + Write-Host ("`t" + $fpr.Name) -ForegroundColor Cyan + + if ($publisher.ToLower().Contains("microsoft") -and $product.Length -eq 0 -and ($ruleCollection.Length -eq 0 -or $ruleCollection -eq "Exe")) + { + Write-Warning -Message ("Warning: Trusting all Microsoft-signed files is an overly-broad whitelisting strategy") + } + + if ($ruleCollection) + { + $node = $signerPolXml.SelectSingleNode("//RuleCollection[@Type='" + $ruleCollection + "']") + if ($node -eq $null) + { + Write-Warning ("Couldn't find RuleCollection Type = " + $ruleCollection + " (RuleCollection is case-sensitive)") + } + else + { + $fpr.Id = [string]([GUID]::NewGuid().Guid) + $node.AppendChild($fpr) | Out-Null + } + } + else + { + # Append a copy of the new publisher rule into each rule set with a different GUID in each. + $signerPolXml.SelectNodes("//RuleCollection") | foreach { + $fpr0 = $fpr.CloneNode($true) + + $fpr0.Id = [string]([GUID]::NewGuid().Guid) + $_.AppendChild($fpr0) | Out-Null + } + } + } + } +} + +# Don't generate the file if it doesn't contain any rules +if ($fprRulesNotEmpty) +{ + # Delete the blank publisher rule from the rule set. + $fprTemplate.ParentNode.RemoveChild($fprTemplate) | Out-Null + + #$signerPolXml.OuterXml | clip + $outfile = [System.IO.Path]::Combine($mergeRulesDynamicDir, "TrustedSigners.xml") + # Save XML as Unicode + SaveXmlDocAsUnicode -xmlDoc $signerPolXml -xmlFilename $outfile +} + +#################################################################################################### +# Create custom hash rules +#################################################################################################### +Write-Host "Creating extra hash rules ..." -ForegroundColor Cyan + +# Define an empty AppLocker policy to fill, with a blank hash rule to use as a template. +$hashRuleXml = [xml]@" + + + + + + + + + + + + + + +"@ +# Get the blank hash rule. It will be cloned to make the real hash rules. +$fhrTemplate = $hashRuleXml.DocumentElement.SelectNodes("//FileHashRule")[0] +# Remove the template rule from the main document +$fhrTemplate.ParentNode.RemoveChild($fhrTemplate) | Out-Null +# fhrRulesNotEmpty: Don't generate ExtraHashRules.xml if it doesn't have any rules. +$fhrRulesNotEmpty = $false + +# Run the script that produces the hash information to process. Should come in as a sequence of hashtables. +# Each hashtable must have the following properties: +# * RuleCollection (case-sensitive) +# * RuleName +# * RuleDesc +# * HashVal (must be SHA256 with "0x" and 64 hex digits) +# * FileName +$hashRuleData = (& $ps1_HashRuleData) + +$hashRuleData | foreach { + + $fhr = $fhrTemplate.Clone() + $fhr.Id = [string]([GUID]::NewGuid().Guid) + $fhr.Name = $_.RuleName + $fhr.Description = $_.RuleDesc + $fhr.Conditions.FileHashCondition.FileHash.Data = $_.HashVal + $fhr.Conditions.FileHashCondition.FileHash.SourceFileName = $_.FileName + + $node = $hashRuleXml.SelectSingleNode("//RuleCollection[@Type='" + $_.RuleCollection + "']") + if ($node -eq $null) + { + Write-Warning ("Couldn't find RuleCollection Type = " + $_.RuleCollection + " (RuleCollection is case-sensitive)") + } + else + { + $node.AppendChild($fhr) | Out-Null + $fhrRulesNotEmpty = $true + } +} + +# Don't generate the file if it doesn't contain any rules +if ($fhrRulesNotEmpty) +{ + $outfile = [System.IO.Path]::Combine($mergeRulesDynamicDir, "ExtraHashRules.xml") + # Save XML as Unicode + SaveXmlDocAsUnicode -xmlDoc $hashRuleXml -xmlFilename $outfile +} + +#################################################################################################### +# Rules for files in user-writable directories +#################################################################################################### + +# -------------------------------------------------------------------------------- +# Helper function used to replace current username with another in paths. +function RenamePaths($paths, $forUsername) +{ + # Warning: if $forUsername is "Users" that will be a problem. + $forUsername = "\" + $forUsername + # Look for username bracketed by backslashes, or at end of the path. + $CurrentName = "\" + $env:USERNAME.ToLower() + "\" + $CurrentNameFinal = "\" + $env:USERNAME.ToLower() + + $paths | ForEach-Object { + $origTargetDir = $_ + # Temporarily remove trailing \* if present; can't GetFullPath with that. + if ($origTargetDir.EndsWith("\*")) + { + $bAppend = "\*" + $targetDir = $origTargetDir.Substring(0, $origTargetDir.Length - 2) + } + else + { + $bAppend = "" + $targetDir = $origTargetDir + } + # GetFullPath in case the provided name is 8.3-shortened. + $targetDir = [System.IO.Path]::GetFullPath($targetDir).ToLower() + if ($targetDir.Contains($CurrentName) -or $targetDir.EndsWith($CurrentNameFinal)) + { + $targetDir.Replace($CurrentNameFinal, $forUsername) + $bAppend + } + else + { + $origTargetDir + } + } +} + +# -------------------------------------------------------------------------------- +# Build rules for files in writable directories identified in the "unsafe paths to build rules for" script. +# Uses BuildRulesForFilesInWritableDirectories.ps1. +# Writes results to the dynamic merge-rules directory, using the script-supplied labels as part of the file name. +# The files in the merge-rules directories will be merged into the main document later. +# (Doing this after the other files are created in the MergeRulesDynamicDir - file naming logic handles cases where +# file already exists from the other dynamically-generated files above, or if multiple items have the same label. + +if ( !(Test-Path($ps1_UnsafePathsToBuildRulesFor)) ) +{ + $errmsg = "Script file not found: $ps1_UnsafePathsToBuildRulesFor`nNo new rules generated for files in writable directories." + Write-Warning $errmsg +} +else +{ + Write-Host "Creating rules for files in writable directories..." -ForegroundColor Cyan + $UnsafePathsToBuildRulesFor = (& $ps1_UnsafePathsToBuildRulesFor) + $UnsafePathsToBuildRulesFor | foreach { + $label = $_.label + if ($ForUser) + { + $paths = RenamePaths -paths $_.paths -forUsername $ForUser + } + else + { + $paths = $_.paths + } + $recurse = $true; + if ($null -ne $_.noRecurse) { $recurse = !$_.noRecurse } + $enforceMinFileVersion = $true + if ($null -ne $_.enforceMinVersion) { $enforceMinFileVersion = $_.enforceMinVersion } + $outfile = [System.IO.Path]::Combine($mergeRulesDynamicDir, $label + " Rules.xml") + # If it already exists, create a name that doesn't exist yet + $ixOutfile = [int]2 + while (Test-Path($outfile)) + { + $outfile = [System.IO.Path]::Combine($mergeRulesDynamicDir, $label + " (" + $ixOutfile.ToString() + ") Rules.xml") + $ixOutfile++ + } + Write-Host ("Scanning $label`:", $paths) -Separator "`n`t" -ForegroundColor Cyan + & $ps1_BuildRulesForFilesInWritableDirectories -FileSystemPaths $paths -RecurseDirectories: $recurse -EnforceMinimumVersion: $enforceMinFileVersion -RuleNamePrefix $label -OutputFileName $outfile + } +} + +#################################################################################################### +# Merging custom rules +#################################################################################################### + +# -------------------------------------------------------------------------------- +# Load the XML document with modifications into an AppLockerPolicy object +$masterPolicy = [Microsoft.Security.ApplicationId.PolicyManagement.PolicyModel.AppLockerPolicy]::FromXml($xDocument.OuterXml) + +Write-Host "Loading custom rule sets..." -ForegroundColor Cyan +# Merge any and all policy files found in the MergeRules directories, typically for authorized files in writable directories. +# Some may have been created in the previous step; others might have been dropped in from other sources. +Get-ChildItem $mergeRulesDynamicDir\*.xml, $mergeRulesStaticDir\*.xml | foreach { + $policyFileToMerge = $_ + Write-Host ("`tMerging " + $_.Directory.Name + "\" + $_.Name) -ForegroundColor Cyan + $policyToMerge = [Microsoft.Security.ApplicationId.PolicyManagement.PolicyModel.AppLockerPolicy]::Load($policyFileToMerge) + $masterPolicy.Merge($policyToMerge) +} + +#TODO: Optimize rules in rule collections here - combine/remove redundant/overlapping rules + +#################################################################################################### +# Generate final outputs +#################################################################################################### + +# Generate two versions of the rules file: one with rules enforced, and one with auditing only. + +Write-Host "Creating final rule outputs..." -ForegroundColor Cyan + +# Generate the Enforced version +foreach( $ruleCollection in $masterPolicy.RuleCollections) +{ + $ruleCollection.EnforcementMode = "Enabled" +} +SaveAppLockerPolicyAsUnicodeXml -ALPolicy $masterPolicy -xmlFilename $rulesFileEnforceNew + +# Generate the AuditOnly version +foreach( $ruleCollection in $masterPolicy.RuleCollections) +{ + $ruleCollection.EnforcementMode = "AuditOnly" +} +SaveAppLockerPolicyAsUnicodeXml -ALPolicy $masterPolicy -xmlFilename $rulesFileAuditNew + +if ($Excel) +{ + & $ps1_ExportPolicyToExcel -AppLockerXML $rulesFileEnforceNew -SaveWorkbook + & $ps1_ExportPolicyToExcel -AppLockerXML $rulesFileAuditNew -SaveWorkbook +} + +# -------------------------------------------------------------------------------- diff --git a/AaronLocker/CustomizationInputs/GetExeFilesToBlacklist.ps1 b/AaronLockerScriptBased/AaronLocker/CustomizationInputs/GetExeFilesToBlacklist.ps1 similarity index 97% rename from AaronLocker/CustomizationInputs/GetExeFilesToBlacklist.ps1 rename to AaronLockerScriptBased/AaronLocker/CustomizationInputs/GetExeFilesToBlacklist.ps1 index 3b222dc..34e67fe 100644 --- a/AaronLocker/CustomizationInputs/GetExeFilesToBlacklist.ps1 +++ b/AaronLockerScriptBased/AaronLocker/CustomizationInputs/GetExeFilesToBlacklist.ps1 @@ -1,44 +1,44 @@ -<# -.SYNOPSIS -Script used by Create-Policies.ps1 to identify EXE files that should be disallowed by AppLocker for non-admin use. Can be edited if necessary. - -.DESCRIPTION -This script outputs a list of file paths under %windir% that need to be specifically disallowed by whitelisting rules. -The list of files is consumed by Create-Policies.ps1, which builds the necessary AppLocker rules to block them. -You can edit this file as needed for your environment, although it is recommended that none of the programs -identified in this script be removed. - -Note: the solution also blocks the loading of PowerShell v2 modules - these blocks are hardcoded into the base XML file. This module -as currently designed can block only EXE files, not DLLs. -http://www.leeholmes.com/blog/2017/03/17/detecting-and-preventing-powershell-downgrade-attacks/ - -#> - -# -------------------------------------------------------------------------------- -# Files used to bypass whitelisting: - -# Find the multiple instances of .NET executables that have been identified as whitelist bypasses. -# Create-Policies.ps1 will remove redundant information. -$dotnetProgramsToBlacklist = - "InstallUtil.exe", - "IEExec.exe", - "RegAsm.exe", - "RegSvcs.exe", - "MSBuild.exe" -$dotnetProgramsToBlacklist | ForEach-Object { - Get-ChildItem -Path $env:windir\Microsoft.NET -Recurse -Include $_ | ForEach-Object { $_.FullName } -} - -"$env:windir\System32\mshta.exe" -"$env:windir\System32\PresentationHost.exe" -"$env:windir\System32\wbem\WMIC.exe" -# Note: also need Code Integrity rules to block other bypasses - -# -------------------------------------------------------------------------------- -# Files used by ransomware -"$env:windir\System32\cipher.exe" - -# -------------------------------------------------------------------------------- -# Block common credential exposure risk (also need to disable GUI option via registry, and SecondaryLogon service) -"$env:windir\System32\runas.exe" - +<# +.SYNOPSIS +Script used by Create-Policies.ps1 to identify EXE files that should be disallowed by AppLocker for non-admin use. Can be edited if necessary. + +.DESCRIPTION +This script outputs a list of file paths under %windir% that need to be specifically disallowed by whitelisting rules. +The list of files is consumed by Create-Policies.ps1, which builds the necessary AppLocker rules to block them. +You can edit this file as needed for your environment, although it is recommended that none of the programs +identified in this script be removed. + +Note: the solution also blocks the loading of PowerShell v2 modules - these blocks are hardcoded into the base XML file. This module +as currently designed can block only EXE files, not DLLs. +http://www.leeholmes.com/blog/2017/03/17/detecting-and-preventing-powershell-downgrade-attacks/ + +#> + +# -------------------------------------------------------------------------------- +# Files used to bypass whitelisting: + +# Find the multiple instances of .NET executables that have been identified as whitelist bypasses. +# Create-Policies.ps1 will remove redundant information. +$dotnetProgramsToBlacklist = + "InstallUtil.exe", + "IEExec.exe", + "RegAsm.exe", + "RegSvcs.exe", + "MSBuild.exe" +$dotnetProgramsToBlacklist | ForEach-Object { + Get-ChildItem -Path $env:windir\Microsoft.NET -Recurse -Include $_ | ForEach-Object { $_.FullName } +} + +"$env:windir\System32\mshta.exe" +"$env:windir\System32\PresentationHost.exe" +"$env:windir\System32\wbem\WMIC.exe" +# Note: also need Code Integrity rules to block other bypasses + +# -------------------------------------------------------------------------------- +# Files used by ransomware +"$env:windir\System32\cipher.exe" + +# -------------------------------------------------------------------------------- +# Block common credential exposure risk (also need to disable GUI option via registry, and SecondaryLogon service) +"$env:windir\System32\runas.exe" + diff --git a/AaronLocker/CustomizationInputs/GetSafePathsToAllow.ps1 b/AaronLockerScriptBased/AaronLocker/CustomizationInputs/GetSafePathsToAllow.ps1 similarity index 97% rename from AaronLocker/CustomizationInputs/GetSafePathsToAllow.ps1 rename to AaronLockerScriptBased/AaronLocker/CustomizationInputs/GetSafePathsToAllow.ps1 index c011375..27ff30f 100644 --- a/AaronLocker/CustomizationInputs/GetSafePathsToAllow.ps1 +++ b/AaronLockerScriptBased/AaronLocker/CustomizationInputs/GetSafePathsToAllow.ps1 @@ -1,57 +1,57 @@ -<# -.SYNOPSIS -Customizable script used by Create-Policies.ps1 that produces a list of additional "safe" paths to allow for non-admin execution. - -.DESCRIPTION -This script outputs a simple list of directories that can be considered "safe" for non-admins to execute programs from. -The list is consumed by Create-Policies.ps1, which incorporates the paths into AppLocker rules allowing execution of -EXE, DLL, and Script files. -NOTE: DIRECTORY/FILE PATHS IDENTIFIED IN THIS SCRIPT MUST NOT BE WRITABLE BY NON-ADMIN USERS!!! -You can edit this file as needed for your environment. - -Note that each directory name must be followed by \*, as in these examples: - "C:\ProgramData\App-V\*" - "\\MYSERVER\Apps\*" -Individual files can be allowed by path, also. Do not end those with "\*" - -Specify paths using only fixed local drive letters or UNC paths. Do not use mapped drive letters or -SUBST drive letters, as the user can change their definitions. If X: is mapped to the read-only -\\MYSERVER\Apps file share, and you allow execution in \\MYSERVER\Apps\*, the user can run MyProgram.exe -in that share whether it is referenced as \\MYSERVER\Apps\MyProgram.exe or as X:\MyProgram.exe. Similarly, -AppLocker does the right thing with SUBSTed drive letters. - -TODO: At some point, reimplement with hashtable output supporting "label" and "RuleCollection" properties so that path rules have more descriptive names, and can be applied to specific rule collections> - -#> - -# Add the standard domain controller GPO file shares for the computer's AD domain, and if different, for the user account's domain. -# Needed to allow execution of user logon/logoff scripts. (Computer startup/shutdown scripts run as System and don't need special rules.) -# As an alternative, just output the paths explicitly; e.g., "\\corp.contoso.com\netlogon\*" -# Note that if logon scripts invoke other scripts/programs using an explicit \\DC\netlogon\ syntax, these rules won't cover them. Need -# explicit rules naming domain controllers. (I know that sucks.) -$cs = Get-CimInstance -ClassName CIM_ComputerSystem -if ($null -ne $cs) -{ - if ($cs.PartOfDomain) - { - $computerDomain = $cs.Domain - "\\$computerDomain\netlogon\*" - "\\$computerDomain\sysvol\*" - $userDomain = $env:USERDNSDOMAIN - if ($null -ne $userDomain -and $userDomain.ToUpper() -ne $computerDomain.ToUpper()) - { - "\\$userDomain\netlogon\*" - "\\$userDomain\sysvol\*" - } - } - else - { - Write-Host "Computer is not domain-joined; not adding path for DC shares." -ForegroundColor Cyan - } -} - -### Windows Defender put their binaries in ProgramData for a while. Comment this back out when they move it back. -"%OSDRIVE%\PROGRAMDATA\MICROSOFT\WINDOWS DEFENDER\PLATFORM\*" - -# Windows upgrade -'C:\$WINDOWS.~BT\Sources\*' +<# +.SYNOPSIS +Customizable script used by Create-Policies.ps1 that produces a list of additional "safe" paths to allow for non-admin execution. + +.DESCRIPTION +This script outputs a simple list of directories that can be considered "safe" for non-admins to execute programs from. +The list is consumed by Create-Policies.ps1, which incorporates the paths into AppLocker rules allowing execution of +EXE, DLL, and Script files. +NOTE: DIRECTORY/FILE PATHS IDENTIFIED IN THIS SCRIPT MUST NOT BE WRITABLE BY NON-ADMIN USERS!!! +You can edit this file as needed for your environment. + +Note that each directory name must be followed by \*, as in these examples: + "C:\ProgramData\App-V\*" + "\\MYSERVER\Apps\*" +Individual files can be allowed by path, also. Do not end those with "\*" + +Specify paths using only fixed local drive letters or UNC paths. Do not use mapped drive letters or +SUBST drive letters, as the user can change their definitions. If X: is mapped to the read-only +\\MYSERVER\Apps file share, and you allow execution in \\MYSERVER\Apps\*, the user can run MyProgram.exe +in that share whether it is referenced as \\MYSERVER\Apps\MyProgram.exe or as X:\MyProgram.exe. Similarly, +AppLocker does the right thing with SUBSTed drive letters. + +TODO: At some point, reimplement with hashtable output supporting "label" and "RuleCollection" properties so that path rules have more descriptive names, and can be applied to specific rule collections> + +#> + +# Add the standard domain controller GPO file shares for the computer's AD domain, and if different, for the user account's domain. +# Needed to allow execution of user logon/logoff scripts. (Computer startup/shutdown scripts run as System and don't need special rules.) +# As an alternative, just output the paths explicitly; e.g., "\\corp.contoso.com\netlogon\*" +# Note that if logon scripts invoke other scripts/programs using an explicit \\DC\netlogon\ syntax, these rules won't cover them. Need +# explicit rules naming domain controllers. (I know that sucks.) +$cs = Get-CimInstance -ClassName CIM_ComputerSystem +if ($null -ne $cs) +{ + if ($cs.PartOfDomain) + { + $computerDomain = $cs.Domain + "\\$computerDomain\netlogon\*" + "\\$computerDomain\sysvol\*" + $userDomain = $env:USERDNSDOMAIN + if ($null -ne $userDomain -and $userDomain.ToUpper() -ne $computerDomain.ToUpper()) + { + "\\$userDomain\netlogon\*" + "\\$userDomain\sysvol\*" + } + } + else + { + Write-Host "Computer is not domain-joined; not adding path for DC shares." -ForegroundColor Cyan + } +} + +### Windows Defender put their binaries in ProgramData for a while. Comment this back out when they move it back. +"%OSDRIVE%\PROGRAMDATA\MICROSOFT\WINDOWS DEFENDER\PLATFORM\*" + +# Windows upgrade +'C:\$WINDOWS.~BT\Sources\*' diff --git a/AaronLocker/CustomizationInputs/HashRuleData.ps1 b/AaronLockerScriptBased/AaronLocker/CustomizationInputs/HashRuleData.ps1 similarity index 96% rename from AaronLocker/CustomizationInputs/HashRuleData.ps1 rename to AaronLockerScriptBased/AaronLocker/CustomizationInputs/HashRuleData.ps1 index c6aafa8..191f563 100644 --- a/AaronLocker/CustomizationInputs/HashRuleData.ps1 +++ b/AaronLockerScriptBased/AaronLocker/CustomizationInputs/HashRuleData.ps1 @@ -1,30 +1,30 @@ -<# -.SYNOPSIS -Script used to define hash rules without direct access to the files. - -.DESCRIPTION -This script outputs zero or more hashtables containing information to define hash rules. -It supports creating hash rules based on AppLocker event data rather than on direct access to the files. - -Each hashtable must have each of the following properties: -* RuleCollection -* RuleName -* RuleDesc -* HashVal -* FileName - -NOTES: -* RuleCollection must be one of "Exe", "Dll", "Script", or "Msi", and is CASE-SENSITIVE. -* HashVal must be "0x" followed by 64 hex digits (SHA256 hash). - -Example: - -@{ -RuleCollection = "Script"; -RuleName = "Contoso Products: DoGoodStuff.cmd - HASH RULE"; -RuleDesc = "Identified in: %LOCALAPPDATA%\TEMP\DoGoodStuff.cmd"; -HashVal = "0x4CA1CD60FBFBA42C00EA6EA1B56BEFE6AD90FE0EFF58285A75D77B515D864DAE"; -FileName = "DoGoodStuff.cmd" -} - -#> +<# +.SYNOPSIS +Script used to define hash rules without direct access to the files. + +.DESCRIPTION +This script outputs zero or more hashtables containing information to define hash rules. +It supports creating hash rules based on AppLocker event data rather than on direct access to the files. + +Each hashtable must have each of the following properties: +* RuleCollection +* RuleName +* RuleDesc +* HashVal +* FileName + +NOTES: +* RuleCollection must be one of "Exe", "Dll", "Script", or "Msi", and is CASE-SENSITIVE. +* HashVal must be "0x" followed by 64 hex digits (SHA256 hash). + +Example: + +@{ +RuleCollection = "Script"; +RuleName = "Contoso Products: DoGoodStuff.cmd - HASH RULE"; +RuleDesc = "Identified in: %LOCALAPPDATA%\TEMP\DoGoodStuff.cmd"; +HashVal = "0x4CA1CD60FBFBA42C00EA6EA1B56BEFE6AD90FE0EFF58285A75D77B515D864DAE"; +FileName = "DoGoodStuff.cmd" +} + +#> diff --git a/AaronLocker/CustomizationInputs/KnownAdmins.ps1 b/AaronLockerScriptBased/AaronLocker/CustomizationInputs/KnownAdmins.ps1 similarity index 97% rename from AaronLocker/CustomizationInputs/KnownAdmins.ps1 rename to AaronLockerScriptBased/AaronLocker/CustomizationInputs/KnownAdmins.ps1 index 32d6bce..b5d1891 100644 --- a/AaronLocker/CustomizationInputs/KnownAdmins.ps1 +++ b/AaronLockerScriptBased/AaronLocker/CustomizationInputs/KnownAdmins.ps1 @@ -1,23 +1,23 @@ -<# -.SYNOPSIS -Outputs a list of known administrative users or groups that should be ignored when scanning for "user-writable" directories. - -.DESCRIPTION -Outputs a list of zero or more administrative users or groups that Enum-WritableDirs.ps1 does not know about (e.g., custom domain or local groups or users), one to a line. - -The script framework scans for "user-writable" directories, looking for "write" permissions and ignoring permissions granted -to "known administrative" users and groups. The framework might fail to recognize custom domain groups and (in some cases) -local user accounts as administrative. This script enables adding those entities to the list of known administrative users/groups. -Output one entity name or SID per line. - -Examples where this might be needed: -* Custom domain groups that have administrative rights. -* On Azure Active Directory joined systems, enumeration of BUILTIN\Administrators might not work correctly - might need to enumerate administrative accounts explicitly. - -Examples: - - "DESKTOP-7TPCJ7J\renamedAdmin" - "CONTOSO\SCCM-Admins" - -#> - +<# +.SYNOPSIS +Outputs a list of known administrative users or groups that should be ignored when scanning for "user-writable" directories. + +.DESCRIPTION +Outputs a list of zero or more administrative users or groups that Enum-WritableDirs.ps1 does not know about (e.g., custom domain or local groups or users), one to a line. + +The script framework scans for "user-writable" directories, looking for "write" permissions and ignoring permissions granted +to "known administrative" users and groups. The framework might fail to recognize custom domain groups and (in some cases) +local user accounts as administrative. This script enables adding those entities to the list of known administrative users/groups. +Output one entity name or SID per line. + +Examples where this might be needed: +* Custom domain groups that have administrative rights. +* On Azure Active Directory joined systems, enumeration of BUILTIN\Administrators might not work correctly - might need to enumerate administrative accounts explicitly. + +Examples: + + "DESKTOP-7TPCJ7J\renamedAdmin" + "CONTOSO\SCCM-Admins" + +#> + diff --git a/AaronLocker/CustomizationInputs/TrustedSigners-MsvcMfc.ps1 b/AaronLockerScriptBased/AaronLocker/CustomizationInputs/TrustedSigners-MsvcMfc.ps1 similarity index 96% rename from AaronLocker/CustomizationInputs/TrustedSigners-MsvcMfc.ps1 rename to AaronLockerScriptBased/AaronLocker/CustomizationInputs/TrustedSigners-MsvcMfc.ps1 index f7480e4..fed4758 100644 --- a/AaronLocker/CustomizationInputs/TrustedSigners-MsvcMfc.ps1 +++ b/AaronLockerScriptBased/AaronLocker/CustomizationInputs/TrustedSigners-MsvcMfc.ps1 @@ -1,200 +1,200 @@ -<# -.SYNOPSIS -Script designed to be dot-sourced into TrustedSigners.ps1 that supports the creation of publisher rules for observed MSVC*.DLL and MFC*.DLL files. - -.DESCRIPTION -There are already MSVC* and MFC* DLLs in Windows - this script also allows redistributable DLLs that often ship with other products and are installed into user-writable directories. -This output allows any version of signed MSVC* or MFC* DLLs that shipped with a known version of Visual Studio. -This is not the same as allowing anything signed by Microsoft or is part of Visual Studio - just the runtime library support DLLs. - -This file can be updated as additional MSVC* and MFC* DLLs appear in event logs when observed executing from user-writable directories. -Add more files as they are identified. - -See TrustedSigners.ps1 for details about how this input is used. - -#> - -########################################################################### -# Visual Studio 2005 -########################################################################### - -@{ -label = "MSVC runtime DLL"; -RuleCollection = "Dll"; -PublisherName = "O=MICROSOFT CORPORATION, L=REDMOND, S=WASHINGTON, C=US"; -ProductName = "MICROSOFT® VISUAL STUDIO® 2005"; -BinaryName = "MSVCP80.DLL"; -} - -@{ -label = "MSVC runtime DLL"; -RuleCollection = "Dll"; -PublisherName = "O=MICROSOFT CORPORATION, L=REDMOND, S=WASHINGTON, C=US"; -ProductName = "MICROSOFT® VISUAL STUDIO® 2005"; -BinaryName = "MSVCR80.DLL"; -} - -########################################################################### -# Visual Studio 2008 -########################################################################### - -@{ -label = "MFC runtime DLL"; -RuleCollection = "Dll"; -PublisherName = "O=MICROSOFT CORPORATION, L=REDMOND, S=WASHINGTON, C=US"; -ProductName = "MICROSOFT® VISUAL STUDIO® 2008"; -BinaryName = "MFC90U.DLL"; -} - -@{ -label = "MSVC runtime DLL"; -RuleCollection = "Dll"; -PublisherName = "O=MICROSOFT CORPORATION, L=REDMOND, S=WASHINGTON, C=US"; -ProductName = "MICROSOFT® VISUAL STUDIO® 2008"; -BinaryName = "MSVCP90.DLL"; -} - -@{ -label = "MSVC runtime DLL"; -RuleCollection = "Dll"; -PublisherName = "O=MICROSOFT CORPORATION, L=REDMOND, S=WASHINGTON, C=US"; -ProductName = "MICROSOFT® VISUAL STUDIO® 2008"; -BinaryName = "MSVCR90.DLL"; -} - -########################################################################### -# Visual Studio 2010 -########################################################################### - -@{ -label = "MSVC runtime DLL"; -RuleCollection = "Dll"; -PublisherName = "O=MICROSOFT CORPORATION, L=REDMOND, S=WASHINGTON, C=US"; -ProductName = "MICROSOFT® VISUAL STUDIO® 2010"; -BinaryName = "MSVCP100.DLL"; -} - -@{ -label = "MSVC runtime DLL"; -RuleCollection = "Dll"; -PublisherName = "O=MICROSOFT CORPORATION, L=REDMOND, S=WASHINGTON, C=US"; -ProductName = "MICROSOFT® VISUAL STUDIO® 2010"; -BinaryName = "MSVCR100_CLR0400.DLL"; -} - -########################################################################### -# Visual Studio 2012 -########################################################################### - -@{ -label = "MFC runtime DLL"; -RuleCollection = "Dll"; -PublisherName = "O=MICROSOFT CORPORATION, L=REDMOND, S=WASHINGTON, C=US"; -ProductName = "MICROSOFT® VISUAL STUDIO® 2012"; -BinaryName = "MFC110.DLL"; -} - -@{ -label = "MSVC runtime DLL"; -RuleCollection = "Dll"; -PublisherName = "O=MICROSOFT CORPORATION, L=REDMOND, S=WASHINGTON, C=US"; -ProductName = "MICROSOFT® VISUAL STUDIO® 2012"; -BinaryName = "MSVCP110.DLL"; -} - -@{ -label = "MSVC runtime DLL"; -RuleCollection = "Dll"; -PublisherName = "O=MICROSOFT CORPORATION, L=REDMOND, S=WASHINGTON, C=US"; -ProductName = "MICROSOFT® VISUAL STUDIO® 2012"; -BinaryName = "MSVCR110.DLL"; -} - -########################################################################### -# Visual Studio 2013 -########################################################################### - -@{ -label = "MFC runtime DLL"; -RuleCollection = "Dll"; -PublisherName = "O=MICROSOFT CORPORATION, L=REDMOND, S=WASHINGTON, C=US"; -ProductName = "MICROSOFT® VISUAL STUDIO® 2013"; -BinaryName = "MFC120.DLL"; -} - -@{ -label = "MFC runtime DLL"; -RuleCollection = "Dll"; -PublisherName = "O=MICROSOFT CORPORATION, L=REDMOND, S=WASHINGTON, C=US"; -ProductName = "MICROSOFT® VISUAL STUDIO® 2013"; -BinaryName = "MFC120U.DLL"; -} - -@{ -label = "MSVC runtime DLL"; -RuleCollection = "Dll"; -PublisherName = "O=MICROSOFT CORPORATION, L=REDMOND, S=WASHINGTON, C=US"; -ProductName = "MICROSOFT® VISUAL STUDIO® 2013"; -BinaryName = "MSVCP120.DLL"; -} - -@{ -label = "MSVC runtime DLL"; -RuleCollection = "Dll"; -PublisherName = "O=MICROSOFT CORPORATION, L=REDMOND, S=WASHINGTON, C=US"; -ProductName = "MICROSOFT® VISUAL STUDIO® 2013"; -BinaryName = "MSVCR120.DLL"; -} - -########################################################################### -# Visual Studio 2015 -########################################################################### - -@{ -label = "MSVC runtime DLL"; -RuleCollection = "Dll"; -PublisherName = "O=MICROSOFT CORPORATION, L=REDMOND, S=WASHINGTON, C=US"; -ProductName = "MICROSOFT® VISUAL STUDIO® 2015"; -BinaryName = "MSVCP140.DLL"; -} - -@{ -label = "MSVC runtime DLL"; -RuleCollection = "Dll"; -PublisherName = "O=MICROSOFT CORPORATION, L=REDMOND, S=WASHINGTON, C=US"; -ProductName = "MICROSOFT® VISUAL STUDIO® 2015"; -BinaryName = "VCRUNTIME140.DLL"; -} - -########################################################################### -# Visual Studio 2017 -########################################################################### - -@{ -label = "MSVC runtime DLL"; -RuleCollection = "Dll"; -PublisherName = "O=MICROSOFT CORPORATION, L=REDMOND, S=WASHINGTON, C=US"; -ProductName = "MICROSOFT® VISUAL STUDIO® 2017"; -BinaryName = "MSVCP140.DLL"; -} - -@{ -label = "MSVC runtime DLL"; -RuleCollection = "Dll"; -PublisherName = "O=MICROSOFT CORPORATION, L=REDMOND, S=WASHINGTON, C=US"; -ProductName = "MICROSOFT® VISUAL STUDIO® 2017"; -BinaryName = "VCRUNTIME140.DLL"; -} - -########################################################################### -# Visual Studio 10 -########################################################################### - -@{ -label = "MFC runtime DLL"; -RuleCollection = "Dll"; -PublisherName = "O=MICROSOFT CORPORATION, L=REDMOND, S=WASHINGTON, C=US"; -ProductName = "MICROSOFT® VISUAL STUDIO® 10"; -BinaryName = "MFC100U.DLL"; -} - +<# +.SYNOPSIS +Script designed to be dot-sourced into TrustedSigners.ps1 that supports the creation of publisher rules for observed MSVC*.DLL and MFC*.DLL files. + +.DESCRIPTION +There are already MSVC* and MFC* DLLs in Windows - this script also allows redistributable DLLs that often ship with other products and are installed into user-writable directories. +This output allows any version of signed MSVC* or MFC* DLLs that shipped with a known version of Visual Studio. +This is not the same as allowing anything signed by Microsoft or is part of Visual Studio - just the runtime library support DLLs. + +This file can be updated as additional MSVC* and MFC* DLLs appear in event logs when observed executing from user-writable directories. +Add more files as they are identified. + +See TrustedSigners.ps1 for details about how this input is used. + +#> + +########################################################################### +# Visual Studio 2005 +########################################################################### + +@{ +label = "MSVC runtime DLL"; +RuleCollection = "Dll"; +PublisherName = "O=MICROSOFT CORPORATION, L=REDMOND, S=WASHINGTON, C=US"; +ProductName = "MICROSOFT® VISUAL STUDIO® 2005"; +BinaryName = "MSVCP80.DLL"; +} + +@{ +label = "MSVC runtime DLL"; +RuleCollection = "Dll"; +PublisherName = "O=MICROSOFT CORPORATION, L=REDMOND, S=WASHINGTON, C=US"; +ProductName = "MICROSOFT® VISUAL STUDIO® 2005"; +BinaryName = "MSVCR80.DLL"; +} + +########################################################################### +# Visual Studio 2008 +########################################################################### + +@{ +label = "MFC runtime DLL"; +RuleCollection = "Dll"; +PublisherName = "O=MICROSOFT CORPORATION, L=REDMOND, S=WASHINGTON, C=US"; +ProductName = "MICROSOFT® VISUAL STUDIO® 2008"; +BinaryName = "MFC90U.DLL"; +} + +@{ +label = "MSVC runtime DLL"; +RuleCollection = "Dll"; +PublisherName = "O=MICROSOFT CORPORATION, L=REDMOND, S=WASHINGTON, C=US"; +ProductName = "MICROSOFT® VISUAL STUDIO® 2008"; +BinaryName = "MSVCP90.DLL"; +} + +@{ +label = "MSVC runtime DLL"; +RuleCollection = "Dll"; +PublisherName = "O=MICROSOFT CORPORATION, L=REDMOND, S=WASHINGTON, C=US"; +ProductName = "MICROSOFT® VISUAL STUDIO® 2008"; +BinaryName = "MSVCR90.DLL"; +} + +########################################################################### +# Visual Studio 2010 +########################################################################### + +@{ +label = "MSVC runtime DLL"; +RuleCollection = "Dll"; +PublisherName = "O=MICROSOFT CORPORATION, L=REDMOND, S=WASHINGTON, C=US"; +ProductName = "MICROSOFT® VISUAL STUDIO® 2010"; +BinaryName = "MSVCP100.DLL"; +} + +@{ +label = "MSVC runtime DLL"; +RuleCollection = "Dll"; +PublisherName = "O=MICROSOFT CORPORATION, L=REDMOND, S=WASHINGTON, C=US"; +ProductName = "MICROSOFT® VISUAL STUDIO® 2010"; +BinaryName = "MSVCR100_CLR0400.DLL"; +} + +########################################################################### +# Visual Studio 2012 +########################################################################### + +@{ +label = "MFC runtime DLL"; +RuleCollection = "Dll"; +PublisherName = "O=MICROSOFT CORPORATION, L=REDMOND, S=WASHINGTON, C=US"; +ProductName = "MICROSOFT® VISUAL STUDIO® 2012"; +BinaryName = "MFC110.DLL"; +} + +@{ +label = "MSVC runtime DLL"; +RuleCollection = "Dll"; +PublisherName = "O=MICROSOFT CORPORATION, L=REDMOND, S=WASHINGTON, C=US"; +ProductName = "MICROSOFT® VISUAL STUDIO® 2012"; +BinaryName = "MSVCP110.DLL"; +} + +@{ +label = "MSVC runtime DLL"; +RuleCollection = "Dll"; +PublisherName = "O=MICROSOFT CORPORATION, L=REDMOND, S=WASHINGTON, C=US"; +ProductName = "MICROSOFT® VISUAL STUDIO® 2012"; +BinaryName = "MSVCR110.DLL"; +} + +########################################################################### +# Visual Studio 2013 +########################################################################### + +@{ +label = "MFC runtime DLL"; +RuleCollection = "Dll"; +PublisherName = "O=MICROSOFT CORPORATION, L=REDMOND, S=WASHINGTON, C=US"; +ProductName = "MICROSOFT® VISUAL STUDIO® 2013"; +BinaryName = "MFC120.DLL"; +} + +@{ +label = "MFC runtime DLL"; +RuleCollection = "Dll"; +PublisherName = "O=MICROSOFT CORPORATION, L=REDMOND, S=WASHINGTON, C=US"; +ProductName = "MICROSOFT® VISUAL STUDIO® 2013"; +BinaryName = "MFC120U.DLL"; +} + +@{ +label = "MSVC runtime DLL"; +RuleCollection = "Dll"; +PublisherName = "O=MICROSOFT CORPORATION, L=REDMOND, S=WASHINGTON, C=US"; +ProductName = "MICROSOFT® VISUAL STUDIO® 2013"; +BinaryName = "MSVCP120.DLL"; +} + +@{ +label = "MSVC runtime DLL"; +RuleCollection = "Dll"; +PublisherName = "O=MICROSOFT CORPORATION, L=REDMOND, S=WASHINGTON, C=US"; +ProductName = "MICROSOFT® VISUAL STUDIO® 2013"; +BinaryName = "MSVCR120.DLL"; +} + +########################################################################### +# Visual Studio 2015 +########################################################################### + +@{ +label = "MSVC runtime DLL"; +RuleCollection = "Dll"; +PublisherName = "O=MICROSOFT CORPORATION, L=REDMOND, S=WASHINGTON, C=US"; +ProductName = "MICROSOFT® VISUAL STUDIO® 2015"; +BinaryName = "MSVCP140.DLL"; +} + +@{ +label = "MSVC runtime DLL"; +RuleCollection = "Dll"; +PublisherName = "O=MICROSOFT CORPORATION, L=REDMOND, S=WASHINGTON, C=US"; +ProductName = "MICROSOFT® VISUAL STUDIO® 2015"; +BinaryName = "VCRUNTIME140.DLL"; +} + +########################################################################### +# Visual Studio 2017 +########################################################################### + +@{ +label = "MSVC runtime DLL"; +RuleCollection = "Dll"; +PublisherName = "O=MICROSOFT CORPORATION, L=REDMOND, S=WASHINGTON, C=US"; +ProductName = "MICROSOFT® VISUAL STUDIO® 2017"; +BinaryName = "MSVCP140.DLL"; +} + +@{ +label = "MSVC runtime DLL"; +RuleCollection = "Dll"; +PublisherName = "O=MICROSOFT CORPORATION, L=REDMOND, S=WASHINGTON, C=US"; +ProductName = "MICROSOFT® VISUAL STUDIO® 2017"; +BinaryName = "VCRUNTIME140.DLL"; +} + +########################################################################### +# Visual Studio 10 +########################################################################### + +@{ +label = "MFC runtime DLL"; +RuleCollection = "Dll"; +PublisherName = "O=MICROSOFT CORPORATION, L=REDMOND, S=WASHINGTON, C=US"; +ProductName = "MICROSOFT® VISUAL STUDIO® 10"; +BinaryName = "MFC100U.DLL"; +} + diff --git a/AaronLocker/CustomizationInputs/TrustedSigners.ps1 b/AaronLockerScriptBased/AaronLocker/CustomizationInputs/TrustedSigners.ps1 similarity index 97% rename from AaronLocker/CustomizationInputs/TrustedSigners.ps1 rename to AaronLockerScriptBased/AaronLocker/CustomizationInputs/TrustedSigners.ps1 index 5fc9592..ab9bf8b 100644 --- a/AaronLocker/CustomizationInputs/TrustedSigners.ps1 +++ b/AaronLockerScriptBased/AaronLocker/CustomizationInputs/TrustedSigners.ps1 @@ -1,114 +1,114 @@ -<# -.SYNOPSIS -Customizable script used by Create-Policies.ps1 that identifies publishers or publisher+product/file combinations to trust. - -.DESCRIPTION -TrustedSigners.ps1 outputs a sequence of hashtables that specify a label, and either a literal publisher name, or a path to a signed file to use as an example. - -Each hashtable has a "label" property that is incorporated into the rule name and description. - -Each hashtable also has either a "PublisherName" or an "exemplar" property: -* "PublisherName" is a literal canonical name identifying a publisher to trust. - When using PublisherName, you can also add optional properties: - * "ProductName", to restrict trust just to that product by that publisher; with "ProductName" you can also add "BinaryName" to restrict to a specific internal file name, - and optionally then "FileVersion" as well to specify a minimum allowed file version. - When using BinaryName, you should also specify an explicit RuleCollection, to reduce the number of rules. (E.g., no sense in having a Script rule allowing "MSVCP80.DLL".) - * "RuleCollection", to apply the trust only within a single RuleCollection. RuleCollection must be one of "Exe", "Dll", "Script", or "Msi", and it is CASE-SENSITIVE. -* "exemplar" is the path to a signed file; the publisher to trust is extracted from that signature. When using exemplar, you can also add an optional "useProduct" boolean value indicating whether to restrict publisher trust only to that file's product name. If "useProduct" is not specified, all files signed by the publisher are trusted. - -Examples showing possible combinations: - - # Trust everything by a specific publisher - @{ - label = "Trust all Contoso"; - PublisherName = "O=CONTOSO, L=SEATTLE, S=WASHINGTON, C=US"; - } - - # Trust all DLLs by a specific publisher - @{ - label = "Trust all Contoso DLLs"; - PublisherName = "O=CONTOSO, L=SEATTLE, S=WASHINGTON, C=US"; - RuleCollection = "Dll"; - } - - # Trust a specific product published by a specific publisher - @{ - label = "Trust all CUSTOMAPP files published by Contoso"; - PublisherName = "O=CONTOSO, L=SEATTLE, S=WASHINGTON, C=US"; - ProductName = "CUSTOMAPP"; - } - - # Trust any version of a specific signed file by a specific publisher/product - # RuleCollection must be one of Exe, Dll, Script, or Msi, and is CASE-SENSITIVE - @{ - label = "Trust Contoso's SAMPLE.DLL in CUSTOMAPP"; - PublisherName = "O=CONTOSO, L=SEATTLE, S=WASHINGTON, C=US"; - ProductName = "CUSTOMAPP"; - BinaryName = "SAMPLE.DLL"; - FileVersion = "10.0.15063.0"; - RuleCollection = "Dll"; - } - - # Trust everything signed by the same publisher as the exemplar file (Autoruns.exe) - @{ - label = "Trust the publisher of Autoruns.exe"; - exemplar = "C:\Program Files\Sysinternals\Autoruns.exe"; - } - - # Trust everything with the same publisher and product as the exemplar file (LuaBuglight.exe) - @{ - label = "Trust everything with the same publisher and product as LuaBuglight.exe"; - exemplar = "C:\Program Files\Utils\LuaBuglight.exe"; - useProduct = $true - } -#> - -@{ -# Allow Microsoft-signed OneDrive EXE and DLL files with the OneDrive product name; -# This rule doesn't cover all of OneDrive's files because they include files from other products (Visual Studio, QT5, etc.) -label = "Microsoft OneDrive (partial)"; -PublisherName = "O=MICROSOFT CORPORATION, L=REDMOND, S=WASHINGTON, C=US"; -ProductName = "MICROSOFT ONEDRIVE"; -} - -@{ -label = "Microsoft-signed MSI files"; -RuleCollection = "Msi"; -PublisherName = "O=MICROSOFT CORPORATION, L=REDMOND, S=WASHINGTON, C=US"; -} - -@{ -# Windows' built-in troubleshooting often involves running Microsoft-signed scripts in the user's profile -label = "Microsoft-signed script files"; -RuleCollection = "Script"; -PublisherName = "O=MICROSOFT CORPORATION, L=REDMOND, S=WASHINGTON, C=US"; -} - -# Uncomment this block if Google Chrome is installed to ProgramFiles. -# Google Chrome runs some code in the user profile even when Chrome is installed to Program Files. -# This creates publisher rules that allow those components to run. -<# - @{ - label = "Google Chrome SWReporter tool"; - RuleCollection = "Exe"; - PublisherName = "O=GOOGLE INC, L=MOUNTAIN VIEW, S=CALIFORNIA, C=US"; - ProductName = "SOFTWARE REPORTER TOOL"; - BinaryName = "SOFTWARE_REPORTER_TOOL.EXE"; - } - @{ - label = "Google Chrome Cleanup"; - RuleCollection = "Dll"; - PublisherName = "O=ESET, SPOL. S R.O., L=BRATISLAVA, S=SLOVAKIA, C=SK"; - ProductName = "CHROME CLEANUP"; - } - @{ - label = "Google Chrome Protector"; - RuleCollection = "Dll"; - PublisherName = "O=ESET, SPOL. S R.O., L=BRATISLAVA, S=SLOVAKIA, C=SK"; - ProductName = "CHROME PROTECTOR"; - } -#> - -# Allow MSVC/MFC redistributable DLLs. Dot-source the MSVC/MFC DLL include file in this directory -. ([System.IO.Path]::Combine( [System.IO.Path]::GetDirectoryName($MyInvocation.MyCommand.Path), "TrustedSigners-MsvcMfc.ps1")) - +<# +.SYNOPSIS +Customizable script used by Create-Policies.ps1 that identifies publishers or publisher+product/file combinations to trust. + +.DESCRIPTION +TrustedSigners.ps1 outputs a sequence of hashtables that specify a label, and either a literal publisher name, or a path to a signed file to use as an example. + +Each hashtable has a "label" property that is incorporated into the rule name and description. + +Each hashtable also has either a "PublisherName" or an "exemplar" property: +* "PublisherName" is a literal canonical name identifying a publisher to trust. + When using PublisherName, you can also add optional properties: + * "ProductName", to restrict trust just to that product by that publisher; with "ProductName" you can also add "BinaryName" to restrict to a specific internal file name, + and optionally then "FileVersion" as well to specify a minimum allowed file version. + When using BinaryName, you should also specify an explicit RuleCollection, to reduce the number of rules. (E.g., no sense in having a Script rule allowing "MSVCP80.DLL".) + * "RuleCollection", to apply the trust only within a single RuleCollection. RuleCollection must be one of "Exe", "Dll", "Script", or "Msi", and it is CASE-SENSITIVE. +* "exemplar" is the path to a signed file; the publisher to trust is extracted from that signature. When using exemplar, you can also add an optional "useProduct" boolean value indicating whether to restrict publisher trust only to that file's product name. If "useProduct" is not specified, all files signed by the publisher are trusted. + +Examples showing possible combinations: + + # Trust everything by a specific publisher + @{ + label = "Trust all Contoso"; + PublisherName = "O=CONTOSO, L=SEATTLE, S=WASHINGTON, C=US"; + } + + # Trust all DLLs by a specific publisher + @{ + label = "Trust all Contoso DLLs"; + PublisherName = "O=CONTOSO, L=SEATTLE, S=WASHINGTON, C=US"; + RuleCollection = "Dll"; + } + + # Trust a specific product published by a specific publisher + @{ + label = "Trust all CUSTOMAPP files published by Contoso"; + PublisherName = "O=CONTOSO, L=SEATTLE, S=WASHINGTON, C=US"; + ProductName = "CUSTOMAPP"; + } + + # Trust any version of a specific signed file by a specific publisher/product + # RuleCollection must be one of Exe, Dll, Script, or Msi, and is CASE-SENSITIVE + @{ + label = "Trust Contoso's SAMPLE.DLL in CUSTOMAPP"; + PublisherName = "O=CONTOSO, L=SEATTLE, S=WASHINGTON, C=US"; + ProductName = "CUSTOMAPP"; + BinaryName = "SAMPLE.DLL"; + FileVersion = "10.0.15063.0"; + RuleCollection = "Dll"; + } + + # Trust everything signed by the same publisher as the exemplar file (Autoruns.exe) + @{ + label = "Trust the publisher of Autoruns.exe"; + exemplar = "C:\Program Files\Sysinternals\Autoruns.exe"; + } + + # Trust everything with the same publisher and product as the exemplar file (LuaBuglight.exe) + @{ + label = "Trust everything with the same publisher and product as LuaBuglight.exe"; + exemplar = "C:\Program Files\Utils\LuaBuglight.exe"; + useProduct = $true + } +#> + +@{ +# Allow Microsoft-signed OneDrive EXE and DLL files with the OneDrive product name; +# This rule doesn't cover all of OneDrive's files because they include files from other products (Visual Studio, QT5, etc.) +label = "Microsoft OneDrive (partial)"; +PublisherName = "O=MICROSOFT CORPORATION, L=REDMOND, S=WASHINGTON, C=US"; +ProductName = "MICROSOFT ONEDRIVE"; +} + +@{ +label = "Microsoft-signed MSI files"; +RuleCollection = "Msi"; +PublisherName = "O=MICROSOFT CORPORATION, L=REDMOND, S=WASHINGTON, C=US"; +} + +@{ +# Windows' built-in troubleshooting often involves running Microsoft-signed scripts in the user's profile +label = "Microsoft-signed script files"; +RuleCollection = "Script"; +PublisherName = "O=MICROSOFT CORPORATION, L=REDMOND, S=WASHINGTON, C=US"; +} + +# Uncomment this block if Google Chrome is installed to ProgramFiles. +# Google Chrome runs some code in the user profile even when Chrome is installed to Program Files. +# This creates publisher rules that allow those components to run. +<# + @{ + label = "Google Chrome SWReporter tool"; + RuleCollection = "Exe"; + PublisherName = "O=GOOGLE INC, L=MOUNTAIN VIEW, S=CALIFORNIA, C=US"; + ProductName = "SOFTWARE REPORTER TOOL"; + BinaryName = "SOFTWARE_REPORTER_TOOL.EXE"; + } + @{ + label = "Google Chrome Cleanup"; + RuleCollection = "Dll"; + PublisherName = "O=ESET, SPOL. S R.O., L=BRATISLAVA, S=SLOVAKIA, C=SK"; + ProductName = "CHROME CLEANUP"; + } + @{ + label = "Google Chrome Protector"; + RuleCollection = "Dll"; + PublisherName = "O=ESET, SPOL. S R.O., L=BRATISLAVA, S=SLOVAKIA, C=SK"; + ProductName = "CHROME PROTECTOR"; + } +#> + +# Allow MSVC/MFC redistributable DLLs. Dot-source the MSVC/MFC DLL include file in this directory +. ([System.IO.Path]::Combine( [System.IO.Path]::GetDirectoryName($MyInvocation.MyCommand.Path), "TrustedSigners-MsvcMfc.ps1")) + diff --git a/AaronLocker/CustomizationInputs/UnsafePathsToBuildRulesFor.ps1 b/AaronLockerScriptBased/AaronLocker/CustomizationInputs/UnsafePathsToBuildRulesFor.ps1 similarity index 97% rename from AaronLocker/CustomizationInputs/UnsafePathsToBuildRulesFor.ps1 rename to AaronLockerScriptBased/AaronLocker/CustomizationInputs/UnsafePathsToBuildRulesFor.ps1 index b082404..0b8f0e3 100644 --- a/AaronLocker/CustomizationInputs/UnsafePathsToBuildRulesFor.ps1 +++ b/AaronLockerScriptBased/AaronLocker/CustomizationInputs/UnsafePathsToBuildRulesFor.ps1 @@ -1,54 +1,54 @@ -<# -.SYNOPSIS -Customizable script used by Create-Policies.ps1 that identifies user-writable paths containing files that need to be allowed to execute. - -.DESCRIPTION -This script outputs a sequence of hashtables that identify user-writable files or directory paths containing content that users must be allowed to execute. -(The scripts favor publisher rules over hash rules.) -Each hashtable must include "label" and "paths" properties, with additional optional properties. -Hashtable properties: -* label - REQUIRED; incorporated into rules' names and descriptions. -* paths - REQUIRED; identifies one or more paths (comma separated if more than one). - If a path is a directory, rules are generated for the existing files in that directory. - If a path is to a file, a rule is generated for that file. -* noRecurse - OPTIONAL; if specified, rules are generated only for the files in the specified directory or directories. - Otherwise, rules are also generated for files in subdirectories of the specified directory or directories. -* enforceMinVersion - OPTIONAL; if specified, generated publisher rules enforce a minimum file version based on the file versions of the observed files. - Otherwise, the generated rules do not enforce a minimum file version. - -Examples of valid hash tables: - - # Search one directory and its subdirectories for files to generate rules for. Don't include file version in generated publisher rules. - @{ - label = "OneDrive"; - paths = "$env:LOCALAPPDATA\Microsoft\OneDrive"; - enforceMinVersion = $false - } - - - # Search two separate directory structures for files to generate rules for, plus one explicitly-identified file. - @{ - label = "ContosoIT"; - paths = "$env:LOCALAPPDATA\Programs\MyContosoIT\Helper", - "C:\ProgramData\COntosoIT\ContosoIT System Health Client", - "$env:LOCALAPPDATA\TEMP\CORPSEC\ITGSECLOGONGPEXEC.EXE" - } - - # Generate rules for three distinct files; do not recurse subdirectories looking for additional matches. - @{ - label = "Custom backup scripts"; - paths = "C:\Backups\MyBackup.vbs", - "C:\Backups\MyPersonalBackup.vbs", - "C:\Backups\Exports\RegExport.1.cmd"; - noRecurse = $true - } -#> - -@{ -label = "OneDrive"; -paths = "$env:LOCALAPPDATA\Microsoft\OneDrive"; -enforceMinVersion = $false; -} - - - +<# +.SYNOPSIS +Customizable script used by Create-Policies.ps1 that identifies user-writable paths containing files that need to be allowed to execute. + +.DESCRIPTION +This script outputs a sequence of hashtables that identify user-writable files or directory paths containing content that users must be allowed to execute. +(The scripts favor publisher rules over hash rules.) +Each hashtable must include "label" and "paths" properties, with additional optional properties. +Hashtable properties: +* label - REQUIRED; incorporated into rules' names and descriptions. +* paths - REQUIRED; identifies one or more paths (comma separated if more than one). + If a path is a directory, rules are generated for the existing files in that directory. + If a path is to a file, a rule is generated for that file. +* noRecurse - OPTIONAL; if specified, rules are generated only for the files in the specified directory or directories. + Otherwise, rules are also generated for files in subdirectories of the specified directory or directories. +* enforceMinVersion - OPTIONAL; if specified, generated publisher rules enforce a minimum file version based on the file versions of the observed files. + Otherwise, the generated rules do not enforce a minimum file version. + +Examples of valid hash tables: + + # Search one directory and its subdirectories for files to generate rules for. Don't include file version in generated publisher rules. + @{ + label = "OneDrive"; + paths = "$env:LOCALAPPDATA\Microsoft\OneDrive"; + enforceMinVersion = $false + } + + + # Search two separate directory structures for files to generate rules for, plus one explicitly-identified file. + @{ + label = "ContosoIT"; + paths = "$env:LOCALAPPDATA\Programs\MyContosoIT\Helper", + "C:\ProgramData\COntosoIT\ContosoIT System Health Client", + "$env:LOCALAPPDATA\TEMP\CORPSEC\ITGSECLOGONGPEXEC.EXE" + } + + # Generate rules for three distinct files; do not recurse subdirectories looking for additional matches. + @{ + label = "Custom backup scripts"; + paths = "C:\Backups\MyBackup.vbs", + "C:\Backups\MyPersonalBackup.vbs", + "C:\Backups\Exports\RegExport.1.cmd"; + noRecurse = $true + } +#> + +@{ +label = "OneDrive"; +paths = "$env:LOCALAPPDATA\Microsoft\OneDrive"; +enforceMinVersion = $false; +} + + + diff --git a/AaronLocker/ExportPolicy-ToExcel.ps1 b/AaronLockerScriptBased/AaronLocker/ExportPolicy-ToExcel.ps1 similarity index 97% rename from AaronLocker/ExportPolicy-ToExcel.ps1 rename to AaronLockerScriptBased/AaronLocker/ExportPolicy-ToExcel.ps1 index ce93778..cc49757 100644 --- a/AaronLocker/ExportPolicy-ToExcel.ps1 +++ b/AaronLockerScriptBased/AaronLocker/ExportPolicy-ToExcel.ps1 @@ -1,138 +1,138 @@ -<# -.SYNOPSIS -Turns AppLocker policy into a more human-readable Excel worksheet. - -.DESCRIPTION -The script gets AppLocker policy from one of four sources, imports it into a new Excel instance, and formats it. - -The four source options are: -* Current effective policy (default behavior -- use no parameters); -* Current local policy (use -Local switch); -* Exported AppLocker policy in an XML file (use -AppLockerXML parameter with file path); -* Output previously captured from ExportPolicy-ToCsv.ps1 (use -AppLockerCSV with file path); - -This script depends on ExportPolicy-ToCsv.ps1, which should be in the Support subdirectory. -It also depends on Microsoft Excel's being installed. - -The three command line options (-Local, -AppLockerXML, -AppLockerCSV) are mutually exclusive: only one can be used at a time. - -.PARAMETER Local -If this switch is specified, the script processes the computer's local AppLocker policy. -If no parameters are specified or this switch is set to -Local:$false, the script processes the computer's effective AppLocker policy. - -.PARAMETER AppLockerXML -If this parameter is specified, AppLocker policy is read from the specified exported XML policy file. - -.PARAMETER AppLockerCSV -If this parameter is specified, AppLocker policy is read from the specified CSV file previously created from ExportPolicy-ToCsv.ps1 output. - -.PARAMETER SaveWorkbook -If set, saves workbook to same directory as input file with same file name and default Excel file extension. - -.EXAMPLE -.\ExportPolicy-ToExcel.ps1 - -Generates an Excel worksheet representing the computer's effective AppLocker policy. - -.EXAMPLE -.\Support\ExportPolicy-ToCsv.ps1 | Out-File .\AppLocker.csv; .\ExportPolicy-ToExcel.ps1 -AppLockerCSV .\AppLocker.csv - -Generates an Excel worksheet representing AppLocker policy previously generated from ExportPolicy-ToCsv.ps1 output. - -.EXAMPLE -Get-AppLockerPolicy -Local -Xml | Out-File .\AppLocker.xml; .\ExportPolicy-ToExcel.ps1 -AppLockerXML .\AppLocker.xml - -Generates an Excel worksheet representing AppLocker policy exported from a system into an XML file. - -#> - -#TODO: Add option to get AppLocker policy from AD GPO, if/when ExportPolicy-ToCsv.ps1 adds it. - -[CmdletBinding(DefaultParameterSetName="LocalPolicy")] -param( - # If specified, inspects local AppLocker policy rather than effective policy or an XML file - [parameter(ParameterSetName="LocalPolicy")] - [switch] - $Local = $false, - - # Optional: path to XML file containing AppLocker policy - [parameter(ParameterSetName="SavedXML")] - [String] - $AppLockerXML, - - # If specified, uses CSV previously collected instead of running ExportPolicy-ToCsv.ps1 - [parameter(ParameterSetName="SavedCSV")] - [String] - $AppLockerCSV, - - [parameter(ParameterSetName="SavedXML")] - [parameter(ParameterSetName="SavedCSV")] - [switch] - $SaveWorkbook -) - -$rootDir = [System.IO.Path]::GetDirectoryName($MyInvocation.MyCommand.Path) -# Get configuration settings and global functions from .\Support\Config.ps1) -# Dot-source the config file. -. $rootDir\Support\Config.ps1 - -$OutputEncodingPrevious = $OutputEncoding -$OutputEncoding = [System.Text.ASCIIEncoding]::Unicode - - -$tabname = "AppLocker policy" -$filename = $tempfile = $xlFname = [String]::Empty - -$linebreakSeq = "^|^" - -if ($AppLockerCSV.Length -gt 0) -{ - $filename = $AppLockerCSV - $tabname = [System.IO.Path]::GetFileName($AppLockerCSV) - if ($SaveWorkbook) - { - $xlFname = [System.IO.Path]::ChangeExtension($AppLockerCSV, ".xlsx") - } -} -else -{ - $filename = $tempfile = [System.IO.Path]::GetTempFileName() - - if ($AppLockerXML.Length -gt 0) - { - & $ps1_ExportPolicyToCSV -AppLockerPolicyFile $AppLockerXML -linebreakSeq $linebreakSeq | Out-File $tempfile -Encoding unicode - $tabname = [System.IO.Path]::GetFileNameWithoutExtension($AppLockerXML) - if ($SaveWorkbook) - { - $xlFname = [System.IO.Path]::ChangeExtension($AppLockerXML, ".xlsx") - } - } - else - { - & $ps1_ExportPolicyToCSV -Local:$Local -linebreakSeq $linebreakSeq | Out-File $tempfile -Encoding unicode - if ($Local) - { - $tabname = "AppLocker policy - Local" - } - else - { - $tabname = "AppLocker policy - Effective" - } - } -} - -if ($xlFname.Length -gt 0) -{ - # Ensure absolute path - if (!([System.IO.Path]::IsPathRooted($xlFname))) - { - $xlFname = [System.IO.Path]::Combine((Get-Location).Path, $xlFname) - } -} - -CreateExcelFromCsvFile $filename $tabname $linebreakSeq $xlFname - -# Delete the temp file -if ($tempfile.Length -gt 0) { Remove-Item $tempfile } - -$OutputEncoding = $OutputEncodingPrevious +<# +.SYNOPSIS +Turns AppLocker policy into a more human-readable Excel worksheet. + +.DESCRIPTION +The script gets AppLocker policy from one of four sources, imports it into a new Excel instance, and formats it. + +The four source options are: +* Current effective policy (default behavior -- use no parameters); +* Current local policy (use -Local switch); +* Exported AppLocker policy in an XML file (use -AppLockerXML parameter with file path); +* Output previously captured from ExportPolicy-ToCsv.ps1 (use -AppLockerCSV with file path); + +This script depends on ExportPolicy-ToCsv.ps1, which should be in the Support subdirectory. +It also depends on Microsoft Excel's being installed. + +The three command line options (-Local, -AppLockerXML, -AppLockerCSV) are mutually exclusive: only one can be used at a time. + +.PARAMETER Local +If this switch is specified, the script processes the computer's local AppLocker policy. +If no parameters are specified or this switch is set to -Local:$false, the script processes the computer's effective AppLocker policy. + +.PARAMETER AppLockerXML +If this parameter is specified, AppLocker policy is read from the specified exported XML policy file. + +.PARAMETER AppLockerCSV +If this parameter is specified, AppLocker policy is read from the specified CSV file previously created from ExportPolicy-ToCsv.ps1 output. + +.PARAMETER SaveWorkbook +If set, saves workbook to same directory as input file with same file name and default Excel file extension. + +.EXAMPLE +.\ExportPolicy-ToExcel.ps1 + +Generates an Excel worksheet representing the computer's effective AppLocker policy. + +.EXAMPLE +.\Support\ExportPolicy-ToCsv.ps1 | Out-File .\AppLocker.csv; .\ExportPolicy-ToExcel.ps1 -AppLockerCSV .\AppLocker.csv + +Generates an Excel worksheet representing AppLocker policy previously generated from ExportPolicy-ToCsv.ps1 output. + +.EXAMPLE +Get-AppLockerPolicy -Local -Xml | Out-File .\AppLocker.xml; .\ExportPolicy-ToExcel.ps1 -AppLockerXML .\AppLocker.xml + +Generates an Excel worksheet representing AppLocker policy exported from a system into an XML file. + +#> + +#TODO: Add option to get AppLocker policy from AD GPO, if/when ExportPolicy-ToCsv.ps1 adds it. + +[CmdletBinding(DefaultParameterSetName="LocalPolicy")] +param( + # If specified, inspects local AppLocker policy rather than effective policy or an XML file + [parameter(ParameterSetName="LocalPolicy")] + [switch] + $Local = $false, + + # Optional: path to XML file containing AppLocker policy + [parameter(ParameterSetName="SavedXML")] + [String] + $AppLockerXML, + + # If specified, uses CSV previously collected instead of running ExportPolicy-ToCsv.ps1 + [parameter(ParameterSetName="SavedCSV")] + [String] + $AppLockerCSV, + + [parameter(ParameterSetName="SavedXML")] + [parameter(ParameterSetName="SavedCSV")] + [switch] + $SaveWorkbook +) + +$rootDir = [System.IO.Path]::GetDirectoryName($MyInvocation.MyCommand.Path) +# Get configuration settings and global functions from .\Support\Config.ps1) +# Dot-source the config file. +. $rootDir\Support\Config.ps1 + +$OutputEncodingPrevious = $OutputEncoding +$OutputEncoding = [System.Text.ASCIIEncoding]::Unicode + + +$tabname = "AppLocker policy" +$filename = $tempfile = $xlFname = [String]::Empty + +$linebreakSeq = "^|^" + +if ($AppLockerCSV.Length -gt 0) +{ + $filename = $AppLockerCSV + $tabname = [System.IO.Path]::GetFileName($AppLockerCSV) + if ($SaveWorkbook) + { + $xlFname = [System.IO.Path]::ChangeExtension($AppLockerCSV, ".xlsx") + } +} +else +{ + $filename = $tempfile = [System.IO.Path]::GetTempFileName() + + if ($AppLockerXML.Length -gt 0) + { + & $ps1_ExportPolicyToCSV -AppLockerPolicyFile $AppLockerXML -linebreakSeq $linebreakSeq | Out-File $tempfile -Encoding unicode + $tabname = [System.IO.Path]::GetFileNameWithoutExtension($AppLockerXML) + if ($SaveWorkbook) + { + $xlFname = [System.IO.Path]::ChangeExtension($AppLockerXML, ".xlsx") + } + } + else + { + & $ps1_ExportPolicyToCSV -Local:$Local -linebreakSeq $linebreakSeq | Out-File $tempfile -Encoding unicode + if ($Local) + { + $tabname = "AppLocker policy - Local" + } + else + { + $tabname = "AppLocker policy - Effective" + } + } +} + +if ($xlFname.Length -gt 0) +{ + # Ensure absolute path + if (!([System.IO.Path]::IsPathRooted($xlFname))) + { + $xlFname = [System.IO.Path]::Combine((Get-Location).Path, $xlFname) + } +} + +CreateExcelFromCsvFile $filename $tabname $linebreakSeq $xlFname + +# Delete the temp file +if ($tempfile.Length -gt 0) { Remove-Item $tempfile } + +$OutputEncoding = $OutputEncodingPrevious diff --git a/AaronLocker/Generate-EventWorkbook.ps1 b/AaronLockerScriptBased/AaronLocker/Generate-EventWorkbook.ps1 similarity index 98% rename from AaronLocker/Generate-EventWorkbook.ps1 rename to AaronLockerScriptBased/AaronLocker/Generate-EventWorkbook.ps1 index adf849b..71cf343 100644 --- a/AaronLocker/Generate-EventWorkbook.ps1 +++ b/AaronLockerScriptBased/AaronLocker/Generate-EventWorkbook.ps1 @@ -1,166 +1,166 @@ -<# -.SYNOPSIS -Produces a multi-tab Excel workbook containing summary and details of AppLocker events to support advanced analysis. - -.DESCRIPTION -Converts the saved output from the Get-AppLockerEvents.ps1 or Save-WEFEvents.ps1 scripts to a multi-tab Excel workbook supporting numerous views of the data, including: -* Summary tab showing date/time ranges of the reported events and other summary information. -* List of machines reporting events, and the number of events per machine. -* List of publishers of signed files appearing in events, and the number of events per publisher. -* All combinations of publishers/products for signed files in events. -* All combinations of publishers/products and generic file paths ("generic" meaning that user-specific paths are replaced with %LOCALAPPDATA%, %USERPROFILE%, etc., as appropriate). -* Paths of unsigned files, with filename alone, file type, and file hash. -* Files grouped by user. -* Full details from Get-AppLockerEvents.ps1. -These separate tabs enable quick determination of the files running afoul of AppLocker rules and help quickly determine whether/how to adjust the rules. - -.PARAMETER AppLockerEventsCsvFile -Path to CSV file produced by Get-AppLockerEvents.ps1 or Save-WEFEvents.ps1, ideally without any attributes removed, but must contain at least these: MachineName, PublisherName, ProductName, GenericPath, GenericDir, FileName, FileType, Hash - -.PARAMETER SaveWorkbook -If set, saves workbook to same directory as input file with same file name and default Excel file extension. -#> - - -param( - # Path to CSV file produced by Get-AppLockerEvents.ps1 - [parameter(Mandatory=$true)] - [String] - $AppLockerEventsCsvFile, - - [switch] - $SaveWorkbook -) - -if (!(Test-Path($AppLockerEventsCsvFile))) -{ - Write-Warning "File not found: $AppLockerEventsCsvFile" - return -} - -# Get absolute path to input file. (Note that [System.IO.Path]::GetFullName doesn't do this...) -$AppLockerEventsCsvFileFullPath = $AppLockerEventsCsvFile -if (!([System.IO.Path]::IsPathRooted($AppLockerEventsCsvFile))) -{ - $AppLockerEventsCsvFileFullPath = [System.IO.Path]::Combine((Get-Location).Path, $AppLockerEventsCsvFile) -} - -$rootDir = [System.IO.Path]::GetDirectoryName($MyInvocation.MyCommand.Path) -# Get configuration settings and global functions from .\Support\Config.ps1) -# Dot-source the config file. Contains Excel-generation scripts. -. $rootDir\Support\Config.ps1 - -$OutputEncodingPrevious = $OutputEncoding -$OutputEncoding = [System.Text.ASCIIEncoding]::Unicode - -# String constant -$sFiltered = "FILTERED" - -if (CreateExcelApplication) -{ - Write-Host "Reading data from $AppLockerEventsCsvFile" -ForegroundColor Cyan - $csvFull = @(Get-Content $AppLockerEventsCsvFile) - #Write-Host "Converting to CSV" -ForegroundColor Yellow - $dataUnfiltered = @($csvFull | ConvertFrom-Csv -Delimiter "`t") - #Write-Host "Getting filtered events" -ForegroundColor Yellow - $dataFiltered = @($dataUnfiltered | Where-Object { $_.EventType -ne $sFiltered }) - #Write-Host "Getting signed events" -ForegroundColor Yellow - $eventsSigned = @($dataFiltered | Where-Object { $_.PublisherName -ne "-" }) - #Write-Host "Getting unsigned events" -ForegroundColor Yellow - $eventsUnsigned = @($dataFiltered | Where-Object { $_.PublisherName -eq "-" }) - - # Lines of text for the summary page - $tabname = "Summary" - Write-Host "Gathering data for `"$tabname`"..." -ForegroundColor Cyan - $text = @() - $dtsort = ($dataFiltered.EventTime | Sort-Object); - $text += "Summary information" - $text += "" - $text += "Data source:`t" + [System.IO.Path]::GetFileName($AppLockerEventsCsvFile) - $text += "First event:`t" + ([datetime]($dtsort[0])).ToString() - $text += "Last event:`t" + ([datetime]($dtsort[$dtsort.Length - 1])).ToString() - $text += "Number of events:`t" + $dataFiltered.Count.ToString() - $text += "Number of signed-file events:`t" + $eventsSigned.Count.ToString() - $text += "Number of unsigned-file events:`t" + $eventsUnsigned.Count.ToString() - # Make sure the result of the pipe is an array, even if only one item. - $text += "Number of machines reporting events:`t" + ( @() + ($dataUnfiltered.MachineName | Group-Object)).Count.ToString() - AddWorksheetFromText -text $text -tabname $tabname - - # Events per machine: - $tabname = "Machines and event counts" - Write-Host "Gathering data for `"$tabname`"..." -ForegroundColor Cyan - $csv = ($dataFiltered.MachineName | Group-Object | Select-Object Name, Count | Sort-Object Name | ConvertTo-Csv -Delimiter "`t" -NoTypeInformation) - $csv += ($dataUnfiltered | Where-Object { $_.EventType -eq $sFiltered } | ForEach-Object { $_.MachineName + "`t0" }) - AddWorksheetFromCsvData -csv $csv -tabname $tabname -CrLfEncoded "" - - # Counts of each publisher: - $tabname = "Publishers and event counts" - Write-Host "Gathering data for `"$tabname`"..." -ForegroundColor Cyan - $csv = ($dataFiltered.PublisherName | Group-Object | Select-Object Name, Count | Sort-Object Name | ConvertTo-Csv -Delimiter "`t" -NoTypeInformation) - AddWorksheetFromCsvData -csv $csv -tabname $tabname - - # Publisher/product combinations: - $tabname = "Publisher-product combinations" - Write-Host "Gathering data for `"$tabname`"..." -ForegroundColor Cyan - $csv = ($eventsSigned | Select-Object PublisherName, ProductName | Sort-Object PublisherName, ProductName -Unique | ConvertTo-Csv -Delimiter "`t" -NoTypeInformation) - AddWorksheetFromCsvData -csv $csv -tabname $tabname - - # Publisher/product/file combinations: - $tabname = "Signed file info" - Write-Host "Gathering data for `"$tabname`"..." -ForegroundColor Cyan - $csv = ($eventsSigned | Select-Object PublisherName, ProductName, GenericPath, FileName, FileType | Sort-Object PublisherName, ProductName, GenericPath -Unique | ConvertTo-Csv -Delimiter "`t" -NoTypeInformation) - AddWorksheetFromCsvData -csv $csv -tabname $tabname - - # # Publisher/product/directory combinations: - # $tabname = "Signed file info (dir only)" - # Write-Host "Gathering data for `"$tabname`"..." -ForegroundColor Cyan - # $csv = ($eventsSigned | Select-Object PublisherName, ProductName, GenericDir, FileType | Sort-Object PublisherName, ProductName, GenericDir -Unique | ConvertTo-Csv -Delimiter "`t" -NoTypeInformation) - # AddWorksheetFromCsvData -csv $csv -tabname $tabname - - # Analysis of unsigned files: - $tabname = "Unsigned file info" - Write-Host "Gathering data for `"$tabname`"..." -ForegroundColor Cyan - $csv = ($eventsUnsigned | Select-Object GenericPath, FileName, FileType, Hash | Sort-Object GenericPath -Unique | ConvertTo-Csv -Delimiter "`t" -NoTypeInformation) - AddWorksheetFromCsvData -csv $csv -tabname $tabname - - # # Analysis of unsigned files (dir only): - # $tabname = "Dirs of unsigned files" - # Write-Host "Gathering data for `"$tabname`"..." -ForegroundColor Cyan - # $csv = ($eventsUnsigned | Select-Object GenericDir | Sort-Object GenericDir -Unique | ConvertTo-Csv -Delimiter "`t" -NoTypeInformation) - # AddWorksheetFromCsvData -csv $csv -tabname $tabname - - # Events per user: - $tabname = "Users and event counts" - Write-Host "Gathering data for `"$tabname`"..." -ForegroundColor Cyan - $csv = ($dataFiltered.UserName | Group-Object | Select-Object Name, Count | Sort-Object Name | ConvertTo-Csv -Delimiter "`t" -NoTypeInformation) - AddWorksheetFromCsvData -csv $csv -tabname $tabname -CrLfEncoded "" - - # Per-user details - $tabname = "Files by user" - Write-Host "Gathering data for `"$tabname`"..." -ForegroundColor Cyan - $csv = ($dataFiltered | Select-Object UserName, GenericPath, PublisherName, ProductName | Sort-Object UserName, GenericPath, PublisherName, ProductName -Unique | ConvertTo-Csv -Delimiter "`t" -NoTypeInformation) - AddWorksheetFromCsvData -csv $csv -tabname $tabname - - # Per-user details - $tabname = "Files by user (details)" - Write-Host "Gathering data for `"$tabname`"..." -ForegroundColor Cyan - $csv = ($dataFiltered | Select-Object UserName, MachineName, EventTimeXL, GenericPath, PublisherName, ProductName | Sort-Object UserName, MachineName, EventTimeXL, GenericPath, PublisherName, ProductName -Unique | ConvertTo-Csv -Delimiter "`t" -NoTypeInformation) - AddWorksheetFromCsvData -csv $csv -tabname $tabname - - # All event data - AddWorksheetFromCsvFile -filename $AppLockerEventsCsvFileFullPath -tabname "Full details" - - SelectFirstWorksheet - - if ($SaveWorkbook) - { - $xlFname = [System.IO.Path]::ChangeExtension($AppLockerEventsCsvFileFullPath, ".xlsx") - SaveWorkbook -filename $xlFname - } - - ReleaseExcelApplication -} - -$OutputEncoding = $OutputEncodingPrevious - - +<# +.SYNOPSIS +Produces a multi-tab Excel workbook containing summary and details of AppLocker events to support advanced analysis. + +.DESCRIPTION +Converts the saved output from the Get-AppLockerEvents.ps1 or Save-WEFEvents.ps1 scripts to a multi-tab Excel workbook supporting numerous views of the data, including: +* Summary tab showing date/time ranges of the reported events and other summary information. +* List of machines reporting events, and the number of events per machine. +* List of publishers of signed files appearing in events, and the number of events per publisher. +* All combinations of publishers/products for signed files in events. +* All combinations of publishers/products and generic file paths ("generic" meaning that user-specific paths are replaced with %LOCALAPPDATA%, %USERPROFILE%, etc., as appropriate). +* Paths of unsigned files, with filename alone, file type, and file hash. +* Files grouped by user. +* Full details from Get-AppLockerEvents.ps1. +These separate tabs enable quick determination of the files running afoul of AppLocker rules and help quickly determine whether/how to adjust the rules. + +.PARAMETER AppLockerEventsCsvFile +Path to CSV file produced by Get-AppLockerEvents.ps1 or Save-WEFEvents.ps1, ideally without any attributes removed, but must contain at least these: MachineName, PublisherName, ProductName, GenericPath, GenericDir, FileName, FileType, Hash + +.PARAMETER SaveWorkbook +If set, saves workbook to same directory as input file with same file name and default Excel file extension. +#> + + +param( + # Path to CSV file produced by Get-AppLockerEvents.ps1 + [parameter(Mandatory=$true)] + [String] + $AppLockerEventsCsvFile, + + [switch] + $SaveWorkbook +) + +if (!(Test-Path($AppLockerEventsCsvFile))) +{ + Write-Warning "File not found: $AppLockerEventsCsvFile" + return +} + +# Get absolute path to input file. (Note that [System.IO.Path]::GetFullName doesn't do this...) +$AppLockerEventsCsvFileFullPath = $AppLockerEventsCsvFile +if (!([System.IO.Path]::IsPathRooted($AppLockerEventsCsvFile))) +{ + $AppLockerEventsCsvFileFullPath = [System.IO.Path]::Combine((Get-Location).Path, $AppLockerEventsCsvFile) +} + +$rootDir = [System.IO.Path]::GetDirectoryName($MyInvocation.MyCommand.Path) +# Get configuration settings and global functions from .\Support\Config.ps1) +# Dot-source the config file. Contains Excel-generation scripts. +. $rootDir\Support\Config.ps1 + +$OutputEncodingPrevious = $OutputEncoding +$OutputEncoding = [System.Text.ASCIIEncoding]::Unicode + +# String constant +$sFiltered = "FILTERED" + +if (CreateExcelApplication) +{ + Write-Host "Reading data from $AppLockerEventsCsvFile" -ForegroundColor Cyan + $csvFull = @(Get-Content $AppLockerEventsCsvFile) + #Write-Host "Converting to CSV" -ForegroundColor Yellow + $dataUnfiltered = @($csvFull | ConvertFrom-Csv -Delimiter "`t") + #Write-Host "Getting filtered events" -ForegroundColor Yellow + $dataFiltered = @($dataUnfiltered | Where-Object { $_.EventType -ne $sFiltered }) + #Write-Host "Getting signed events" -ForegroundColor Yellow + $eventsSigned = @($dataFiltered | Where-Object { $_.PublisherName -ne "-" }) + #Write-Host "Getting unsigned events" -ForegroundColor Yellow + $eventsUnsigned = @($dataFiltered | Where-Object { $_.PublisherName -eq "-" }) + + # Lines of text for the summary page + $tabname = "Summary" + Write-Host "Gathering data for `"$tabname`"..." -ForegroundColor Cyan + $text = @() + $dtsort = ($dataFiltered.EventTime | Sort-Object); + $text += "Summary information" + $text += "" + $text += "Data source:`t" + [System.IO.Path]::GetFileName($AppLockerEventsCsvFile) + $text += "First event:`t" + ([datetime]($dtsort[0])).ToString() + $text += "Last event:`t" + ([datetime]($dtsort[$dtsort.Length - 1])).ToString() + $text += "Number of events:`t" + $dataFiltered.Count.ToString() + $text += "Number of signed-file events:`t" + $eventsSigned.Count.ToString() + $text += "Number of unsigned-file events:`t" + $eventsUnsigned.Count.ToString() + # Make sure the result of the pipe is an array, even if only one item. + $text += "Number of machines reporting events:`t" + ( @() + ($dataUnfiltered.MachineName | Group-Object)).Count.ToString() + AddWorksheetFromText -text $text -tabname $tabname + + # Events per machine: + $tabname = "Machines and event counts" + Write-Host "Gathering data for `"$tabname`"..." -ForegroundColor Cyan + $csv = ($dataFiltered.MachineName | Group-Object | Select-Object Name, Count | Sort-Object Name | ConvertTo-Csv -Delimiter "`t" -NoTypeInformation) + $csv += ($dataUnfiltered | Where-Object { $_.EventType -eq $sFiltered } | ForEach-Object { $_.MachineName + "`t0" }) + AddWorksheetFromCsvData -csv $csv -tabname $tabname -CrLfEncoded "" + + # Counts of each publisher: + $tabname = "Publishers and event counts" + Write-Host "Gathering data for `"$tabname`"..." -ForegroundColor Cyan + $csv = ($dataFiltered.PublisherName | Group-Object | Select-Object Name, Count | Sort-Object Name | ConvertTo-Csv -Delimiter "`t" -NoTypeInformation) + AddWorksheetFromCsvData -csv $csv -tabname $tabname + + # Publisher/product combinations: + $tabname = "Publisher-product combinations" + Write-Host "Gathering data for `"$tabname`"..." -ForegroundColor Cyan + $csv = ($eventsSigned | Select-Object PublisherName, ProductName | Sort-Object PublisherName, ProductName -Unique | ConvertTo-Csv -Delimiter "`t" -NoTypeInformation) + AddWorksheetFromCsvData -csv $csv -tabname $tabname + + # Publisher/product/file combinations: + $tabname = "Signed file info" + Write-Host "Gathering data for `"$tabname`"..." -ForegroundColor Cyan + $csv = ($eventsSigned | Select-Object PublisherName, ProductName, GenericPath, FileName, FileType | Sort-Object PublisherName, ProductName, GenericPath -Unique | ConvertTo-Csv -Delimiter "`t" -NoTypeInformation) + AddWorksheetFromCsvData -csv $csv -tabname $tabname + + # # Publisher/product/directory combinations: + # $tabname = "Signed file info (dir only)" + # Write-Host "Gathering data for `"$tabname`"..." -ForegroundColor Cyan + # $csv = ($eventsSigned | Select-Object PublisherName, ProductName, GenericDir, FileType | Sort-Object PublisherName, ProductName, GenericDir -Unique | ConvertTo-Csv -Delimiter "`t" -NoTypeInformation) + # AddWorksheetFromCsvData -csv $csv -tabname $tabname + + # Analysis of unsigned files: + $tabname = "Unsigned file info" + Write-Host "Gathering data for `"$tabname`"..." -ForegroundColor Cyan + $csv = ($eventsUnsigned | Select-Object GenericPath, FileName, FileType, Hash | Sort-Object GenericPath -Unique | ConvertTo-Csv -Delimiter "`t" -NoTypeInformation) + AddWorksheetFromCsvData -csv $csv -tabname $tabname + + # # Analysis of unsigned files (dir only): + # $tabname = "Dirs of unsigned files" + # Write-Host "Gathering data for `"$tabname`"..." -ForegroundColor Cyan + # $csv = ($eventsUnsigned | Select-Object GenericDir | Sort-Object GenericDir -Unique | ConvertTo-Csv -Delimiter "`t" -NoTypeInformation) + # AddWorksheetFromCsvData -csv $csv -tabname $tabname + + # Events per user: + $tabname = "Users and event counts" + Write-Host "Gathering data for `"$tabname`"..." -ForegroundColor Cyan + $csv = ($dataFiltered.UserName | Group-Object | Select-Object Name, Count | Sort-Object Name | ConvertTo-Csv -Delimiter "`t" -NoTypeInformation) + AddWorksheetFromCsvData -csv $csv -tabname $tabname -CrLfEncoded "" + + # Per-user details + $tabname = "Files by user" + Write-Host "Gathering data for `"$tabname`"..." -ForegroundColor Cyan + $csv = ($dataFiltered | Select-Object UserName, GenericPath, PublisherName, ProductName | Sort-Object UserName, GenericPath, PublisherName, ProductName -Unique | ConvertTo-Csv -Delimiter "`t" -NoTypeInformation) + AddWorksheetFromCsvData -csv $csv -tabname $tabname + + # Per-user details + $tabname = "Files by user (details)" + Write-Host "Gathering data for `"$tabname`"..." -ForegroundColor Cyan + $csv = ($dataFiltered | Select-Object UserName, MachineName, EventTimeXL, GenericPath, PublisherName, ProductName | Sort-Object UserName, MachineName, EventTimeXL, GenericPath, PublisherName, ProductName -Unique | ConvertTo-Csv -Delimiter "`t" -NoTypeInformation) + AddWorksheetFromCsvData -csv $csv -tabname $tabname + + # All event data + AddWorksheetFromCsvFile -filename $AppLockerEventsCsvFileFullPath -tabname "Full details" + + SelectFirstWorksheet + + if ($SaveWorkbook) + { + $xlFname = [System.IO.Path]::ChangeExtension($AppLockerEventsCsvFileFullPath, ".xlsx") + SaveWorkbook -filename $xlFname + } + + ReleaseExcelApplication +} + +$OutputEncoding = $OutputEncodingPrevious + + diff --git a/AaronLocker/Get-AppLockerEvents.ps1 b/AaronLockerScriptBased/AaronLocker/Get-AppLockerEvents.ps1 similarity index 97% rename from AaronLocker/Get-AppLockerEvents.ps1 rename to AaronLockerScriptBased/AaronLocker/Get-AppLockerEvents.ps1 index d2bd31d..d8b0f38 100644 --- a/AaronLocker/Get-AppLockerEvents.ps1 +++ b/AaronLockerScriptBased/AaronLocker/Get-AppLockerEvents.ps1 @@ -1,795 +1,795 @@ -<# -.SYNOPSIS -Retrieves and sorts event data from AppLocker logs, removes duplicates, and reports as tab-delimited CSV output, PSCustomObjects, or as an Excel worksheet. - -TODO: Add support for "Packaged app-Execution" - -.DESCRIPTION -Any fields can be omitted from the output; removing fields with unique data such as event time can result -in removal of more lines that otherwise contain duplicated data. - -AppLocker logs can be saved event log files, or live event logs on the local or a named remote computer. - -Output can be tab-delimited CSV, an array of PSCustomObject, or a formatted Excel worksheet. - -By default, retrieves error and warning events from both the EXE/DLL and MSI/Script event logs on the local computer. -Live-log options include reading events from a remote computer, reading from one of the EXE/DLL and MSI/Script logs -instead of both, or reading from the "Forwarded Events" event log on the local or a remote computer. -Optionally, read from one or more saved .evtx files. - -By default, retrieves error and warning events. AppLocker in audit mode produces warning events ("would have been blocked"), while enforce mode produces error events ("was blocked"). -Optionally, read just errors, just warnings, just information events (file was allowed), or all events. - -Data from each event (minus any omitted fields) is turned into a line of tab-delimited CSV. These lines are then sorted -and duplicates are removed. When fields containing more unique data are omitted, the remaining data will tend to have more -duplication and more lines will be removed. See the detailed parameter descriptions for more information. - -Random-named temporary files created by PowerShell to test whitelisting policy are filtered out by default. - -Use the -ComputerName parameter to name a remote computer from which to retrieve events. -Use the -WarningOnly, -ErrorOnly, -Allowed, or -AllEvents switches to retrieve events other than errors+warnings. -Use the -ExeAndDllOnly or -MsiAndScriptOnly switches to retrieve events only from one of the two live event logs. -Use the -ForwardedEvents switch to read from the ForwardedEvents log instead of from the EXE/DLL and MSI/Script logs. -Use the -EvtxLogFilePaths parameter to name one or more saved event log files to read. -Use the -NoPsFilter switch not to filter out random-named PowerShell policy test script files. -Use the other -No* switches to omit fields from the output. -NoEventTime, -NoEventTimeXL, and -NoPID are the most important for reducing output size. - -See the detailed parameter descriptions for more information. - -.PARAMETER ComputerName -Inspects events on the named remote computer instead of the local computer. Caller must have administrative rights on the remote computer. - -.PARAMETER ExeAndDllOnly -Retrieves only from the EXE and DLL log (doesn't retrieve from the MSI and Script log). -If neither -ExeAndDllOnly or -MsiAndScriptOnly are specified, retrieves from both logs. - -.PARAMETER MsiAndScriptOnly -Retrieves only from the MSI and Script log (doesn't retrieve from the EXE and DLL log). -If neither -ExeAndDllOnly or -MsiAndScriptOnly are specified, retrieves from both logs. - -.PARAMETER ForwardedEvents -Retrieves from the ForwardedEvents log instead of from the EXE/DLL and MSI/Script logs. - -.PARAMETER EvtxLogFilePaths -Specifies path to one or more saved event log files. (Cannot be used with -ComputerName, -ExeAndDllOnly, or -MsiAndScriptOnly.) - -.PARAMETER WarningOnly -Reports only Warning events (AuditOnly mode; "would have been blocked"), instead of Errors + Warnings. - -.PARAMETER ErrorOnly -Reports only Error events (Enforce mode; files actually blocked), instead of Errors + Warnings. - -.PARAMETER Allowed -Reports only Information events (files allowed to run) instead of Errors + Warnings. - -.PARAMETER AllEvents -Reports all Information, Warning, and Error events. - -.PARAMETER FromDateTime -Reports only events on or after the specified date or date-time. E.g., -FromDateTime "9/7/2017" or -FromDateTime "9/7/2017 12:00:00" -Can be used with -ToDateTime to specify a date/time range. Date/time specified in local time zone. - -.PARAMETER ToDateTime -Reports only events on or before the specified date or date-time. E.g., -ToDateTime "9/7/2017" or -ToDateTime "9/7/2017 12:00:00" -Can be used with -FromDateTime to specify a date/time range. Date/time specified in local time zone. - -.PARAMETER NoGenericPath -GenericPath is the original file path with "%LOCALAPPDATA%" replacing the beginning of the path name if it matches the typical pattern "C:\Users\[username]\AppData\Local". -Makes similar replacements for "%APPDATA%" or "%USERPROFILE%" if LOCALAPPDATA isn't applicable. -If -NoGenericPath is specified, GenericPath data is not included in the output. - -.PARAMETER NoGenericDir -GenericDir is the directory-name portion of GenericPath (i.e., with the filename removed). -If -NoGenericDir is specified, GenericDir data is not included in the output. - -.PARAMETER NoOriginalPath -OriginalPath is the file path exactly as reported in the AppLocker event log data. -If a file is used by multiple users, OriginalPath often includes differentiating information such as user profile name. -If -NoOriginalPath is specified, OriginalPath data is not included in the output. This can be useful when aggregating data from many users running the same programs. - -.PARAMETER NoFileName -FileName is the logged filename (including extension) by itself without path information. -If -NoFileName is specified, FileName data is not included in the output. - -.PARAMETER NoFileExt -FileExt is the file extension of the logged file. This can be useful to track files with non-standard file extensions. -If -NoFileExt is specified, FileExt data is not included in the output. - -.PARAMETER NoFileType -FileType is "EXE," "DLL," "MSI," or "SCRIPT." -If -NoFileType is specified, FileType data is not included in the output. - -.PARAMETER NoPublisherName -For signed files, PublisherName is the distinguished name (DN) of the file's digital signer. PublisherName is blank or just a hyphen if the file is not signed by a trusted publisher. -If -NoPublisherName is specified, PublisherName data is not included in the output. - -.PARAMETER NoProductName -For signed files, ProductName is the product name taken from the file's version resource. -If -NoProductName is specified, ProductName data is not included in the output. - -.PARAMETER NoBinaryName -For signed files, BinaryName is the "OriginalName" field taken from the file's version resource. -If -NoBinaryName is specified, BinaryName data is not included in the output. - -.PARAMETER NoFileVersion -For signed files, FileVersion is the binary file version taken from the file's version resource. -If -NoFileVersion is specified, FileVersion data is not included in the output. - -.PARAMETER NoHash -The Hash field, if included, represents the file's SHA256 hash. In addition to being incorporated in rule data, the hash data can help determine whether two files are identical. -If -NoHash is specified, the file's SHA256 hash data is not included in the output. - -.PARAMETER NoUserSID -UserSID is the security identifier (SID) of the user that ran or tried to run the file. -If -NoUserSID is specified, UserSID data is not included in the output. -If a file is used by different users, UserSID is differentiating. -NoUserSID can be useful when aggregating data from many users running the same programs. - -.PARAMETER NoUserName -UserName is the result of SID-to-name translation of the UserSID value performed on the local computer. -If -NoUserName is specified, SID-to-name translation is not attempted and UserName data is not included in the output. -If a file is used by different users, UserName is differentiating. -NoUserName can be useful when aggregating data from many users running the same programs. - -.PARAMETER NoMachineName -MachineName is the computer name on which the event was logged. -If -NoMachineName is specified, MachineName data is not included in output. This can be useful when aggregating data forwarded from many computers. - -.PARAMETER NoEventTime -EventTime is the date and time that the event occurred, in the computer's local time zone and rendered in this sortable format "yyyy-MM-ddTHH:mm:ss.fffffff". -For example, June 13, 2018, 6:49pm plus 17.7210233 seconds is reported as 2018-06-13T18:49:17.7210233. -If -NoEventTime is specified, EventTime data is not included in the output. This is useful when you want to get at most one event for every file referenced. - -.PARAMETER NoEventTimeXL -EventTimeXL is the date and time that the event occurred, in the computer's local time zone and rendered in a format that Excel recognizes as a date/time, and its filter dropdown renders in a tree view. -If -NoEventTimeXL is specified, EventTimeXL data is not included in the output. This is useful when you want to get at most one event for every file referenced. - -.PARAMETER NoPID -PID is the process ID. It can be used to correlate EXE files and other file types, including scripts and DLLs. -If -NoPID is specified, the PID is not included in the output. -Note that a PID is a unique identifier only on the computer the process is running on and only while it is running. When the process exits, the PID value can be assigned to another process. - -.PARAMETER NoEventType -EventType is "Information," "Warning," or "Error," which can be particularly helpful with -AllEvents, as it's not otherwise possible to tell whether the file was allowed. -If -NoEventType is specified, EventType data is not included in the output. - -.PARAMETER NoAutoNGEN -If specified, does not report modern-app AutoNGEN files that are unsigned and in the user's profile. - -.PARAMETER NoPSFilter -If specified, does not try to filter out random-named PowerShell scripts used to determine whether whitelisting is in effect. - -.PARAMETER NoFilteredMachines -By default, this script outputs a single artificial "empty" event line for every machine for which all observed events were filtered out. -If -NoFilteredMachines is specified, these event lines are not output. - -.PARAMETER Excel -If this optional switch is specified, outputs to a formatted Excel rather than tab-delimited CSV text to the pipeline. - -.PARAMETER Objects -If this optional switch is specified, outputs PSCustomObjects rather than tab-delimited CSV. (Passes CSV through ConvertFrom-Csv.) -This switch is ignored if -Excel is also specified. - -.EXAMPLE - -.\Get-AppLockerEvents.ps1 -EvtxLogFilePaths .\ForwardedEvents1.evtx, .\ForwardedEvents2.evtx -NoMachineName -NoEventTime -NoEventTimeXL - -Get warning and error events from events exported into ForwardedEvents1.evtx and ForwardedEvents2.evtx; don't include MachineName or EventTime data in the output. - -.EXAMPLE - -.\Get-AppLockerEvents.ps1 -NoOriginalPath -NoEventTime -NoEventTimeXL -NoUserSID | clip.exe - -Get warning and error events from the EXE/DLL and MSI/Script logs on the local computer, removing user-specific and time-specific fields, with the goal that each referenced file appears at most once in the output, no matter how many users referenced it or how often. Write the output to the Windows clipboard so that it can be pasted into Microsoft Excel. - -.EXAMPLE - -.\Get-AppLockerEvents.ps1 -Objects | Where-Object { [datetime]($_.EventTime) -gt "8/20/2017" } - -Get warning and error events from the EXE/DLL and MSI/Script logs on the local computer since August 20, 2017. -It converts output into objects, and pipes those objects into a filter that passes only events with event dates after midnight, August 20, 2017. - -.EXAMPLE -.\Get-AppLockerEvents.ps1 -FromDateTime "8/1/2017" -ToDateTime "9/1/2017" - -Gets warning and error events from the EXE/DLL and MSI/Script logs on the local computer between Aug 1, 2017 00:00:00 and Sept 1, 2017 00:00:00. - -.EXAMPLE - -.\Get-AppLockerEvents.ps1 -Allowed -Objects | Group-Object PublisherName - -Get allowed files from the EXE/DLL and MSI/Script logs on the local computer. Convert output into objects, group the objects according to the PublisherName field. - -.EXAMPLE - -.\Get-AppLockerEvents.ps1 -NoOriginalPath -NoEventTime -NoEventTimeXL -NoUserSID -Objects | Where-Object { $_.PublisherName.Length -le 1 } | ConvertTo-Csv -Delimiter "`t" -NoTypeInformation - -Get warning and error events from the EXE/DLL and MSI/Script logs on the local computer, outputting only unsigned files. -It converts output into objects, filters on PublisherName length (allowing up to a hyphen in length), then converts back to tab-delimited CSV. - -.EXAMPLE -$ev = .\Get-AppLockerEvents.ps1 -Objects -$ev | Select-Object UserName, MachineName -Unique | Sort-Object UserName, MachineName -$ev.FileExt | Sort-Object -Unique - -Output a list of each combination of users and machines reporting events, and a list of all observed file extensions involved with events. - - - -#> - -[CmdletBinding(DefaultParameterSetName="LiveLogs")] -param( - # Optional remote computer name - [parameter(Mandatory=$false, ParameterSetName="LiveLogs")] - [String] - $ComputerName, - - # Which event log(s) to inspect (default is EXE/DLL and MSI/Script logs) - [parameter(ParameterSetName="LiveLogs")] - [switch] - $ExeAndDllOnly = $false, - [parameter(ParameterSetName="LiveLogs")] - [switch] - $MsiAndScriptOnly = $false, - [parameter(ParameterSetName="LiveLogs")] - [switch] - $ForwardedEvents = $false, - - [parameter(Mandatory=$false, ParameterSetName="SavedLogs")] - [String[]] - $EvtxLogFilePaths, - - # Which event types to inspect (default is warnings + errors) - [switch] - $WarningOnly = $false, - [switch] - $ErrorOnly = $false, - [switch] - $Allowed = $false, - [switch] - $AllEvents = $false, - - # Optional date range - [parameter(Mandatory=$false)] - [datetime] - $FromDateTime, - [parameter(Mandatory=$false)] - [datetime] - $ToDateTime, - - # Data to return. Defaults to all, except those switched off with the following switches - [switch] - $NoGenericPath = $false, - [switch] - $NoGenericDir = $false, - [switch] - $NoOriginalPath = $false, - [switch] - $NoFileName = $false, - [switch] - $NoFileExt = $false, - [switch] - $NoFileType = $false, - [switch] - $NoPublisherName = $false, - [switch] - $NoProductName = $false, - [switch] - $NoBinaryName = $false, - [switch] - $NoFileVersion = $false, - [switch] - $NoHash = $false, - [switch] - $NoUserSID = $false, - [switch] - $NoUserName = $false, - [switch] - $NoMachineName = $false, - [switch] - $NoEventTime = $false, - [switch] - $NoEventTimeXL = $false, - [switch] - $NoPID = $false, - [switch] - $NoEventType = $false, - - # If specified, does not report modern-app AutoNGEN files that are unsigned and in the user's profile. - [switch] - $NoAutoNGEN = $false, - - # This script filters out PowerShell policy test scripts by default. The -NoPSFilter switch allows those not to be filtered. - [switch] - $NoPSFilter = $false, - - # If specified, do not create artificial "empty" event lines for machines for which all observed events were filtered out. - [switch] - $NoFilteredMachines = $false, - - # Output to Excel - [switch] - $Excel, - - # Output PSCustomObjects - [switch] - $Objects -) - -$rootDir = [System.IO.Path]::GetDirectoryName($MyInvocation.MyCommand.Path) -# Get configuration settings and global functions from .\Support\Config.ps1) -# Dot-source the config file. -. $rootDir\Support\Config.ps1 - -# -# Strings -# -$ExeDllLogName = 'Microsoft-Windows-AppLocker/EXE and DLL' -$MsiScriptLogName = 'Microsoft-Windows-AppLocker/MSI and Script' -$FwdEventsLogName = 'ForwardedEvents' -$ExeDllAllowed = 'EventID=8002' -$ExeDllWarning = 'EventID=8003' -$ExeDllError = 'EventID=8004' -$MsiScriptAllowed = 'EventID=8005' -$MsiScriptWarning = 'EventID=8006' -$MsiScriptError = 'EventID=8007' -$SubscriptionBkmrk= 'EventID=111' - -# -# Event logs to query -# -$eventLogs = @() - -# -# Specify event log names to query. -# If looking at ForwardedEvents, also set XPath to filter on provider so we don't inadvertently get events from sources we don't know about. -# -$eventProviderFilter = "" - -if ($ForwardedEvents) -{ - $eventLogs += $FwdEventsLogName - $eventProviderFilter = "Provider[@Name='Microsoft-Windows-AppLocker' or @Name='Microsoft-Windows-EventForwarder'] and" -} -else -{ - if (!$MsiAndScriptOnly) { $eventLogs += $ExeDllLogName } - if (!$ExeAndDllOnly) { $eventLogs += $MsiScriptLogName } -} -if ($eventLogs.Length -eq 0 -and $EvtxLogFilePaths.Length -eq 0) -{ - Write-Error "No logs to inspect." - return -} - -# -# Eventlog XPath query: optional date/time filtering -# -$dateTimeFilter = "" -if ($FromDateTime -or $ToDateTime) -{ - if ($FromDateTime) - { - $dateTimeFilter = "TimeCreated[@SystemTime>='" + $FromDateTime.ToUniversalTime().ToString("s") + "']" - if ($ToDateTime) - { - $dateTimeFilter += " and " - } - } - if ($ToDateTime) - { - $dateTimeFilter += "TimeCreated[@SystemTime<='" + $ToDateTime.ToUniversalTime().ToString("s") + "']" - } - - $dateTimeFilter = "($dateTimeFilter) and" -} - -# -# Event log XPath query: event IDs -# -$eventIdFilter = "$ExeDllWarning or $MsiScriptWarning or $ExeDllError or $MsiScriptError" -if ($WarningOnly) -{ - $eventIdFilter = "$ExeDllWarning or $MsiScriptWarning" -} -if ($ErrorOnly) -{ - $eventIdFilter = "$ExeDllError or $MsiScriptError" -} -if ($Allowed) -{ - $eventIdFilter = "$ExeDllAllowed or $MsiScriptAllowed" -} -if ($AllEvents) -{ - $eventIdFilter = "$ExeDllAllowed or $MsiScriptAllowed or $ExeDllWarning or $MsiScriptWarning or $ExeDllError or $MsiScriptError" -} -if ($ForwardedEvents -and !$NoFilteredMachines) -{ - $eventIdFilter += " or $SubscriptionBkmrk" -} -$eventIdFilter = "($eventIdFilter)" - -# -# Set the XPath filter for the event log(s) query -# -$filter = "*[System[$eventProviderFilter $dateTimeFilter $eventIdFilter]]" -### Use -Verbose to debug the FilterXPath -Write-Verbose "XPath filter = $filter" - -# -# More strings: patterns to look for -# -# Match partial path in temp directory with form XXXXXXXX.XXX.PS* or __PSScriptPolicyTest_XXXXXXXX.XXX.PS* -$PsPolicyTestPattern = "\\APPDATA\\LOCAL\\TEMP\\(__PSScriptPolicyTest_)?[A-Z0-9]{8}\.[A-Z0-9]{3}\.PS" -# Usage: -# if ($origPath -match $PsPolicyTestPattern) { $filterOut = !$NoPSFilter } -# New implementation: PS script policy test file is a one-byte file containing "1". Its SHA256 hash is 0x6B86B273FF34FCE19D6B804EFF5A3F5747ADA4EAA22F1D49C01E52DDB7875B4B -# Check for that hash rather than the filepath pattern. The hash will be reliable; the file pattern could give a false positive. -# (Perf test indicates no benefit of one test over the other.) -# NOTE: if the content of the test file changes, there will be a new hash value to test for. -# NOTE: The PowerShell folks started randomizing the content, so hash check no longer works - need to look for filename patterns. -# $PsPolicyTestFileHash = "0x6B86B273FF34FCE19D6B804EFF5A3F5747ADA4EAA22F1D49C01E52DDB7875B4B" -# Pattern was: if ($hash -eq $PsPolicyTestFileHash) ... - -# Match AutoNGEN native image file path -$AutoNGENPattern = "^(%OSDRIVE%|C:)\\Users\\[^\\]*\\AppData\\Local\\Packages\\.*\\NATIVEIMAGES\\.*\.NI\.(EXE|DLL)$" - -# Pattern that can be replaced by %LOCALAPPDATA% -$LocalAppDataPattern = "^(%OSDRIVE%|C:)\\Users\\[^\\]*\\AppData\\Local\\" -# Pattern that can be replaced by %APPDATA% -$RoamingAppDataPattern = "^(%OSDRIVE%|C:)\\Users\\[^\\]*\\AppData\\Roaming\\" -# Pattern that can be replaced by %USERPROFILE% (after the above already done) -$UserProfilePattern = "^(%OSDRIVE%|C:)\\Users\\[^\\]*\\" - -# Tab -$t = "`t" - -# Properties -$props = @() -if (!$NoGenericPath) { $props += "GenericPath" } -if (!$NoGenericDir) { $props += "GenericDir" } -if (!$NoOriginalPath) { $props += "OriginalPath" } -if (!$NoFileName) { $props += "FileName" } -if (!$NoFileExt) { $props += "FileExt" } -if (!$NoFileType) { $props += "FileType" } -if (!$NoPublisherName) { $props += "PublisherName" } -if (!$NoProductName) { $props += "ProductName" } -if (!$NoBinaryName) { $props += "BinaryName" } -if (!$NoFileVersion) { $props += "FileVersion" } -if (!$NoHash) { $props += "Hash" } -if (!$NoUserSID) { $props += "UserSID" } -if (!$NoUserName) { $props += "UserName" } -if (!$NoMachineName) { $props += "MachineName" } -if (!$NoEventTime) { $props += "EventTime" } -if (!$NoEventTimeXL) { $props += "EventTimeXL" } -if (!$NoPID) { $props += "PID" } -if (!$NoEventType) { $props += "EventType" } -$headers = $props -join $t - -# -# Retrieve events -# -$ev = @() -if ($EvtxLogFilePaths) -{ - $EvtxLogFilePaths | foreach { - Write-Host "Calling Get-WinEvent -Path $_ ..." -ForegroundColor Cyan - $ev += Get-WinEvent -Path $_ -FilterXPath $filter -ErrorAction SilentlyContinue -ErrorVariable gweErr - if ($gweErr.Count -gt 0) - { - $gweErr | foreach { Write-Host ("--> " + $_.ToString()) -ForegroundColor Cyan } - } - } -} -else -{ - $eventLogs | foreach { - if ($ComputerName) - { - Write-Host "Calling Get-WinEvent -LogName $_ -ComputerName $ComputerName ..." -ForegroundColor Cyan - $ev += (Get-WinEvent -LogName $_ -ComputerName $ComputerName -FilterXPath $filter -ErrorAction SilentlyContinue -ErrorVariable gweErr) - } - else - { - Write-Host "Calling Get-WinEvent -LogName $_ ..." -ForegroundColor Cyan - $ev += (Get-WinEvent -LogName $_ -FilterXPath $filter -ErrorAction SilentlyContinue -ErrorVariable gweErr) - } - if ($gweErr.Count -gt 0) - { - $gweErr | foreach { Write-Host ("--> " + $_.ToString()) -ForegroundColor Cyan } - } - } -} -Write-Host ($ev.Count.ToString() + " events retrieved.") -ForegroundColor Cyan - -#TODO: Figure out whether/when only one works, and why: Xml vs. Get-WinEvent objects -$UseXml = $true - -# -# Create output array; add CSV headers -# -$csv = @() -$csv += $headers - -# -# Lookups -# -$SidToName = @{} -$AllMachineNames = @{} -$ReportedMachines = @{} - -# -# Function performs SID-to-name lookups, stores results for later retrieval so the same SID isn't looked up more than once. -# -function SidToNameLookup([string]$sid) -{ - if ($SidToName.ContainsKey($sid)) - { - $SidToName[$sid] - } - else - { - $oSID = New-Object System.Security.Principal.SecurityIdentifier($sid) - $oUser = $null - try { $oUser = $oSID.Translate([System.Security.Principal.NTAccount]) } catch {} - if ($null -ne $oUser) - { - $name = $oUser.Value - } - elseif ($sid.EndsWith("-500")) - { - $name = "[[[built-in local admin]]]"; - } - else - { - $name = "[[[Not translated]]]" - } - $SidToName.Add($sid, $name) - $name - } -} - -# -# Produce output -# -$count = 0 -$filteredOut = 0 -$csv += ( - $ev | foreach { - - # Implement options to hide items that match PowerShell policy test script or AutoNGEN native images: - $filterOut = $false - - <# - Event ID 111 (should maybe also check $_.ProviderName -eq "Microsoft-Windows-EventForwarder") indicates an artificial - event created on the Windows event collector when a client system creates a subscription. - Use it to identify systems that were able to forward events but didn't. - Example event XML: - - - - - - 111 - - myworkstation.contoso.com - - - - - - #> - if ($_.Id -eq 111) - { - # Bookmark event - $filterOut = $true - } - else - { - $xEv = $null - $xData = $null - - if ($UseXml -or $null -eq $_.Properties[0]) - { - $xEv = [xml]($_.ToXml()) - $xData = $xEv.Event.UserData.RuleAndFileData - $origPath = $xData.FilePath - $hash = "0x" + $xData.FileHash - $sPID = $xData.TargetProcessId - } - else - { - $origPath = $_.Properties[10].Value # File path - $hash = "0x" + [System.BitConverter]::ToString( $_.Properties[12].Value ).Replace('-', '') - $sPID = $_.Properties[8].Value - } - - if ($origPath -match $PsPolicyTestPattern) - { - $filterOut = !$NoPSFilter - } - elseif ($origPath -match $AutoNGENPattern) - { - $filterOut = $NoAutoNGEN - } - } - - if ($filterOut) { $filteredOut++ } - - if (!$NoFilteredMachines) - { - # Observed machines - $machineName = $_.MachineName # Computer name - if (!$AllMachineNames.ContainsKey($machineName)) - { - # All observed machines - $AllMachineNames.Add($machineName, "") - } - if (!$filterOut -and !$ReportedMachines.ContainsKey($machineName)) - { - # Machines that have had data reported - $ReportedMachines.Add($machineName, "") - } - } - - if (!$filterOut) - { - $timeCreated = $_.TimeCreated.ToString("yyyy-MM-ddTHH:mm:ss.fffffff") # alpha sort = chronological sort; granularity = ten millionths of a second - $machineName = $_.MachineName # Computer name - - # Manual text conversion in case LevelDisplayName is not populated - if(![string]::IsNullOrEmpty($_.LevelDisplayName)){ - $eventType = $_.LevelDisplayName # Event type (Information, Warning, Error) - } - else{ - $eventType = switch($_.Level) - { - 1 { "Critical" } - 2 { "Error" } - 3 { "Warning" } - 4 { "Information" } - 5 { "Verbose" } - default { $_.Level.ToString() } - } - } - - if ($null -eq $xData) - { - $filetype = $_.Properties[1].Value # EXE, DLL, MSI, or SCRIPT - $userSid = $_.Properties[7].Value.Value # User SID (System.Security.Principal.SecurityIdentifier) - $pubInfo = $_.Properties[14].Value.Split("\") # Publisher info, separated with backslashes - # $hash = "0x" + [System.BitConverter]::ToString( $_.Properties[12].Value ).Replace('-', '') - } - else - { - $filetype = $xData.PolicyName - $userSid = $xData.TargetUser - $pubInfo = $xData.Fqbn.Split("\") - # $hash = "0x" + $xData.FileHash - } - - $pubName = $pubInfo[0] # Publisher name - $prodName = $pubInfo[1] # Product name (syntax works even if array not this long) - $binaryName = $pubInfo[2] # Original "binary" name (syntax works even if array not this long) - $filever = $pubInfo[3] # File version (syntax works even if array not this long) - $filename = [System.IO.Path]::GetFileName($origPath) - $fileext = [System.IO.Path]::GetExtension($origPath) - # Generic path replaces user-specific paths with more generic variable syntax. - # Userprofile has to be performed after more specific appdata replacements. - $genpath = (($origPath -replace $LocalAppDataPattern, "%LOCALAPPDATA%\") -replace $RoamingAppDataPattern, "%APPDATA%\") -replace $UserProfilePattern, "%USERPROFILE%\" - $gendir = [System.IO.Path]::GetDirectoryName($genpath) - - #Anyone wants objects, they can ConvertFrom-Csv. - $data = @() - if (!$NoGenericPath) { $data += $genpath } - if (!$NoGenericDir) { $data += $gendir } - if (!$NoOriginalPath) { $data += $origPath } - if (!$NoFileName) { $data += $filename } - if (!$NoFileExt) { $data += $fileext } - if (!$NoFileType) { $data += $filetype } - if (!$NoPublisherName) { $data += $pubName } - if (!$NoProductName) { $data += $prodName } - if (!$NoBinaryName) { $data += $binaryName } - if (!$NoFileVersion) { $data += $filever } - if (!$NoHash) { $data += $hash } - if (!$NoUserSID) { $data += $userSID } - if (!$NoUserName) { $data += SidToNameLookup $userSid } - if (!$NoMachineName) { $data += $machineName } - if (!$NoEventTime) { $data += $timeCreated } - #TODO: Verify that regional preferences don't interfere with making this useful... - if (!$NoEventTimeXL) { $data += $timeCreated.Replace("T", " ").Substring(0, 19) } - if (!$NoPID) { $data += $sPID } - if (!$NoEventType) { $data += $eventType } - # Output the data as CSV - $data -join $t - } - - $count++ - if ($count -eq 100) - { - Write-Host "." -NoNewline -ForegroundColor Cyan - $count = 0 - } - } | Sort-Object -Unique -) - -# -# Unless specified otherwise, also output "empty" events for machines for which all events were filtered out -# -if (!$NoFilteredMachines) -{ - $csv += ( - $AllMachineNames.Keys | Sort-Object | foreach { - $machineName = $_ - # If machine observed but not reported, report it now - if (!$ReportedMachines.ContainsKey($machineName)) - { - $data = @() - if (!$NoGenericPath) { $data += "" } - if (!$NoGenericDir) { $data += "" } - if (!$NoOriginalPath) { $data += "" } - if (!$NoFileName) { $data += "" } - if (!$NoFileExt) { $data += "" } - if (!$NoFileType) { $data += "NONE" } - if (!$NoPublisherName) { $data += "" } - if (!$NoProductName) { $data += "" } - if (!$NoBinaryName) { $data += "" } - if (!$NoFileVersion) { $data += "" } - if (!$NoHash) { $data += "" } - if (!$NoUserSID) { $data += "" } - if (!$NoUserName) { $data += "" } - if (!$NoMachineName) { $data += $machineName } - if (!$NoEventTime) { $data += "" } - if (!$NoEventTimeXL) { $data += "" } - if (!$NoPID) { $data += "" } - if (!$NoEventType) { $data += "FILTERED" } - # Output the data as CSV - $data -join $t - } - } - ) -} - -Write-Host "" # New line after the dots -Write-Host "$filteredOut events filtered out." -ForegroundColor Cyan - -if ($Excel) -{ - if (CreateExcelApplication) - { - AddWorksheetFromCsvData -csv $csv -tabname "AppLocker events" - ReleaseExcelApplication - } -} -elseif ($Objects) -{ - # Output PSCustomObjects to pipeline - $csv | ConvertFrom-Csv -Delimiter "`t" -} -else -{ - # Output tab-delimited CSV text to pipeline - $csv -} - -<# - Template for 8002, 8003, and 8004 events (and 8005, 8006, and 8007): - +<# +.SYNOPSIS +Retrieves and sorts event data from AppLocker logs, removes duplicates, and reports as tab-delimited CSV output, PSCustomObjects, or as an Excel worksheet. + +TODO: Add support for "Packaged app-Execution" + +.DESCRIPTION +Any fields can be omitted from the output; removing fields with unique data such as event time can result +in removal of more lines that otherwise contain duplicated data. + +AppLocker logs can be saved event log files, or live event logs on the local or a named remote computer. + +Output can be tab-delimited CSV, an array of PSCustomObject, or a formatted Excel worksheet. + +By default, retrieves error and warning events from both the EXE/DLL and MSI/Script event logs on the local computer. +Live-log options include reading events from a remote computer, reading from one of the EXE/DLL and MSI/Script logs +instead of both, or reading from the "Forwarded Events" event log on the local or a remote computer. +Optionally, read from one or more saved .evtx files. + +By default, retrieves error and warning events. AppLocker in audit mode produces warning events ("would have been blocked"), while enforce mode produces error events ("was blocked"). +Optionally, read just errors, just warnings, just information events (file was allowed), or all events. + +Data from each event (minus any omitted fields) is turned into a line of tab-delimited CSV. These lines are then sorted +and duplicates are removed. When fields containing more unique data are omitted, the remaining data will tend to have more +duplication and more lines will be removed. See the detailed parameter descriptions for more information. + +Random-named temporary files created by PowerShell to test whitelisting policy are filtered out by default. + +Use the -ComputerName parameter to name a remote computer from which to retrieve events. +Use the -WarningOnly, -ErrorOnly, -Allowed, or -AllEvents switches to retrieve events other than errors+warnings. +Use the -ExeAndDllOnly or -MsiAndScriptOnly switches to retrieve events only from one of the two live event logs. +Use the -ForwardedEvents switch to read from the ForwardedEvents log instead of from the EXE/DLL and MSI/Script logs. +Use the -EvtxLogFilePaths parameter to name one or more saved event log files to read. +Use the -NoPsFilter switch not to filter out random-named PowerShell policy test script files. +Use the other -No* switches to omit fields from the output. -NoEventTime, -NoEventTimeXL, and -NoPID are the most important for reducing output size. + +See the detailed parameter descriptions for more information. + +.PARAMETER ComputerName +Inspects events on the named remote computer instead of the local computer. Caller must have administrative rights on the remote computer. + +.PARAMETER ExeAndDllOnly +Retrieves only from the EXE and DLL log (doesn't retrieve from the MSI and Script log). +If neither -ExeAndDllOnly or -MsiAndScriptOnly are specified, retrieves from both logs. + +.PARAMETER MsiAndScriptOnly +Retrieves only from the MSI and Script log (doesn't retrieve from the EXE and DLL log). +If neither -ExeAndDllOnly or -MsiAndScriptOnly are specified, retrieves from both logs. + +.PARAMETER ForwardedEvents +Retrieves from the ForwardedEvents log instead of from the EXE/DLL and MSI/Script logs. + +.PARAMETER EvtxLogFilePaths +Specifies path to one or more saved event log files. (Cannot be used with -ComputerName, -ExeAndDllOnly, or -MsiAndScriptOnly.) + +.PARAMETER WarningOnly +Reports only Warning events (AuditOnly mode; "would have been blocked"), instead of Errors + Warnings. + +.PARAMETER ErrorOnly +Reports only Error events (Enforce mode; files actually blocked), instead of Errors + Warnings. + +.PARAMETER Allowed +Reports only Information events (files allowed to run) instead of Errors + Warnings. + +.PARAMETER AllEvents +Reports all Information, Warning, and Error events. + +.PARAMETER FromDateTime +Reports only events on or after the specified date or date-time. E.g., -FromDateTime "9/7/2017" or -FromDateTime "9/7/2017 12:00:00" +Can be used with -ToDateTime to specify a date/time range. Date/time specified in local time zone. + +.PARAMETER ToDateTime +Reports only events on or before the specified date or date-time. E.g., -ToDateTime "9/7/2017" or -ToDateTime "9/7/2017 12:00:00" +Can be used with -FromDateTime to specify a date/time range. Date/time specified in local time zone. + +.PARAMETER NoGenericPath +GenericPath is the original file path with "%LOCALAPPDATA%" replacing the beginning of the path name if it matches the typical pattern "C:\Users\[username]\AppData\Local". +Makes similar replacements for "%APPDATA%" or "%USERPROFILE%" if LOCALAPPDATA isn't applicable. +If -NoGenericPath is specified, GenericPath data is not included in the output. + +.PARAMETER NoGenericDir +GenericDir is the directory-name portion of GenericPath (i.e., with the filename removed). +If -NoGenericDir is specified, GenericDir data is not included in the output. + +.PARAMETER NoOriginalPath +OriginalPath is the file path exactly as reported in the AppLocker event log data. +If a file is used by multiple users, OriginalPath often includes differentiating information such as user profile name. +If -NoOriginalPath is specified, OriginalPath data is not included in the output. This can be useful when aggregating data from many users running the same programs. + +.PARAMETER NoFileName +FileName is the logged filename (including extension) by itself without path information. +If -NoFileName is specified, FileName data is not included in the output. + +.PARAMETER NoFileExt +FileExt is the file extension of the logged file. This can be useful to track files with non-standard file extensions. +If -NoFileExt is specified, FileExt data is not included in the output. + +.PARAMETER NoFileType +FileType is "EXE," "DLL," "MSI," or "SCRIPT." +If -NoFileType is specified, FileType data is not included in the output. + +.PARAMETER NoPublisherName +For signed files, PublisherName is the distinguished name (DN) of the file's digital signer. PublisherName is blank or just a hyphen if the file is not signed by a trusted publisher. +If -NoPublisherName is specified, PublisherName data is not included in the output. + +.PARAMETER NoProductName +For signed files, ProductName is the product name taken from the file's version resource. +If -NoProductName is specified, ProductName data is not included in the output. + +.PARAMETER NoBinaryName +For signed files, BinaryName is the "OriginalName" field taken from the file's version resource. +If -NoBinaryName is specified, BinaryName data is not included in the output. + +.PARAMETER NoFileVersion +For signed files, FileVersion is the binary file version taken from the file's version resource. +If -NoFileVersion is specified, FileVersion data is not included in the output. + +.PARAMETER NoHash +The Hash field, if included, represents the file's SHA256 hash. In addition to being incorporated in rule data, the hash data can help determine whether two files are identical. +If -NoHash is specified, the file's SHA256 hash data is not included in the output. + +.PARAMETER NoUserSID +UserSID is the security identifier (SID) of the user that ran or tried to run the file. +If -NoUserSID is specified, UserSID data is not included in the output. +If a file is used by different users, UserSID is differentiating. -NoUserSID can be useful when aggregating data from many users running the same programs. + +.PARAMETER NoUserName +UserName is the result of SID-to-name translation of the UserSID value performed on the local computer. +If -NoUserName is specified, SID-to-name translation is not attempted and UserName data is not included in the output. +If a file is used by different users, UserName is differentiating. -NoUserName can be useful when aggregating data from many users running the same programs. + +.PARAMETER NoMachineName +MachineName is the computer name on which the event was logged. +If -NoMachineName is specified, MachineName data is not included in output. This can be useful when aggregating data forwarded from many computers. + +.PARAMETER NoEventTime +EventTime is the date and time that the event occurred, in the computer's local time zone and rendered in this sortable format "yyyy-MM-ddTHH:mm:ss.fffffff". +For example, June 13, 2018, 6:49pm plus 17.7210233 seconds is reported as 2018-06-13T18:49:17.7210233. +If -NoEventTime is specified, EventTime data is not included in the output. This is useful when you want to get at most one event for every file referenced. + +.PARAMETER NoEventTimeXL +EventTimeXL is the date and time that the event occurred, in the computer's local time zone and rendered in a format that Excel recognizes as a date/time, and its filter dropdown renders in a tree view. +If -NoEventTimeXL is specified, EventTimeXL data is not included in the output. This is useful when you want to get at most one event for every file referenced. + +.PARAMETER NoPID +PID is the process ID. It can be used to correlate EXE files and other file types, including scripts and DLLs. +If -NoPID is specified, the PID is not included in the output. +Note that a PID is a unique identifier only on the computer the process is running on and only while it is running. When the process exits, the PID value can be assigned to another process. + +.PARAMETER NoEventType +EventType is "Information," "Warning," or "Error," which can be particularly helpful with -AllEvents, as it's not otherwise possible to tell whether the file was allowed. +If -NoEventType is specified, EventType data is not included in the output. + +.PARAMETER NoAutoNGEN +If specified, does not report modern-app AutoNGEN files that are unsigned and in the user's profile. + +.PARAMETER NoPSFilter +If specified, does not try to filter out random-named PowerShell scripts used to determine whether whitelisting is in effect. + +.PARAMETER NoFilteredMachines +By default, this script outputs a single artificial "empty" event line for every machine for which all observed events were filtered out. +If -NoFilteredMachines is specified, these event lines are not output. + +.PARAMETER Excel +If this optional switch is specified, outputs to a formatted Excel rather than tab-delimited CSV text to the pipeline. + +.PARAMETER Objects +If this optional switch is specified, outputs PSCustomObjects rather than tab-delimited CSV. (Passes CSV through ConvertFrom-Csv.) +This switch is ignored if -Excel is also specified. + +.EXAMPLE + +.\Get-AppLockerEvents.ps1 -EvtxLogFilePaths .\ForwardedEvents1.evtx, .\ForwardedEvents2.evtx -NoMachineName -NoEventTime -NoEventTimeXL + +Get warning and error events from events exported into ForwardedEvents1.evtx and ForwardedEvents2.evtx; don't include MachineName or EventTime data in the output. + +.EXAMPLE + +.\Get-AppLockerEvents.ps1 -NoOriginalPath -NoEventTime -NoEventTimeXL -NoUserSID | clip.exe + +Get warning and error events from the EXE/DLL and MSI/Script logs on the local computer, removing user-specific and time-specific fields, with the goal that each referenced file appears at most once in the output, no matter how many users referenced it or how often. Write the output to the Windows clipboard so that it can be pasted into Microsoft Excel. + +.EXAMPLE + +.\Get-AppLockerEvents.ps1 -Objects | Where-Object { [datetime]($_.EventTime) -gt "8/20/2017" } + +Get warning and error events from the EXE/DLL and MSI/Script logs on the local computer since August 20, 2017. +It converts output into objects, and pipes those objects into a filter that passes only events with event dates after midnight, August 20, 2017. + +.EXAMPLE +.\Get-AppLockerEvents.ps1 -FromDateTime "8/1/2017" -ToDateTime "9/1/2017" + +Gets warning and error events from the EXE/DLL and MSI/Script logs on the local computer between Aug 1, 2017 00:00:00 and Sept 1, 2017 00:00:00. + +.EXAMPLE + +.\Get-AppLockerEvents.ps1 -Allowed -Objects | Group-Object PublisherName + +Get allowed files from the EXE/DLL and MSI/Script logs on the local computer. Convert output into objects, group the objects according to the PublisherName field. + +.EXAMPLE + +.\Get-AppLockerEvents.ps1 -NoOriginalPath -NoEventTime -NoEventTimeXL -NoUserSID -Objects | Where-Object { $_.PublisherName.Length -le 1 } | ConvertTo-Csv -Delimiter "`t" -NoTypeInformation + +Get warning and error events from the EXE/DLL and MSI/Script logs on the local computer, outputting only unsigned files. +It converts output into objects, filters on PublisherName length (allowing up to a hyphen in length), then converts back to tab-delimited CSV. + +.EXAMPLE +$ev = .\Get-AppLockerEvents.ps1 -Objects +$ev | Select-Object UserName, MachineName -Unique | Sort-Object UserName, MachineName +$ev.FileExt | Sort-Object -Unique + +Output a list of each combination of users and machines reporting events, and a list of all observed file extensions involved with events. + + + +#> + +[CmdletBinding(DefaultParameterSetName="LiveLogs")] +param( + # Optional remote computer name + [parameter(Mandatory=$false, ParameterSetName="LiveLogs")] + [String] + $ComputerName, + + # Which event log(s) to inspect (default is EXE/DLL and MSI/Script logs) + [parameter(ParameterSetName="LiveLogs")] + [switch] + $ExeAndDllOnly = $false, + [parameter(ParameterSetName="LiveLogs")] + [switch] + $MsiAndScriptOnly = $false, + [parameter(ParameterSetName="LiveLogs")] + [switch] + $ForwardedEvents = $false, + + [parameter(Mandatory=$false, ParameterSetName="SavedLogs")] + [String[]] + $EvtxLogFilePaths, + + # Which event types to inspect (default is warnings + errors) + [switch] + $WarningOnly = $false, + [switch] + $ErrorOnly = $false, + [switch] + $Allowed = $false, + [switch] + $AllEvents = $false, + + # Optional date range + [parameter(Mandatory=$false)] + [datetime] + $FromDateTime, + [parameter(Mandatory=$false)] + [datetime] + $ToDateTime, + + # Data to return. Defaults to all, except those switched off with the following switches + [switch] + $NoGenericPath = $false, + [switch] + $NoGenericDir = $false, + [switch] + $NoOriginalPath = $false, + [switch] + $NoFileName = $false, + [switch] + $NoFileExt = $false, + [switch] + $NoFileType = $false, + [switch] + $NoPublisherName = $false, + [switch] + $NoProductName = $false, + [switch] + $NoBinaryName = $false, + [switch] + $NoFileVersion = $false, + [switch] + $NoHash = $false, + [switch] + $NoUserSID = $false, + [switch] + $NoUserName = $false, + [switch] + $NoMachineName = $false, + [switch] + $NoEventTime = $false, + [switch] + $NoEventTimeXL = $false, + [switch] + $NoPID = $false, + [switch] + $NoEventType = $false, + + # If specified, does not report modern-app AutoNGEN files that are unsigned and in the user's profile. + [switch] + $NoAutoNGEN = $false, + + # This script filters out PowerShell policy test scripts by default. The -NoPSFilter switch allows those not to be filtered. + [switch] + $NoPSFilter = $false, + + # If specified, do not create artificial "empty" event lines for machines for which all observed events were filtered out. + [switch] + $NoFilteredMachines = $false, + + # Output to Excel + [switch] + $Excel, + + # Output PSCustomObjects + [switch] + $Objects +) + +$rootDir = [System.IO.Path]::GetDirectoryName($MyInvocation.MyCommand.Path) +# Get configuration settings and global functions from .\Support\Config.ps1) +# Dot-source the config file. +. $rootDir\Support\Config.ps1 + +# +# Strings +# +$ExeDllLogName = 'Microsoft-Windows-AppLocker/EXE and DLL' +$MsiScriptLogName = 'Microsoft-Windows-AppLocker/MSI and Script' +$FwdEventsLogName = 'ForwardedEvents' +$ExeDllAllowed = 'EventID=8002' +$ExeDllWarning = 'EventID=8003' +$ExeDllError = 'EventID=8004' +$MsiScriptAllowed = 'EventID=8005' +$MsiScriptWarning = 'EventID=8006' +$MsiScriptError = 'EventID=8007' +$SubscriptionBkmrk= 'EventID=111' + +# +# Event logs to query +# +$eventLogs = @() + +# +# Specify event log names to query. +# If looking at ForwardedEvents, also set XPath to filter on provider so we don't inadvertently get events from sources we don't know about. +# +$eventProviderFilter = "" + +if ($ForwardedEvents) +{ + $eventLogs += $FwdEventsLogName + $eventProviderFilter = "Provider[@Name='Microsoft-Windows-AppLocker' or @Name='Microsoft-Windows-EventForwarder'] and" +} +else +{ + if (!$MsiAndScriptOnly) { $eventLogs += $ExeDllLogName } + if (!$ExeAndDllOnly) { $eventLogs += $MsiScriptLogName } +} +if ($eventLogs.Length -eq 0 -and $EvtxLogFilePaths.Length -eq 0) +{ + Write-Error "No logs to inspect." + return +} + +# +# Eventlog XPath query: optional date/time filtering +# +$dateTimeFilter = "" +if ($FromDateTime -or $ToDateTime) +{ + if ($FromDateTime) + { + $dateTimeFilter = "TimeCreated[@SystemTime>='" + $FromDateTime.ToUniversalTime().ToString("s") + "']" + if ($ToDateTime) + { + $dateTimeFilter += " and " + } + } + if ($ToDateTime) + { + $dateTimeFilter += "TimeCreated[@SystemTime<='" + $ToDateTime.ToUniversalTime().ToString("s") + "']" + } + + $dateTimeFilter = "($dateTimeFilter) and" +} + +# +# Event log XPath query: event IDs +# +$eventIdFilter = "$ExeDllWarning or $MsiScriptWarning or $ExeDllError or $MsiScriptError" +if ($WarningOnly) +{ + $eventIdFilter = "$ExeDllWarning or $MsiScriptWarning" +} +if ($ErrorOnly) +{ + $eventIdFilter = "$ExeDllError or $MsiScriptError" +} +if ($Allowed) +{ + $eventIdFilter = "$ExeDllAllowed or $MsiScriptAllowed" +} +if ($AllEvents) +{ + $eventIdFilter = "$ExeDllAllowed or $MsiScriptAllowed or $ExeDllWarning or $MsiScriptWarning or $ExeDllError or $MsiScriptError" +} +if ($ForwardedEvents -and !$NoFilteredMachines) +{ + $eventIdFilter += " or $SubscriptionBkmrk" +} +$eventIdFilter = "($eventIdFilter)" + +# +# Set the XPath filter for the event log(s) query +# +$filter = "*[System[$eventProviderFilter $dateTimeFilter $eventIdFilter]]" +### Use -Verbose to debug the FilterXPath +Write-Verbose "XPath filter = $filter" + +# +# More strings: patterns to look for +# +# Match partial path in temp directory with form XXXXXXXX.XXX.PS* or __PSScriptPolicyTest_XXXXXXXX.XXX.PS* +$PsPolicyTestPattern = "\\APPDATA\\LOCAL\\TEMP\\(__PSScriptPolicyTest_)?[A-Z0-9]{8}\.[A-Z0-9]{3}\.PS" +# Usage: +# if ($origPath -match $PsPolicyTestPattern) { $filterOut = !$NoPSFilter } +# New implementation: PS script policy test file is a one-byte file containing "1". Its SHA256 hash is 0x6B86B273FF34FCE19D6B804EFF5A3F5747ADA4EAA22F1D49C01E52DDB7875B4B +# Check for that hash rather than the filepath pattern. The hash will be reliable; the file pattern could give a false positive. +# (Perf test indicates no benefit of one test over the other.) +# NOTE: if the content of the test file changes, there will be a new hash value to test for. +# NOTE: The PowerShell folks started randomizing the content, so hash check no longer works - need to look for filename patterns. +# $PsPolicyTestFileHash = "0x6B86B273FF34FCE19D6B804EFF5A3F5747ADA4EAA22F1D49C01E52DDB7875B4B" +# Pattern was: if ($hash -eq $PsPolicyTestFileHash) ... + +# Match AutoNGEN native image file path +$AutoNGENPattern = "^(%OSDRIVE%|C:)\\Users\\[^\\]*\\AppData\\Local\\Packages\\.*\\NATIVEIMAGES\\.*\.NI\.(EXE|DLL)$" + +# Pattern that can be replaced by %LOCALAPPDATA% +$LocalAppDataPattern = "^(%OSDRIVE%|C:)\\Users\\[^\\]*\\AppData\\Local\\" +# Pattern that can be replaced by %APPDATA% +$RoamingAppDataPattern = "^(%OSDRIVE%|C:)\\Users\\[^\\]*\\AppData\\Roaming\\" +# Pattern that can be replaced by %USERPROFILE% (after the above already done) +$UserProfilePattern = "^(%OSDRIVE%|C:)\\Users\\[^\\]*\\" + +# Tab +$t = "`t" + +# Properties +$props = @() +if (!$NoGenericPath) { $props += "GenericPath" } +if (!$NoGenericDir) { $props += "GenericDir" } +if (!$NoOriginalPath) { $props += "OriginalPath" } +if (!$NoFileName) { $props += "FileName" } +if (!$NoFileExt) { $props += "FileExt" } +if (!$NoFileType) { $props += "FileType" } +if (!$NoPublisherName) { $props += "PublisherName" } +if (!$NoProductName) { $props += "ProductName" } +if (!$NoBinaryName) { $props += "BinaryName" } +if (!$NoFileVersion) { $props += "FileVersion" } +if (!$NoHash) { $props += "Hash" } +if (!$NoUserSID) { $props += "UserSID" } +if (!$NoUserName) { $props += "UserName" } +if (!$NoMachineName) { $props += "MachineName" } +if (!$NoEventTime) { $props += "EventTime" } +if (!$NoEventTimeXL) { $props += "EventTimeXL" } +if (!$NoPID) { $props += "PID" } +if (!$NoEventType) { $props += "EventType" } +$headers = $props -join $t + +# +# Retrieve events +# +$ev = @() +if ($EvtxLogFilePaths) +{ + $EvtxLogFilePaths | foreach { + Write-Host "Calling Get-WinEvent -Path $_ ..." -ForegroundColor Cyan + $ev += Get-WinEvent -Path $_ -FilterXPath $filter -ErrorAction SilentlyContinue -ErrorVariable gweErr + if ($gweErr.Count -gt 0) + { + $gweErr | foreach { Write-Host ("--> " + $_.ToString()) -ForegroundColor Cyan } + } + } +} +else +{ + $eventLogs | foreach { + if ($ComputerName) + { + Write-Host "Calling Get-WinEvent -LogName $_ -ComputerName $ComputerName ..." -ForegroundColor Cyan + $ev += (Get-WinEvent -LogName $_ -ComputerName $ComputerName -FilterXPath $filter -ErrorAction SilentlyContinue -ErrorVariable gweErr) + } + else + { + Write-Host "Calling Get-WinEvent -LogName $_ ..." -ForegroundColor Cyan + $ev += (Get-WinEvent -LogName $_ -FilterXPath $filter -ErrorAction SilentlyContinue -ErrorVariable gweErr) + } + if ($gweErr.Count -gt 0) + { + $gweErr | foreach { Write-Host ("--> " + $_.ToString()) -ForegroundColor Cyan } + } + } +} +Write-Host ($ev.Count.ToString() + " events retrieved.") -ForegroundColor Cyan + +#TODO: Figure out whether/when only one works, and why: Xml vs. Get-WinEvent objects +$UseXml = $true + +# +# Create output array; add CSV headers +# +$csv = @() +$csv += $headers + +# +# Lookups +# +$SidToName = @{} +$AllMachineNames = @{} +$ReportedMachines = @{} + +# +# Function performs SID-to-name lookups, stores results for later retrieval so the same SID isn't looked up more than once. +# +function SidToNameLookup([string]$sid) +{ + if ($SidToName.ContainsKey($sid)) + { + $SidToName[$sid] + } + else + { + $oSID = New-Object System.Security.Principal.SecurityIdentifier($sid) + $oUser = $null + try { $oUser = $oSID.Translate([System.Security.Principal.NTAccount]) } catch {} + if ($null -ne $oUser) + { + $name = $oUser.Value + } + elseif ($sid.EndsWith("-500")) + { + $name = "[[[built-in local admin]]]"; + } + else + { + $name = "[[[Not translated]]]" + } + $SidToName.Add($sid, $name) + $name + } +} + +# +# Produce output +# +$count = 0 +$filteredOut = 0 +$csv += ( + $ev | foreach { + + # Implement options to hide items that match PowerShell policy test script or AutoNGEN native images: + $filterOut = $false + + <# + Event ID 111 (should maybe also check $_.ProviderName -eq "Microsoft-Windows-EventForwarder") indicates an artificial + event created on the Windows event collector when a client system creates a subscription. + Use it to identify systems that were able to forward events but didn't. + Example event XML: + + + + + + 111 + + myworkstation.contoso.com + + + + + + #> + if ($_.Id -eq 111) + { + # Bookmark event + $filterOut = $true + } + else + { + $xEv = $null + $xData = $null + + if ($UseXml -or $null -eq $_.Properties[0]) + { + $xEv = [xml]($_.ToXml()) + $xData = $xEv.Event.UserData.RuleAndFileData + $origPath = $xData.FilePath + $hash = "0x" + $xData.FileHash + $sPID = $xData.TargetProcessId + } + else + { + $origPath = $_.Properties[10].Value # File path + $hash = "0x" + [System.BitConverter]::ToString( $_.Properties[12].Value ).Replace('-', '') + $sPID = $_.Properties[8].Value + } + + if ($origPath -match $PsPolicyTestPattern) + { + $filterOut = !$NoPSFilter + } + elseif ($origPath -match $AutoNGENPattern) + { + $filterOut = $NoAutoNGEN + } + } + + if ($filterOut) { $filteredOut++ } + + if (!$NoFilteredMachines) + { + # Observed machines + $machineName = $_.MachineName # Computer name + if (!$AllMachineNames.ContainsKey($machineName)) + { + # All observed machines + $AllMachineNames.Add($machineName, "") + } + if (!$filterOut -and !$ReportedMachines.ContainsKey($machineName)) + { + # Machines that have had data reported + $ReportedMachines.Add($machineName, "") + } + } + + if (!$filterOut) + { + $timeCreated = $_.TimeCreated.ToString("yyyy-MM-ddTHH:mm:ss.fffffff") # alpha sort = chronological sort; granularity = ten millionths of a second + $machineName = $_.MachineName # Computer name + + # Manual text conversion in case LevelDisplayName is not populated + if(![string]::IsNullOrEmpty($_.LevelDisplayName)){ + $eventType = $_.LevelDisplayName # Event type (Information, Warning, Error) + } + else{ + $eventType = switch($_.Level) + { + 1 { "Critical" } + 2 { "Error" } + 3 { "Warning" } + 4 { "Information" } + 5 { "Verbose" } + default { $_.Level.ToString() } + } + } + + if ($null -eq $xData) + { + $filetype = $_.Properties[1].Value # EXE, DLL, MSI, or SCRIPT + $userSid = $_.Properties[7].Value.Value # User SID (System.Security.Principal.SecurityIdentifier) + $pubInfo = $_.Properties[14].Value.Split("\") # Publisher info, separated with backslashes + # $hash = "0x" + [System.BitConverter]::ToString( $_.Properties[12].Value ).Replace('-', '') + } + else + { + $filetype = $xData.PolicyName + $userSid = $xData.TargetUser + $pubInfo = $xData.Fqbn.Split("\") + # $hash = "0x" + $xData.FileHash + } + + $pubName = $pubInfo[0] # Publisher name + $prodName = $pubInfo[1] # Product name (syntax works even if array not this long) + $binaryName = $pubInfo[2] # Original "binary" name (syntax works even if array not this long) + $filever = $pubInfo[3] # File version (syntax works even if array not this long) + $filename = [System.IO.Path]::GetFileName($origPath) + $fileext = [System.IO.Path]::GetExtension($origPath) + # Generic path replaces user-specific paths with more generic variable syntax. + # Userprofile has to be performed after more specific appdata replacements. + $genpath = (($origPath -replace $LocalAppDataPattern, "%LOCALAPPDATA%\") -replace $RoamingAppDataPattern, "%APPDATA%\") -replace $UserProfilePattern, "%USERPROFILE%\" + $gendir = [System.IO.Path]::GetDirectoryName($genpath) + + #Anyone wants objects, they can ConvertFrom-Csv. + $data = @() + if (!$NoGenericPath) { $data += $genpath } + if (!$NoGenericDir) { $data += $gendir } + if (!$NoOriginalPath) { $data += $origPath } + if (!$NoFileName) { $data += $filename } + if (!$NoFileExt) { $data += $fileext } + if (!$NoFileType) { $data += $filetype } + if (!$NoPublisherName) { $data += $pubName } + if (!$NoProductName) { $data += $prodName } + if (!$NoBinaryName) { $data += $binaryName } + if (!$NoFileVersion) { $data += $filever } + if (!$NoHash) { $data += $hash } + if (!$NoUserSID) { $data += $userSID } + if (!$NoUserName) { $data += SidToNameLookup $userSid } + if (!$NoMachineName) { $data += $machineName } + if (!$NoEventTime) { $data += $timeCreated } + #TODO: Verify that regional preferences don't interfere with making this useful... + if (!$NoEventTimeXL) { $data += $timeCreated.Replace("T", " ").Substring(0, 19) } + if (!$NoPID) { $data += $sPID } + if (!$NoEventType) { $data += $eventType } + # Output the data as CSV + $data -join $t + } + + $count++ + if ($count -eq 100) + { + Write-Host "." -NoNewline -ForegroundColor Cyan + $count = 0 + } + } | Sort-Object -Unique +) + +# +# Unless specified otherwise, also output "empty" events for machines for which all events were filtered out +# +if (!$NoFilteredMachines) +{ + $csv += ( + $AllMachineNames.Keys | Sort-Object | foreach { + $machineName = $_ + # If machine observed but not reported, report it now + if (!$ReportedMachines.ContainsKey($machineName)) + { + $data = @() + if (!$NoGenericPath) { $data += "" } + if (!$NoGenericDir) { $data += "" } + if (!$NoOriginalPath) { $data += "" } + if (!$NoFileName) { $data += "" } + if (!$NoFileExt) { $data += "" } + if (!$NoFileType) { $data += "NONE" } + if (!$NoPublisherName) { $data += "" } + if (!$NoProductName) { $data += "" } + if (!$NoBinaryName) { $data += "" } + if (!$NoFileVersion) { $data += "" } + if (!$NoHash) { $data += "" } + if (!$NoUserSID) { $data += "" } + if (!$NoUserName) { $data += "" } + if (!$NoMachineName) { $data += $machineName } + if (!$NoEventTime) { $data += "" } + if (!$NoEventTimeXL) { $data += "" } + if (!$NoPID) { $data += "" } + if (!$NoEventType) { $data += "FILTERED" } + # Output the data as CSV + $data -join $t + } + } + ) +} + +Write-Host "" # New line after the dots +Write-Host "$filteredOut events filtered out." -ForegroundColor Cyan + +if ($Excel) +{ + if (CreateExcelApplication) + { + AddWorksheetFromCsvData -csv $csv -tabname "AppLocker events" + ReleaseExcelApplication + } +} +elseif ($Objects) +{ + # Output PSCustomObjects to pipeline + $csv | ConvertFrom-Csv -Delimiter "`t" +} +else +{ + # Output tab-delimited CSV text to pipeline + $csv +} + +<# + Template for 8002, 8003, and 8004 events (and 8005, 8006, and 8007): + #> \ No newline at end of file diff --git a/AaronLocker/LocalConfiguration/ApplyPolicyToLocalGPO.ps1 b/AaronLockerScriptBased/AaronLocker/LocalConfiguration/ApplyPolicyToLocalGPO.ps1 similarity index 95% rename from AaronLocker/LocalConfiguration/ApplyPolicyToLocalGPO.ps1 rename to AaronLockerScriptBased/AaronLocker/LocalConfiguration/ApplyPolicyToLocalGPO.ps1 index 4a3e2ba..8648fb3 100644 --- a/AaronLocker/LocalConfiguration/ApplyPolicyToLocalGPO.ps1 +++ b/AaronLockerScriptBased/AaronLocker/LocalConfiguration/ApplyPolicyToLocalGPO.ps1 @@ -1,46 +1,46 @@ - -<# -.SYNOPSIS -Applies the most-recently generated AppLocker rules to local Group Policy. - -.DESCRIPTION -Applies the most recent generated "Audit" or "Enforce" rules to local Group Policy. -Applies the "Enforce" rules by default; to apply the "Audit" rules, use the -AuditOnly switch. -Requires administrative rights. - -.PARAMETER AuditOnly -If this switch is set, this script applies the "Audit" rules to local Group Policy. -If this switch is $false, this script applies the "Enforce" rules to local Group Policy. - -#> - -param( - # If set, applies auditing rules. Otherwise, applies enforcing rules. - [switch] - $AuditOnly = $false -) - -$rootDir = [System.IO.Path]::GetDirectoryName($MyInvocation.MyCommand.Path) + "\.." - -# Dot-source the config file. -. $rootDir\Support\Config.ps1 - -if ($AuditOnly) -{ - $policyFile = RulesFileAuditLatest -} -else -{ - $policyFile = RulesFileEnforceLatest -} - -if ($null -eq $policyFile) -{ - Write-Error "No policy file found" -} -else -{ - Write-Host "Applying $policyFile" -ForegroundColor Cyan - Set-AppLockerPolicy -XmlPolicy $policyFile -} - + +<# +.SYNOPSIS +Applies the most-recently generated AppLocker rules to local Group Policy. + +.DESCRIPTION +Applies the most recent generated "Audit" or "Enforce" rules to local Group Policy. +Applies the "Enforce" rules by default; to apply the "Audit" rules, use the -AuditOnly switch. +Requires administrative rights. + +.PARAMETER AuditOnly +If this switch is set, this script applies the "Audit" rules to local Group Policy. +If this switch is $false, this script applies the "Enforce" rules to local Group Policy. + +#> + +param( + # If set, applies auditing rules. Otherwise, applies enforcing rules. + [switch] + $AuditOnly = $false +) + +$rootDir = [System.IO.Path]::GetDirectoryName($MyInvocation.MyCommand.Path) + "\.." + +# Dot-source the config file. +. $rootDir\Support\Config.ps1 + +if ($AuditOnly) +{ + $policyFile = RulesFileAuditLatest +} +else +{ + $policyFile = RulesFileEnforceLatest +} + +if ($null -eq $policyFile) +{ + Write-Error "No policy file found" +} +else +{ + Write-Host "Applying $policyFile" -ForegroundColor Cyan + Set-AppLockerPolicy -XmlPolicy $policyFile +} + diff --git a/AaronLocker/LocalConfiguration/ClearApplockerLogs.ps1 b/AaronLockerScriptBased/AaronLocker/LocalConfiguration/ClearApplockerLogs.ps1 similarity index 97% rename from AaronLocker/LocalConfiguration/ClearApplockerLogs.ps1 rename to AaronLockerScriptBased/AaronLocker/LocalConfiguration/ClearApplockerLogs.ps1 index 788f7ec..85554d5 100644 --- a/AaronLocker/LocalConfiguration/ClearApplockerLogs.ps1 +++ b/AaronLockerScriptBased/AaronLocker/LocalConfiguration/ClearApplockerLogs.ps1 @@ -1,10 +1,10 @@ -<# -.SYNOPSIS -Clears events from local AppLocker event logs. -Requires administrative rights. -#> - -wevtutil.exe clear-log "Microsoft-Windows-AppLocker/EXE and DLL" -wevtutil.exe clear-log "Microsoft-Windows-AppLocker/MSI and Script" -wevtutil.exe clear-log "Microsoft-Windows-AppLocker/Packaged app-Deployment" -wevtutil.exe clear-log "Microsoft-Windows-AppLocker/Packaged app-Execution" +<# +.SYNOPSIS +Clears events from local AppLocker event logs. +Requires administrative rights. +#> + +wevtutil.exe clear-log "Microsoft-Windows-AppLocker/EXE and DLL" +wevtutil.exe clear-log "Microsoft-Windows-AppLocker/MSI and Script" +wevtutil.exe clear-log "Microsoft-Windows-AppLocker/Packaged app-Deployment" +wevtutil.exe clear-log "Microsoft-Windows-AppLocker/Packaged app-Execution" diff --git a/AaronLocker/LocalConfiguration/ClearLocalAppLockerPolicy.ps1 b/AaronLockerScriptBased/AaronLocker/LocalConfiguration/ClearLocalAppLockerPolicy.ps1 similarity index 98% rename from AaronLocker/LocalConfiguration/ClearLocalAppLockerPolicy.ps1 rename to AaronLockerScriptBased/AaronLocker/LocalConfiguration/ClearLocalAppLockerPolicy.ps1 index 2a15a2f..f054bf6 100644 --- a/AaronLocker/LocalConfiguration/ClearLocalAppLockerPolicy.ps1 +++ b/AaronLockerScriptBased/AaronLocker/LocalConfiguration/ClearLocalAppLockerPolicy.ps1 @@ -1,13 +1,13 @@ -<# -.SYNOPSIS -Revert local AppLocker policy to "not configured". -Requires administrative rights. -#> - -#################################################################################################### -# Ensure the AppLocker assembly is loaded. (Scripts sometimes run into TypeNotFound errors if not.) -#################################################################################################### -[void][System.Reflection.Assembly]::LoadWithPartialName("Microsoft.Security.ApplicationId.PolicyManagement.PolicyModel") - - +<# +.SYNOPSIS +Revert local AppLocker policy to "not configured". +Requires administrative rights. +#> + +#################################################################################################### +# Ensure the AppLocker assembly is loaded. (Scripts sometimes run into TypeNotFound errors if not.) +#################################################################################################### +[void][System.Reflection.Assembly]::LoadWithPartialName("Microsoft.Security.ApplicationId.PolicyManagement.PolicyModel") + + Set-AppLockerPolicy -PolicyObject ([Microsoft.Security.ApplicationId.PolicyManagement.PolicyModel.AppLockerPolicy]::new()) \ No newline at end of file diff --git a/AaronLocker/LocalConfiguration/ConfigureForAppLocker.ps1 b/AaronLockerScriptBased/AaronLocker/LocalConfiguration/ConfigureForAppLocker.ps1 similarity index 97% rename from AaronLocker/LocalConfiguration/ConfigureForAppLocker.ps1 rename to AaronLockerScriptBased/AaronLocker/LocalConfiguration/ConfigureForAppLocker.ps1 index a608b80..fdc4b78 100644 --- a/AaronLocker/LocalConfiguration/ConfigureForAppLocker.ps1 +++ b/AaronLockerScriptBased/AaronLocker/LocalConfiguration/ConfigureForAppLocker.ps1 @@ -1,44 +1,44 @@ -<# -.SYNOPSIS -Basic one-time single-computer configuration changes for AppLocker. -Requires administrative rights. - -.DESCRIPTION -Configures the Application Identity service (appidsvc) for automatic start -Starts the Application Identity service -Sets the maximum log size for each of the AppLocker event logs to 1GB. - -#> - -# Configure AppIdSvc for Automatic start -SC.EXE config AppIdSvc start= auto - -# Start the service if not already running -SC.exe start appidsvc - -# Set the primary AppLocker event log sizes to 1GB - -$logName = 'Microsoft-Windows-AppLocker/EXE and DLL' -$log = New-Object System.Diagnostics.Eventing.Reader.EventLogConfiguration $logName -$log.MaximumSizeInBytes = (1024 * 1024 * 1024) -$log.SaveChanges() - -$logName = 'Microsoft-Windows-AppLocker/MSI and Script' -$log = New-Object System.Diagnostics.Eventing.Reader.EventLogConfiguration $logName -$log.MaximumSizeInBytes = (1024 * 1024 * 1024) -$log.SaveChanges() - -#These event logs don't exist on Windows 7: ignore any errors. -try -{ - $logName = 'Microsoft-Windows-AppLocker/Packaged app-Deployment' - $log = New-Object System.Diagnostics.Eventing.Reader.EventLogConfiguration $logName - $log.MaximumSizeInBytes = (1024 * 1024 * 1024) - $log.SaveChanges() - - $logName = 'Microsoft-Windows-AppLocker/Packaged app-Execution' - $log = New-Object System.Diagnostics.Eventing.Reader.EventLogConfiguration $logName - $log.MaximumSizeInBytes = (1024 * 1024 * 1024) - $log.SaveChanges() -} -catch {} +<# +.SYNOPSIS +Basic one-time single-computer configuration changes for AppLocker. +Requires administrative rights. + +.DESCRIPTION +Configures the Application Identity service (appidsvc) for automatic start +Starts the Application Identity service +Sets the maximum log size for each of the AppLocker event logs to 1GB. + +#> + +# Configure AppIdSvc for Automatic start +SC.EXE config AppIdSvc start= auto + +# Start the service if not already running +SC.exe start appidsvc + +# Set the primary AppLocker event log sizes to 1GB + +$logName = 'Microsoft-Windows-AppLocker/EXE and DLL' +$log = New-Object System.Diagnostics.Eventing.Reader.EventLogConfiguration $logName +$log.MaximumSizeInBytes = (1024 * 1024 * 1024) +$log.SaveChanges() + +$logName = 'Microsoft-Windows-AppLocker/MSI and Script' +$log = New-Object System.Diagnostics.Eventing.Reader.EventLogConfiguration $logName +$log.MaximumSizeInBytes = (1024 * 1024 * 1024) +$log.SaveChanges() + +#These event logs don't exist on Windows 7: ignore any errors. +try +{ + $logName = 'Microsoft-Windows-AppLocker/Packaged app-Deployment' + $log = New-Object System.Diagnostics.Eventing.Reader.EventLogConfiguration $logName + $log.MaximumSizeInBytes = (1024 * 1024 * 1024) + $log.SaveChanges() + + $logName = 'Microsoft-Windows-AppLocker/Packaged app-Execution' + $log = New-Object System.Diagnostics.Eventing.Reader.EventLogConfiguration $logName + $log.MaximumSizeInBytes = (1024 * 1024 * 1024) + $log.SaveChanges() +} +catch {} diff --git a/AaronLocker/MergeRules-Static/Allow-GoogleChromeFlashPlayer.xml b/AaronLockerScriptBased/AaronLocker/MergeRules-Static/Allow-GoogleChromeFlashPlayer.xml similarity index 100% rename from AaronLocker/MergeRules-Static/Allow-GoogleChromeFlashPlayer.xml rename to AaronLockerScriptBased/AaronLocker/MergeRules-Static/Allow-GoogleChromeFlashPlayer.xml diff --git a/AaronLocker/MergeRules-Static/Deny-OldBginfo.xml b/AaronLockerScriptBased/AaronLocker/MergeRules-Static/Deny-OldBginfo.xml similarity index 98% rename from AaronLocker/MergeRules-Static/Deny-OldBginfo.xml rename to AaronLockerScriptBased/AaronLocker/MergeRules-Static/Deny-OldBginfo.xml index 89d556d..b456b30 100644 --- a/AaronLocker/MergeRules-Static/Deny-OldBginfo.xml +++ b/AaronLockerScriptBased/AaronLocker/MergeRules-Static/Deny-OldBginfo.xml @@ -1,11 +1,11 @@ - - - - - - - - - - + + + + + + + + + + \ No newline at end of file diff --git a/AaronLocker/MergeRules-Static/OneDrive-InitialInstall-Win10v1607.xml b/AaronLockerScriptBased/AaronLocker/MergeRules-Static/OneDrive-InitialInstall-Win10v1607.xml similarity index 100% rename from AaronLocker/MergeRules-Static/OneDrive-InitialInstall-Win10v1607.xml rename to AaronLockerScriptBased/AaronLocker/MergeRules-Static/OneDrive-InitialInstall-Win10v1607.xml diff --git a/AaronLocker/MergeRules-Static/OneDrive-InitialInstall-Win10v1803.xml b/AaronLockerScriptBased/AaronLocker/MergeRules-Static/OneDrive-InitialInstall-Win10v1803.xml similarity index 100% rename from AaronLocker/MergeRules-Static/OneDrive-InitialInstall-Win10v1803.xml rename to AaronLockerScriptBased/AaronLocker/MergeRules-Static/OneDrive-InitialInstall-Win10v1803.xml diff --git a/AaronLocker/MergeRules-Static/OneDrive-InitialInstall-Win10v1809.xml b/AaronLockerScriptBased/AaronLocker/MergeRules-Static/OneDrive-InitialInstall-Win10v1809.xml similarity index 100% rename from AaronLocker/MergeRules-Static/OneDrive-InitialInstall-Win10v1809.xml rename to AaronLockerScriptBased/AaronLocker/MergeRules-Static/OneDrive-InitialInstall-Win10v1809.xml diff --git a/AaronLocker/Save-WEFEvents.ps1 b/AaronLockerScriptBased/AaronLocker/Save-WEFEvents.ps1 similarity index 96% rename from AaronLocker/Save-WEFEvents.ps1 rename to AaronLockerScriptBased/AaronLocker/Save-WEFEvents.ps1 index 580ab86..b205949 100644 --- a/AaronLocker/Save-WEFEvents.ps1 +++ b/AaronLockerScriptBased/AaronLocker/Save-WEFEvents.ps1 @@ -1,56 +1,56 @@ -<# -.SYNOPSIS -Captures forwarded events to a CSV file with the timestamp embedded in the file name. -Intended to be executed on a Windows Event Collector server. - -.PARAMETER rootdir -Directory in which to create the CSV file. - -.PARAMETER daysBack -Number of days ago from which to start retrieving data. E.g., "-daysBack 5" pulls data from the last 5 days. - -.PARAMETER label -Name to insert into the filename (can be useful to distinguish sources). - -#> - -param( - [parameter(Mandatory=$false)] - [string] - $rootdir, - - [parameter(Mandatory=$false)] - [int] - $daysBack = 0, - - [parameter(Mandatory=$false)] - [string] - $label = "" -) - -if (!$rootdir) { $rootdir = "." } - -$strTimestamp = [datetime]::Now.ToString("yyyyMMdd-HHmm") -if ($label.Length -eq 0) -{ - $filenameFull = [System.IO.Path]::Combine($rootdir, "ForwardedEvents-" + $strTimestamp + ".csv") -} -else -{ - $filenameFull = [System.IO.Path]::Combine($rootdir, "ForwardedEvents-" + $label + "-" + $strTimestamp + ".csv") -} - -$OutputEncoding = [System.Text.ASCIIEncoding]::Unicode - -if ($daysBack -gt 0) -{ - $csvFull = .\Get-AppLockerEvents.ps1 -ForwardedEvents -NoAutoNGEN -FromDateTime ([datetime]::Now.AddDays(-$daysBack)) -} -else -{ - $csvFull = .\Get-AppLockerEvents.ps1 -ForwardedEvents -NoAutoNGEN -} - -$csvFull | Out-File -Encoding unicode $filenameFull -Write-Host "Events written to $filenameFull" -ForegroundColor Cyan - +<# +.SYNOPSIS +Captures forwarded events to a CSV file with the timestamp embedded in the file name. +Intended to be executed on a Windows Event Collector server. + +.PARAMETER rootdir +Directory in which to create the CSV file. + +.PARAMETER daysBack +Number of days ago from which to start retrieving data. E.g., "-daysBack 5" pulls data from the last 5 days. + +.PARAMETER label +Name to insert into the filename (can be useful to distinguish sources). + +#> + +param( + [parameter(Mandatory=$false)] + [string] + $rootdir, + + [parameter(Mandatory=$false)] + [int] + $daysBack = 0, + + [parameter(Mandatory=$false)] + [string] + $label = "" +) + +if (!$rootdir) { $rootdir = "." } + +$strTimestamp = [datetime]::Now.ToString("yyyyMMdd-HHmm") +if ($label.Length -eq 0) +{ + $filenameFull = [System.IO.Path]::Combine($rootdir, "ForwardedEvents-" + $strTimestamp + ".csv") +} +else +{ + $filenameFull = [System.IO.Path]::Combine($rootdir, "ForwardedEvents-" + $label + "-" + $strTimestamp + ".csv") +} + +$OutputEncoding = [System.Text.ASCIIEncoding]::Unicode + +if ($daysBack -gt 0) +{ + $csvFull = .\Get-AppLockerEvents.ps1 -ForwardedEvents -NoAutoNGEN -FromDateTime ([datetime]::Now.AddDays(-$daysBack)) +} +else +{ + $csvFull = .\Get-AppLockerEvents.ps1 -ForwardedEvents -NoAutoNGEN +} + +$csvFull | Out-File -Encoding unicode $filenameFull +Write-Host "Events written to $filenameFull" -ForegroundColor Cyan + diff --git a/AaronLocker/Scan-Directories.ps1 b/AaronLockerScriptBased/AaronLocker/Scan-Directories.ps1 similarity index 97% rename from AaronLocker/Scan-Directories.ps1 rename to AaronLockerScriptBased/AaronLocker/Scan-Directories.ps1 index 2d81fc0..acf2633 100644 --- a/AaronLocker/Scan-Directories.ps1 +++ b/AaronLockerScriptBased/AaronLocker/Scan-Directories.ps1 @@ -1,528 +1,528 @@ -<# -.SYNOPSIS -Scan directories to identify files that might need additional AppLocker rules. - -.DESCRIPTION -Produces tab-delimited CSV or an Excel worksheet listing files in various directories that might need additional AppLocker rules to allow them to execute. -Optionally, the script can list non-standard directories in the %SystemDrive% root directory. These directories might require additional scanning. - -The script searches specified directory hierarchies for MSIs and scripts (according to file extension), and EXE/DLL files regardless of extension. That is, a file can be identified as a Portable Executable (PE) file (typically an EXE or DLL) even if it has a non-standard extension or no extension. - -Output columns include: -* IsSafeDir - indicates whether the file's parent directory is "safe" (not user-writable) or "unsafe" (user-writable); -* File type - EXE/DLL, MSI, or Script; -* File extension - the file's extension; -* File name - the file name without path information; -* File path - Full path to the file; -* Parent directory - The file's parent directory; -* Publisher name, Product name - signature and product name that can be used in publisher rules; -* Hash - the file's hash; -* CreationTime, LastAccessTime, LastWriteTime - the file's timestamps according to the file system; -* File size. - -Directories that can be searched: -* WritableWindir - writable subdirectories of the %windir% directory, based on results of the last scan performed by Create-Policies.ps1; -* WritablePF - writable subdirectories of the %ProgramFiles% directories, based on results of the last scan performed by Create-Policies.ps1; -* SearchProgramData - the %ProgramData% directory hierarchy; -* SearchOneUserProfile - the current user's profile directory; -* SearchAllUserProfiles - the root directory of user profiles (C:\Users); -* DirsToSearch - one or more caller-specified, comma-separated directory paths. - -Results can be imported into Microsoft Excel and analyzed. - -Note that results from this script do not necessarily require that rules be created: -this is just an indicator about files that *might* need rules, if the files need to be allowed. - -.PARAMETER WritableWindir -If this switch is specified, searches user-writable subdirectories under %windir% according to results of the last scan performed by Create-Policies.ps1. - -.PARAMETER WritablePF -If this switch is specified, searches user-writable subdirectories under the %ProgramFiles% directories according to results of the last scan performed by Create-Policies.ps1. - -.PARAMETER SearchProgramData -If this switch is specified, searches the %ProgramData% directory hierarchy, which can contain a mix of "safe" and "unsafe" directories. - -.PARAMETER SearchOneUserProfile -If this switch is specified, searches the user's profile directory. - -.PARAMETER SearchAllUserProfiles -If this switch is specified, searches from the root directory of all users' profiles (C:\Users) - -.PARAMETER DirsToSearch -Specifies one or more directories to search. - -.PARAMETER NoPEFiles -If this switch is specified, does not search for Portable Executable files (EXE/DLL files) - -.PARAMETER NoScripts -If this switch is specified, does not search for script files. - -.PARAMETER NoMSIs -If this switch is specified, does not search for MSI files. - -.PARAMETER DirectoryNamesOnly -If this switch is specified, reports the names and "safety" of directories that contain files of interest but no file information. - -.PARAMETER Excel -If this switch is specified, outputs to formatted Excel worksheet instead of to pipeline - -.PARAMETER FindNonDefaultRootDirs -If this switch is specified, identifies non-standard directories in the %SystemDrive% root directory. These directories often contain LOB applications. -This switch cannot be used with any other options - -.PARAMETER Verbose -Shows progress through directory scans, and other verbose diagnostics. - - -.EXAMPLE -Scan-Directories.ps1 -SearchOneUserProfile -DirsToSearch H:\ - -Searches the user's profile directory and the H: drive. - -#> - -#TODO: Find a way to miss the .js false-positives, including but not only in browser caches. -#TODO: Skip .js in browser temp caches (IE on Win10: localappdata\Microsoft\Windows\INetCache) - possibly obviated by not looking at .js -#TODO: Maybe offer an option not to exclude .js; could be useful outside of user profiles? Maybe include .js for some directory types and not others. -#TODO: Distinguish between Exe and Dll files based on IMAGE_FILE_HEADER characteristics. - -param( - [parameter(ParameterSetName="SearchDirectories")] - [switch] - $WritableWindir = $false, - - [parameter(ParameterSetName="SearchDirectories")] - [switch] - $WritablePF = $false, - - [parameter(ParameterSetName="SearchDirectories")] - [switch] - $SearchProgramData = $false, - - [parameter(ParameterSetName="SearchDirectories")] - [switch] - $SearchOneUserProfile = $false, - - [parameter(ParameterSetName="SearchDirectories")] - [switch] - $SearchAllUserProfiles = $false, - - [parameter(ParameterSetName="SearchDirectories", Mandatory=$false)] - [String[]] - $DirsToSearch, - - [parameter(ParameterSetName="SearchDirectories")] - [switch] - $NoPEFiles = $false, - - [parameter(ParameterSetName="SearchDirectories")] - [switch] - $NoScripts = $false, - - [parameter(ParameterSetName="SearchDirectories")] - [switch] - $NoMSIs = $false, - - [parameter(ParameterSetName="SearchDirectories")] - [switch] - $DirectoryNamesOnly = $false, - - [parameter(ParameterSetName="SearchDirectories")] - [switch] - $Excel = $false, - - [parameter(ParameterSetName="NonDefaultRootDirs")] - [switch] - $FindNonDefaultRootDirs = $false -) - -### ====================================================================== - -Set-StrictMode -Version Latest - - -### ====================================================================== -### The FindNonDefaultRootDirs is a standalone option that cannot be used with other switches. -### It searches the SystemDrive root directory and enumerates non-default directory names. -if ($FindNonDefaultRootDirs) -{ - $defaultRootDirs = - '$Recycle.Bin', - 'Config.Msi', - 'MSOCache', - 'MSOTraceLite', - 'OneDriveTemp', - 'PerfLogs', - 'Program Files', - 'Program Files (x86)', - 'ProgramData', - 'Recovery', - 'System Volume Information', - 'Users', - 'Windows', - 'Windows.old' - - # Enumerate root-level directories whether hidden or not, but exclude junctions and symlinks. - # Output the ones that don't exist in a default Windows installation. - Get-ChildItem -Directory -Force ($env:SystemDrive + "\") | - Where-Object { !$_.Attributes.HasFlag([System.IO.FileAttributes]::ReparsePoint) -and !($_ -in $defaultRootDirs) } | - foreach { $_.FullName } - - return -} - - -### ====================================================================== -### Inspect files for PE properties (on the cheap!) -### If it's 64 bytes or more, and the first two are "MZ", we're calling it a PE file. -### $file is a System.IO.FileInfo object. -function IsExecutable($file) -{ - #Write-Host $file.FullName -ForegroundColor Cyan - if ($file.Length -lt 64) - { - return $false - } - - $mzHeader = Get-Content -LiteralPath $file.FullName -TotalCount 2 -Encoding Byte -ErrorAction SilentlyContinue - - # 0x4D = 'M', 0x5A = 'Z' - return $null -ne $mzHeader -and ($mzHeader[0] -eq 0x4D -and $mzHeader[1] -eq 0x5A) -} - -### ====================================================================== - -$rootDir = [System.IO.Path]::GetDirectoryName($MyInvocation.MyCommand.Path) -# Dot-source the config file. -. $rootDir\Support\Config.ps1 - -# Define some constants -Set-Variable UnsafeDir -option Constant -value "UnsafeDir" -Set-Variable SafeDir -option Constant -value "SafeDir" -Set-Variable UnknownDir -option Constant -value "UnknownDir" - - -$scriptExtensions = - ".bat", - ".cmd", - # ".js", ### Too many false positives; these are almost always executed within programs that do not restrict .js. - ".vbs", - ".wsf", - ".wsh", - ".ps1" -$MsiExtensions = - ".msi", - ".msp", - ".mst" - -# Hashtable: key is path to inspect; value is indicator whether safe/unsafe -$dirsToInspect = @{} - -# Writable directories under \Windows; known to be unsafe paths -if ($WritableWindir) -{ - if (!(Test-Path($windirTxt))) - { - Write-Warning "$windirTxt does not exist yet. Run Create-Policies.ps1." - } - else - { - Get-Content $windirTxt | foreach { - $dirsToInspect.Add($_, $UnsafeDir) - } - } -} - -# Writable directories under ProgramFiles; known to be unsafe paths -if ($WritablePF) -{ - if (!(Test-Path($PfTxt))) - { - Write-Warning "$PfTxt does not exist yet. Run Create-Policies.ps1." - } - elseif (!(Test-Path($Pf86Txt))) - { - Write-Warning "$Pf86Txt does not exist yet. Run Create-Policies.ps1." - } - else - { - Get-Content $PfTxt, $Pf86Txt | foreach { - $dirsToInspect.Add($_, $UnsafeDir) - } - } -} - -if ($SearchProgramData) -{ - # Probably a mix of safe and unsafe paths - $dirsToInspect.Add($env:ProgramData, $UnknownDir) -} - -if ($SearchOneUserProfile) -{ - #Assume all unsafe paths - #TODO: Skip browser-cache temp directories - $dirsToInspect.Add($env:USERPROFILE, $UnsafeDir) -} - -if ($SearchAllUserProfiles) -{ - #Assume all unsafe paths - # No special folder or environment variable available. Get root directory from parent directory of user profile directory - $rootdir = [System.IO.Path]::GetDirectoryName($env:USERPROFILE) - #TODO: Skip browser-cache temp directories - # Skip app-compat juntions (most disallow FILE_LIST_DIRECTORY) - # Skip symlinks -- "All Users" is a symlinkd for \ProgramData but unlike most app-compat junctions it can be listed/traversed. - # This code prevents that. - Get-ChildItem -Force -Directory C:\Users | Where-Object { !$_.Attributes.HasFlag([System.IO.FileAttributes]::ReparsePoint) } | foreach { - $dirsToInspect.Add($_.FullName, $UnsafeDir) - } -} - -if ($DirsToSearch) -{ - $DirsToSearch | foreach { $dirsToInspect.Add($_, $UnknownDir) } -} - -$csv = @() - -# Output column headers -if ($DirectoryNamesOnly) -{ - $csv += - "IsSafeDir" + "`t" + - "Parent directory" -} -else -{ - $csv += - "IsSafeDir" + "`t" + - "File type" + "`t" + - "File extension" + "`t" + - "File name" + "`t" + - "File path" + "`t" + - "Parent directory" + "`t" + - "Publisher name" + "`t" + - "Product name" + "`t" + - "Hash" + "`t" + - "CreationTime" + "`t" + - "LastAccessTime" + "`t" + - "LastWriteTime" + "`t" + - "File size" -} - -function InspectFiles([string]$directory, [string]$safety, [ref] [string[]]$writableDirs) -{ - $doNoMore = $false - - Get-ChildItem -File $directory -Force -ErrorAction SilentlyContinue -PipelineVariable file | foreach { - - # Work around Get-AppLockerFileInformation bug that vomits on zero-length input files - if ($_.Length -gt 0 -and !$doNoMore) - { - $filetype = $null - if ((!($NoScripts)) -and ($file.Extension -in $scriptExtensions)) - { - $filetype = "Script" - } - elseif ((!($NoMSIs)) -and ($file.Extension -in $MsiExtensions)) - { - $filetype = "MSI" - } - elseif ((!($NoPEFiles) -and (IsExecutable($file)))) - { - $filetype = "EXE/DLL" - } - - # Output - if ($null -ne $filetype) - { - $fullname = $file.FullName - $fileext = $file.Extension - $filename = $file.Name - $parentDir = [System.IO.Path]::GetDirectoryName($fullname) - $pubName = $prodName = [String]::Empty - $alfi = Get-AppLockerFileInformation $file.FullName -ErrorAction SilentlyContinue -ErrorVariable alfiErr - # Diagnostics. Seeing sharing violations on some operations - if ($alfiErr.Count -gt 0) - { - Write-Host ($file.FullName + "`tLength = " + $file.Length.ToString()) -ForegroundColor Yellow -BackgroundColor Black - $alfiErr | foreach { Write-Host $_.Exception -ForegroundColor Red -BackgroundColor Black} - } - if ($null -ne $alfi) - { - $pub = $alfi.Publisher - if ($null -ne $pub) - { - $pubName = $pub.PublisherName - $prodName = $pub.ProductName - } - $hash = $alfi.Hash.HashDataString - } - $safetyOut = $safety - if ($safety -eq $UnknownDir) - { - #$dbgInfo = $fullname + "`t" + $parentDir - if ($parentDir -in $writableDirs.Value) - { - #$dbgInfo = $UnsafeDir + "`t" + $dbgInfo - $safetyOut = $UnsafeDir - } - else - { - #$dbgInfo = ($SafeDir + "`t" + $dbgInfo) - $safetyOut = $SafeDir - } - #$dbgInfo - } - - if ($DirectoryNamesOnly) - { - $safetyOut + "`t" + - $parentDir - - # Found one file - don't need to continue inspection of files in this directory - $doNoMore = $true - } - else - { - $safetyOut + "`t" + - $filetype + "`t" + - $fileext + "`t" + - $filename + "`t" + - $fullname + "`t" + - $parentDir + "`t" + - $pubName + "`t" + - $prodName + "`t" + - $hash + "`t" + - $file.CreationTime + "`t" + - $file.LastAccessTime + "`t" + - $file.LastWriteTime + "`t" + - $file.Length - } - } - } - } -} - -function InspectDirectories([string]$directory, [string]$safety, [ref][string[]]$writableDirs) -{ - InspectFiles $directory $safety $writableDirs - - Get-ChildItem -Directory $directory -Force -ErrorAction SilentlyContinue | foreach { - $subdir = $_ - # Decide here whether to recurse into the subdirectory: - # * Skip junctions and symlinks (typically app-compat junctions). - # * Can add criteria here to skip browser caches, etc. - if (!$subdir.Attributes.HasFlag([System.IO.FileAttributes]::ReparsePoint)) - { - Write-Verbose ("... " + $subdir.FullName) - InspectDirectories $subdir.FullName $safety $writableDirs - } - else - { - Write-Verbose ("SKIPPING " + $subdir.FullName) - } - } -} - - -# Scanning requires that AccessChk.exe be available. -# If accesschk.exe is in the rootdir, temporarily add the rootdir to the path. -# (Previous implementation invoked Get-Command to see whether accesschk.exe was in the path, and only if that failed looked for -# accesschk.exe in the rootdir. However, there was no good way to keep Get-Command from displaying a "Suggestion" message in that -# scenario.) -# Variable for restoring original Path, if necessary. -$origPath = "" -# Check for accesschk.exe in the rootdir. -if (Test-Path -Path $rootDir\AccessChk.exe) -{ - # Found it in this script's directory. Temporarily prepend it to the path. - $origPath = $env:Path - $env:Path = "$rootDir;" + $origPath -} - -# Exclude known admins from analysis -$knownAdmins = @() -$knownAdmins += & $ps1_KnownAdmins - -# Capture into hash tables, separate file name, type, and parent path -$dirsToInspect.Keys | foreach { - - $dirToInspect = $_ - $safety = $dirsToInspect[$dirToInspect] - if ($safety -eq $UnknownDir) - { - Write-Host "about to inspect $dirToInspect for writable directories..." -ForegroundColor Cyan - if ((Get-Command AccessChk.exe -ErrorAction SilentlyContinue) -eq $null) - { - $errMsg = "Scanning for writable subdirectories requires that Sysinternals AccessChk.exe be in the Path or in the same directory with this script.`n" + - "AccessChk.exe was not found.`n" + - "(See .\Support\DownloadAccesschk.ps1 for help.)`n" + - "Exiting..." - Write-Error $errMsg - return - } - $writableDirs = [ref] ( & $ps1_EnumWritableDirs -RootDirectory $dirToInspect -KnownAdmins $knownAdmins) - if ($null -eq $writableDirs) - { - $writableDirs = [ref]@() - } - } - else - { - $writableDirs = [ref]@() - } - - Write-Host "About to inspect $dirToInspect..." -ForegroundColor Cyan - $csv += InspectDirectories $dirToInspect $safety $writableDirs -} - -# Restore original Path if it was altered for AccessChk.exe -if ($origPath.Length -gt 0) -{ - $env:Path = $origPath -} - - -if ($Excel) -{ - $OutputEncodingPrevious = $OutputEncoding - $OutputEncoding = [System.Text.ASCIIEncoding]::Unicode - - $tempfile = [System.IO.Path]::GetTempFileName() - - $tabname = "Consider for potential rules" - - $csv | Out-File $tempfile -Encoding unicode - - CreateExcelFromCsvFile $tempfile $tabname # $linebreakSeq - - Remove-Item $tempfile - - $OutputEncoding = $OutputEncodingPrevious -} -else -{ - # Just output the CSV raw - $csv -} - - -<# Informational: - - Get-AppLockerFileInformation -Directory searches for these file extensions: - *.com - *.exe - *.dll - *.ocx - *.msi - *.msp - *.mst - *.bat - *.cmd - *.js - *.ps1 - *.vbs - *.appx +<# +.SYNOPSIS +Scan directories to identify files that might need additional AppLocker rules. + +.DESCRIPTION +Produces tab-delimited CSV or an Excel worksheet listing files in various directories that might need additional AppLocker rules to allow them to execute. +Optionally, the script can list non-standard directories in the %SystemDrive% root directory. These directories might require additional scanning. + +The script searches specified directory hierarchies for MSIs and scripts (according to file extension), and EXE/DLL files regardless of extension. That is, a file can be identified as a Portable Executable (PE) file (typically an EXE or DLL) even if it has a non-standard extension or no extension. + +Output columns include: +* IsSafeDir - indicates whether the file's parent directory is "safe" (not user-writable) or "unsafe" (user-writable); +* File type - EXE/DLL, MSI, or Script; +* File extension - the file's extension; +* File name - the file name without path information; +* File path - Full path to the file; +* Parent directory - The file's parent directory; +* Publisher name, Product name - signature and product name that can be used in publisher rules; +* Hash - the file's hash; +* CreationTime, LastAccessTime, LastWriteTime - the file's timestamps according to the file system; +* File size. + +Directories that can be searched: +* WritableWindir - writable subdirectories of the %windir% directory, based on results of the last scan performed by Create-Policies.ps1; +* WritablePF - writable subdirectories of the %ProgramFiles% directories, based on results of the last scan performed by Create-Policies.ps1; +* SearchProgramData - the %ProgramData% directory hierarchy; +* SearchOneUserProfile - the current user's profile directory; +* SearchAllUserProfiles - the root directory of user profiles (C:\Users); +* DirsToSearch - one or more caller-specified, comma-separated directory paths. + +Results can be imported into Microsoft Excel and analyzed. + +Note that results from this script do not necessarily require that rules be created: +this is just an indicator about files that *might* need rules, if the files need to be allowed. + +.PARAMETER WritableWindir +If this switch is specified, searches user-writable subdirectories under %windir% according to results of the last scan performed by Create-Policies.ps1. + +.PARAMETER WritablePF +If this switch is specified, searches user-writable subdirectories under the %ProgramFiles% directories according to results of the last scan performed by Create-Policies.ps1. + +.PARAMETER SearchProgramData +If this switch is specified, searches the %ProgramData% directory hierarchy, which can contain a mix of "safe" and "unsafe" directories. + +.PARAMETER SearchOneUserProfile +If this switch is specified, searches the user's profile directory. + +.PARAMETER SearchAllUserProfiles +If this switch is specified, searches from the root directory of all users' profiles (C:\Users) + +.PARAMETER DirsToSearch +Specifies one or more directories to search. + +.PARAMETER NoPEFiles +If this switch is specified, does not search for Portable Executable files (EXE/DLL files) + +.PARAMETER NoScripts +If this switch is specified, does not search for script files. + +.PARAMETER NoMSIs +If this switch is specified, does not search for MSI files. + +.PARAMETER DirectoryNamesOnly +If this switch is specified, reports the names and "safety" of directories that contain files of interest but no file information. + +.PARAMETER Excel +If this switch is specified, outputs to formatted Excel worksheet instead of to pipeline + +.PARAMETER FindNonDefaultRootDirs +If this switch is specified, identifies non-standard directories in the %SystemDrive% root directory. These directories often contain LOB applications. +This switch cannot be used with any other options + +.PARAMETER Verbose +Shows progress through directory scans, and other verbose diagnostics. + + +.EXAMPLE +Scan-Directories.ps1 -SearchOneUserProfile -DirsToSearch H:\ + +Searches the user's profile directory and the H: drive. + +#> + +#TODO: Find a way to miss the .js false-positives, including but not only in browser caches. +#TODO: Skip .js in browser temp caches (IE on Win10: localappdata\Microsoft\Windows\INetCache) - possibly obviated by not looking at .js +#TODO: Maybe offer an option not to exclude .js; could be useful outside of user profiles? Maybe include .js for some directory types and not others. +#TODO: Distinguish between Exe and Dll files based on IMAGE_FILE_HEADER characteristics. + +param( + [parameter(ParameterSetName="SearchDirectories")] + [switch] + $WritableWindir = $false, + + [parameter(ParameterSetName="SearchDirectories")] + [switch] + $WritablePF = $false, + + [parameter(ParameterSetName="SearchDirectories")] + [switch] + $SearchProgramData = $false, + + [parameter(ParameterSetName="SearchDirectories")] + [switch] + $SearchOneUserProfile = $false, + + [parameter(ParameterSetName="SearchDirectories")] + [switch] + $SearchAllUserProfiles = $false, + + [parameter(ParameterSetName="SearchDirectories", Mandatory=$false)] + [String[]] + $DirsToSearch, + + [parameter(ParameterSetName="SearchDirectories")] + [switch] + $NoPEFiles = $false, + + [parameter(ParameterSetName="SearchDirectories")] + [switch] + $NoScripts = $false, + + [parameter(ParameterSetName="SearchDirectories")] + [switch] + $NoMSIs = $false, + + [parameter(ParameterSetName="SearchDirectories")] + [switch] + $DirectoryNamesOnly = $false, + + [parameter(ParameterSetName="SearchDirectories")] + [switch] + $Excel = $false, + + [parameter(ParameterSetName="NonDefaultRootDirs")] + [switch] + $FindNonDefaultRootDirs = $false +) + +### ====================================================================== + +Set-StrictMode -Version Latest + + +### ====================================================================== +### The FindNonDefaultRootDirs is a standalone option that cannot be used with other switches. +### It searches the SystemDrive root directory and enumerates non-default directory names. +if ($FindNonDefaultRootDirs) +{ + $defaultRootDirs = + '$Recycle.Bin', + 'Config.Msi', + 'MSOCache', + 'MSOTraceLite', + 'OneDriveTemp', + 'PerfLogs', + 'Program Files', + 'Program Files (x86)', + 'ProgramData', + 'Recovery', + 'System Volume Information', + 'Users', + 'Windows', + 'Windows.old' + + # Enumerate root-level directories whether hidden or not, but exclude junctions and symlinks. + # Output the ones that don't exist in a default Windows installation. + Get-ChildItem -Directory -Force ($env:SystemDrive + "\") | + Where-Object { !$_.Attributes.HasFlag([System.IO.FileAttributes]::ReparsePoint) -and !($_ -in $defaultRootDirs) } | + foreach { $_.FullName } + + return +} + + +### ====================================================================== +### Inspect files for PE properties (on the cheap!) +### If it's 64 bytes or more, and the first two are "MZ", we're calling it a PE file. +### $file is a System.IO.FileInfo object. +function IsExecutable($file) +{ + #Write-Host $file.FullName -ForegroundColor Cyan + if ($file.Length -lt 64) + { + return $false + } + + $mzHeader = Get-Content -LiteralPath $file.FullName -TotalCount 2 -Encoding Byte -ErrorAction SilentlyContinue + + # 0x4D = 'M', 0x5A = 'Z' + return $null -ne $mzHeader -and ($mzHeader[0] -eq 0x4D -and $mzHeader[1] -eq 0x5A) +} + +### ====================================================================== + +$rootDir = [System.IO.Path]::GetDirectoryName($MyInvocation.MyCommand.Path) +# Dot-source the config file. +. $rootDir\Support\Config.ps1 + +# Define some constants +Set-Variable UnsafeDir -option Constant -value "UnsafeDir" +Set-Variable SafeDir -option Constant -value "SafeDir" +Set-Variable UnknownDir -option Constant -value "UnknownDir" + + +$scriptExtensions = + ".bat", + ".cmd", + # ".js", ### Too many false positives; these are almost always executed within programs that do not restrict .js. + ".vbs", + ".wsf", + ".wsh", + ".ps1" +$MsiExtensions = + ".msi", + ".msp", + ".mst" + +# Hashtable: key is path to inspect; value is indicator whether safe/unsafe +$dirsToInspect = @{} + +# Writable directories under \Windows; known to be unsafe paths +if ($WritableWindir) +{ + if (!(Test-Path($windirTxt))) + { + Write-Warning "$windirTxt does not exist yet. Run Create-Policies.ps1." + } + else + { + Get-Content $windirTxt | foreach { + $dirsToInspect.Add($_, $UnsafeDir) + } + } +} + +# Writable directories under ProgramFiles; known to be unsafe paths +if ($WritablePF) +{ + if (!(Test-Path($PfTxt))) + { + Write-Warning "$PfTxt does not exist yet. Run Create-Policies.ps1." + } + elseif (!(Test-Path($Pf86Txt))) + { + Write-Warning "$Pf86Txt does not exist yet. Run Create-Policies.ps1." + } + else + { + Get-Content $PfTxt, $Pf86Txt | foreach { + $dirsToInspect.Add($_, $UnsafeDir) + } + } +} + +if ($SearchProgramData) +{ + # Probably a mix of safe and unsafe paths + $dirsToInspect.Add($env:ProgramData, $UnknownDir) +} + +if ($SearchOneUserProfile) +{ + #Assume all unsafe paths + #TODO: Skip browser-cache temp directories + $dirsToInspect.Add($env:USERPROFILE, $UnsafeDir) +} + +if ($SearchAllUserProfiles) +{ + #Assume all unsafe paths + # No special folder or environment variable available. Get root directory from parent directory of user profile directory + $rootdir = [System.IO.Path]::GetDirectoryName($env:USERPROFILE) + #TODO: Skip browser-cache temp directories + # Skip app-compat juntions (most disallow FILE_LIST_DIRECTORY) + # Skip symlinks -- "All Users" is a symlinkd for \ProgramData but unlike most app-compat junctions it can be listed/traversed. + # This code prevents that. + Get-ChildItem -Force -Directory C:\Users | Where-Object { !$_.Attributes.HasFlag([System.IO.FileAttributes]::ReparsePoint) } | foreach { + $dirsToInspect.Add($_.FullName, $UnsafeDir) + } +} + +if ($DirsToSearch) +{ + $DirsToSearch | foreach { $dirsToInspect.Add($_, $UnknownDir) } +} + +$csv = @() + +# Output column headers +if ($DirectoryNamesOnly) +{ + $csv += + "IsSafeDir" + "`t" + + "Parent directory" +} +else +{ + $csv += + "IsSafeDir" + "`t" + + "File type" + "`t" + + "File extension" + "`t" + + "File name" + "`t" + + "File path" + "`t" + + "Parent directory" + "`t" + + "Publisher name" + "`t" + + "Product name" + "`t" + + "Hash" + "`t" + + "CreationTime" + "`t" + + "LastAccessTime" + "`t" + + "LastWriteTime" + "`t" + + "File size" +} + +function InspectFiles([string]$directory, [string]$safety, [ref] [string[]]$writableDirs) +{ + $doNoMore = $false + + Get-ChildItem -File $directory -Force -ErrorAction SilentlyContinue -PipelineVariable file | foreach { + + # Work around Get-AppLockerFileInformation bug that vomits on zero-length input files + if ($_.Length -gt 0 -and !$doNoMore) + { + $filetype = $null + if ((!($NoScripts)) -and ($file.Extension -in $scriptExtensions)) + { + $filetype = "Script" + } + elseif ((!($NoMSIs)) -and ($file.Extension -in $MsiExtensions)) + { + $filetype = "MSI" + } + elseif ((!($NoPEFiles) -and (IsExecutable($file)))) + { + $filetype = "EXE/DLL" + } + + # Output + if ($null -ne $filetype) + { + $fullname = $file.FullName + $fileext = $file.Extension + $filename = $file.Name + $parentDir = [System.IO.Path]::GetDirectoryName($fullname) + $pubName = $prodName = [String]::Empty + $alfi = Get-AppLockerFileInformation $file.FullName -ErrorAction SilentlyContinue -ErrorVariable alfiErr + # Diagnostics. Seeing sharing violations on some operations + if ($alfiErr.Count -gt 0) + { + Write-Host ($file.FullName + "`tLength = " + $file.Length.ToString()) -ForegroundColor Yellow -BackgroundColor Black + $alfiErr | foreach { Write-Host $_.Exception -ForegroundColor Red -BackgroundColor Black} + } + if ($null -ne $alfi) + { + $pub = $alfi.Publisher + if ($null -ne $pub) + { + $pubName = $pub.PublisherName + $prodName = $pub.ProductName + } + $hash = $alfi.Hash.HashDataString + } + $safetyOut = $safety + if ($safety -eq $UnknownDir) + { + #$dbgInfo = $fullname + "`t" + $parentDir + if ($parentDir -in $writableDirs.Value) + { + #$dbgInfo = $UnsafeDir + "`t" + $dbgInfo + $safetyOut = $UnsafeDir + } + else + { + #$dbgInfo = ($SafeDir + "`t" + $dbgInfo) + $safetyOut = $SafeDir + } + #$dbgInfo + } + + if ($DirectoryNamesOnly) + { + $safetyOut + "`t" + + $parentDir + + # Found one file - don't need to continue inspection of files in this directory + $doNoMore = $true + } + else + { + $safetyOut + "`t" + + $filetype + "`t" + + $fileext + "`t" + + $filename + "`t" + + $fullname + "`t" + + $parentDir + "`t" + + $pubName + "`t" + + $prodName + "`t" + + $hash + "`t" + + $file.CreationTime + "`t" + + $file.LastAccessTime + "`t" + + $file.LastWriteTime + "`t" + + $file.Length + } + } + } + } +} + +function InspectDirectories([string]$directory, [string]$safety, [ref][string[]]$writableDirs) +{ + InspectFiles $directory $safety $writableDirs + + Get-ChildItem -Directory $directory -Force -ErrorAction SilentlyContinue | foreach { + $subdir = $_ + # Decide here whether to recurse into the subdirectory: + # * Skip junctions and symlinks (typically app-compat junctions). + # * Can add criteria here to skip browser caches, etc. + if (!$subdir.Attributes.HasFlag([System.IO.FileAttributes]::ReparsePoint)) + { + Write-Verbose ("... " + $subdir.FullName) + InspectDirectories $subdir.FullName $safety $writableDirs + } + else + { + Write-Verbose ("SKIPPING " + $subdir.FullName) + } + } +} + + +# Scanning requires that AccessChk.exe be available. +# If accesschk.exe is in the rootdir, temporarily add the rootdir to the path. +# (Previous implementation invoked Get-Command to see whether accesschk.exe was in the path, and only if that failed looked for +# accesschk.exe in the rootdir. However, there was no good way to keep Get-Command from displaying a "Suggestion" message in that +# scenario.) +# Variable for restoring original Path, if necessary. +$origPath = "" +# Check for accesschk.exe in the rootdir. +if (Test-Path -Path $rootDir\AccessChk.exe) +{ + # Found it in this script's directory. Temporarily prepend it to the path. + $origPath = $env:Path + $env:Path = "$rootDir;" + $origPath +} + +# Exclude known admins from analysis +$knownAdmins = @() +$knownAdmins += & $ps1_KnownAdmins + +# Capture into hash tables, separate file name, type, and parent path +$dirsToInspect.Keys | foreach { + + $dirToInspect = $_ + $safety = $dirsToInspect[$dirToInspect] + if ($safety -eq $UnknownDir) + { + Write-Host "about to inspect $dirToInspect for writable directories..." -ForegroundColor Cyan + if ((Get-Command AccessChk.exe -ErrorAction SilentlyContinue) -eq $null) + { + $errMsg = "Scanning for writable subdirectories requires that Sysinternals AccessChk.exe be in the Path or in the same directory with this script.`n" + + "AccessChk.exe was not found.`n" + + "(See .\Support\DownloadAccesschk.ps1 for help.)`n" + + "Exiting..." + Write-Error $errMsg + return + } + $writableDirs = [ref] ( & $ps1_EnumWritableDirs -RootDirectory $dirToInspect -KnownAdmins $knownAdmins) + if ($null -eq $writableDirs) + { + $writableDirs = [ref]@() + } + } + else + { + $writableDirs = [ref]@() + } + + Write-Host "About to inspect $dirToInspect..." -ForegroundColor Cyan + $csv += InspectDirectories $dirToInspect $safety $writableDirs +} + +# Restore original Path if it was altered for AccessChk.exe +if ($origPath.Length -gt 0) +{ + $env:Path = $origPath +} + + +if ($Excel) +{ + $OutputEncodingPrevious = $OutputEncoding + $OutputEncoding = [System.Text.ASCIIEncoding]::Unicode + + $tempfile = [System.IO.Path]::GetTempFileName() + + $tabname = "Consider for potential rules" + + $csv | Out-File $tempfile -Encoding unicode + + CreateExcelFromCsvFile $tempfile $tabname # $linebreakSeq + + Remove-Item $tempfile + + $OutputEncoding = $OutputEncodingPrevious +} +else +{ + # Just output the CSV raw + $csv +} + + +<# Informational: + + Get-AppLockerFileInformation -Directory searches for these file extensions: + *.com + *.exe + *.dll + *.ocx + *.msi + *.msp + *.mst + *.bat + *.cmd + *.js + *.ps1 + *.vbs + *.appx #> \ No newline at end of file diff --git a/AaronLocker/Support/BuildRulesForFilesInWritableDirectories.ps1 b/AaronLockerScriptBased/AaronLocker/Support/BuildRulesForFilesInWritableDirectories.ps1 similarity index 97% rename from AaronLocker/Support/BuildRulesForFilesInWritableDirectories.ps1 rename to AaronLockerScriptBased/AaronLocker/Support/BuildRulesForFilesInWritableDirectories.ps1 index 3f44007..bc8385a 100644 --- a/AaronLocker/Support/BuildRulesForFilesInWritableDirectories.ps1 +++ b/AaronLockerScriptBased/AaronLocker/Support/BuildRulesForFilesInWritableDirectories.ps1 @@ -1,306 +1,306 @@ -<# -.SYNOPSIS -Builds tightly-scoped but forward-compatible AppLocker rules for files in user-writable directories. The rules are intended to be merged into a larger set using Create-Policies.ps1 in the root directory. - -.DESCRIPTION -This script takes a list of one or more file system objects (files and/or directories) and generates rules to allow execution of the corresponding files. - -Rule files generated with this script can be incorporated into comprehensive rule sets using Create-Policies.ps1 in the root directory. - -Publisher rules are generated where possible: -* Publisher rules restrict to a specific binary name, product name, and publisher, and (optionally) the identified version or above. -* Redundant rules are removed; if multiple versions of a specific file are found, the rule allows execution of the lowest-identified version or above. -Hash rules are generated when publisher rules cannot be created. -The script creates rule names and descriptions designed for readability in the Security Policy editor. The RuleNamePrefix option enables you to give each rule in the set a common prefix (e.g., "OneDrive") to make the source of the rule more apparent and so that related rules can be grouped alphabetically by name. -The rules' EnforcementMode is left NotConfigured. (Create-Policies.ps1 takes care of setting EnforcementMode in the larger set.) -(Note that the New-AppLockerPolicy's -Optimize switch "overoptimizes," allowing any file name within a given publisher and product name. Not using that.) - -File system objects can be identified on the command line with -FileSystemPaths, or listed in a file (one object per line) referenced by -FileOfFileSystemObjects. - -This script determines whether each object is a file or a directory. For directories, this script enumerates and identifies EXE, DLL, and Script files based on file extension. Subdirectories are scanned if the -RecurseDirectories switch is specified on the command line. - -The intent of this script is to create fragments of policies that can be incorporated into a "master" policy in a modular way. For example, create a file representing the rules needed to allow OneDrive to run, and separate files for LOB apps. If/when the OneDrive rules need to be updated, they can be updated in isolation and those results incorporated into a new master set. - - -.PARAMETER FileSystemPaths -An array of file paths and/or directory paths to scan. The array can be a comma-separated list of file system paths. -Either FileSystemPaths or FileOfFileSystemPaths must be specified. - -.PARAMETER FileOfFileSystemPaths -The name of a file containing a list of file paths and/or directory paths to scan; one path to a line. -Either FileSystemPaths or FileOfFileSystemPaths must be specified. - -.PARAMETER RecurseDirectories -If this switch is specified, scanning of directories includes subdirectories; otherwise, only files in the named directory are scanned. - -.PARAMETER EnforceMinimumVersion -If this switch is specified, generated publisher rules enforce minimum file version based on versions of the scanned files; otherwise rules do not enforce file versions - -.PARAMETER OutputFileName -Required: the name/path of the XML output file containing the generated rules. - -.PARAMETER RuleNamePrefix -Optional: If specified, all rule names begin with the specified RuleNamePrefix. - -.EXAMPLE -.\BuildRulesForFilesInWritableDirectories.ps1 -FileSystemPaths $env:LOCALAPPDATA\Microsoft\OneDrive -RecurseDirectories -RuleNamePrefix OneDrive -OutputFileName ..\WorkingFiles\OneDriveRules.xml - -Scans the OneDrive directory and subdirectories in the current user's profile. -All generated rule names will begin with "OneDrive". -The generated rules are written to ..\WorkingFiles\OneDriveRules.xml. - -#> - -#################################################################################################### -# Parameters -#################################################################################################### - - -# Must use FileSystemPaths or FileOfFileSystemPaths; you can use RecurseDirectories with either. -param( - # Comma-separated file paths and/or directory paths - [parameter(Mandatory=$true, ParameterSetName="OnCommandLine")] - [String[]] - $FileSystemPaths, - - # Path to a file containing a list of file paths and/or directory paths - [parameter(Mandatory=$true, ParameterSetName="SpecifiedInFile")] - [String] - $FileOfFileSystemPaths, - - # If specified, directories are recursed - [switch] - $RecurseDirectories, - - # If specified, publisher rules enforce minimum file versions; otherwise, generated publisher rules do not restrict based on file version. - [switch] - $EnforceMinimumVersion, - - # Name of output file - [parameter(Mandatory=$true)] - [String] - $OutputFileName, - - # Optional prefix incorporated into each rule name - [parameter(Mandatory=$false)] - [String] - $RuleNamePrefix -) - -#################################################################################################### -# Initialize -#################################################################################################### - -# Depends on global support functions -$thisDir = [System.IO.Path]::GetDirectoryName($MyInvocation.MyCommand.Path) -. $thisDir\SupportFunctions.ps1 - -# Build an absolute path for the output file name -if (![System.IO.Path]::IsPathRooted($OutputFileName)) -{ - $rootDir = [System.IO.Path]::GetDirectoryName($MyInvocation.MyCommand.Path) - $OutputFileName = [System.IO.Path]::Combine($rootDir, $OutputFileName) -} - -# Files/directories to scan are on the command line (FileSystemPaths) or listed in a file (FileOfFileSystemPaths). -# If the latter, populate $FileSystemPaths from that file. -# Otherwise, $FileSystemPaths is already populated. -if ($FileOfFileSystemPaths.Length -gt 0) -{ - # Test path of the file name, verify that it's a file - if ((Test-Path $FileOfFileSystemPaths) -and ((Get-Item $FileOfFileSystemPaths) -is [System.IO.FileInfo])) - { - $FileSystemPaths = Get-Content $FileOfFileSystemPaths - } - else - { - Write-Error -Category InvalidArgument -Message "`nINVALID FILE PATH: $FileOfFileSystemPaths" - return - } -} - -# If RuleNamePrefix specified, append ": " to it before incorporating into rule names -if ($RuleNamePrefix.Length -gt 0) -{ - $RuleNamePrefix += ": " -} - -# Array of AppLocker File Information objects -$arrALFI = @() -# Hash table of rules with redundant entries removed -$policies = @{} - -#################################################################################################### -# Gather file information -#################################################################################################### - -# Build the array of AppLocker File Information objects -foreach($fsp in $FileSystemPaths) -{ - # E.g., in case of blank lines in input file - $fsp = $fsp.Trim() - if ($fsp.Length -gt 0) - { - if (Test-Path $fsp) - { - # Determine whether directory or file - $fspInfo = Get-Item $fsp -Force - if ($fspInfo -is [System.IO.DirectoryInfo]) - { - # Item is a directory; inspect directory (possibly with recursion) - # Note: dependent on file extensions - # Get-AppLockerFileInformation -Directory inspects files with these extensions: - # .com, .exe, .dll, .ocx, .msi, .msp, .mst, .bat, .cmd, .js, .ps1, .vbs, .appx - # But this script drops .msi, .msp, .mst, and .appx - if ($RecurseDirectories) - { - $arrALFI += Get-AppLockerFileInformation -FileType Exe,Dll,Script -Directory $fsp -Recurse - } - else - { - $arrALFI += Get-AppLockerFileInformation -FileType Exe,Dll,Script -Directory $fsp - } - } - elseif ($fspInfo -is [System.IO.FileInfo]) - { - # Item is a file; get applocker information for the file - $arrALFI += Get-AppLockerFileInformation -Path $fsp - } - else - { - # Specified object exists and is not a file or a directory. - # Display a warning but continue. - $msg = "UNEXPECTED OBJECT TYPE FOR $fsp`n" + $fspInfo.GetType().FullName - Write-Warning -Message $msg - } - } - else - { - # Specified object does not exist. - # Display a warning but continue. - Write-Warning -Message "FILE SYSTEM OBJECT DOES NOT EXIST: $fsp" - } - } -} - -# If no valid items captured, quit now. -if ($arrALFI.Length -eq 0) -{ - Write-Warning -Message "NO FILES SCANNED." - return -} - -#################################################################################################### -# Build rules -#################################################################################################### - -# Convert the AppLockerFileInformation objects into AppLockerPolicy objects. -# Add them to collection if duplicate information not already present. -foreach($alfi in $arrALFI) -{ - # Favor publisher rule; hash rule otherwise - $pol = New-AppLockerPolicy -FileInformation $alfi -RuleType Publisher,Hash - - foreach ($ruleCollection in $pol.RuleCollections) - { - $rtype = $ruleCollection.RuleCollectionType - foreach($rule in $ruleCollection) - { - # Publisher rule - file is signed and has required PE version information - if ($rule -is [Microsoft.Security.ApplicationId.PolicyManagement.PolicyModel.FilePublisherRule]) - { - $pubInfo = $rule.PublisherConditions - # Key on file name, product name, and publisher name; don't incorporate version number into the key - $key = $pubInfo.BinaryName + "|" + $pubInfo.ProductName + "|" + $pubInfo.PublisherName - # Build new rule name and description - $rule.Description = "Product: " + $pubInfo.ProductName + "`r`n" + "Publisher: " + $pubInfo.PublisherName + "`r`n" + "Original path: " + $alfi.Path.Path - $rule.Name = $RuleNamePrefix + $pubInfo.BinaryName - $pubInfo.BinaryVersionRange.HighSection = $null - if ($EnforceMinimumVersion) - { - # Allow scanned version and above - $rule.Name += ", v" + $pubInfo.BinaryVersionRange.LowSection.ToString() + " and above" - } - else - { - $pubInfo.BinaryVersionRange.LowSection = $null - } - if (!$policies.ContainsKey($key)) - { - # Add this publisher rule to the collection - #DBG "PUBLISHER RULE (" + $rtype + "): ADDING " + $key - $policies.Add($key, $pol) - } - elseif ($EnforceMinimumVersion) - { - # File already seen; see whether the newly-scanned file has a lower file version that needs to be allowed - $rulesPrev = $policies[$key] - foreach ( $rcPrev in $rulesPrev.RuleCollections ) { foreach($rulePrev in $rcPrev) { - # Get the previously-scanned file version; compare to the new one - $verPrev = $rulePrev.PublisherConditions.BinaryVersionRange.LowSection - $verCurr = $pubInfo.BinaryVersionRange.LowSection - if ($verCurr.CompareTo($verPrev) -lt 0) - { - # The new one is a lower file version; replace the rule we had with the new one. - #DBG $pubInfo.BinaryName + " REPLACE WITH EARLIER VERSION, FROM " + $verPrev.ToString() + " TO " + $verCurr.ToString() - $policies[$key] = $pol - } - else - { - #DBG $pubInfo.BinaryName + " KEEPING VERSION " + $verCurr.ToString() + " IN FAVOR OF " + $verPrev.ToString() - } - } } - } - } - elseif ($rule -is [Microsoft.Security.ApplicationId.PolicyManagement.PolicyModel.FileHashRule]) - { - # Hash rule - file is missing signature and/or PE version information - # Record the full path into the policy - $hashInfo = $rule.HashConditions.Hashes - # Key on file name and hash - $key = $hashInfo.SourceFileName + "|" + $hashInfo.HashDataString - if (!$policies.ContainsKey($key)) - { - # Default rule name is just the file name; append "HASH RULE" - # Set the rule description to the full path. - # If the same file appears in multiple locations, one path will be picked; it doesn't matter which - $rule.Name = $RuleNamePrefix + $rule.Name + " - HASH RULE" - $rule.Description = "Identified in: " + $alfi.Path.Path - # Add this hash rule to the collection - #DBG "HASH RULE (" + $rtype + "): ADDING " + $key - $policies.Add($key, $pol) - } - else - { - # Saw an identical file already - # "HASH RULE (" + $rtype + "): ALREADY HAVE " + $key - } - } - #else - #{ - # "WHAT KIND OF RULE IS THIS?" - # $rule - #} - } - } -} - -#################################################################################################### -# Build output -#################################################################################################### - -# Combine all the rules into one policy and save it as XML. -$combinedPolicy = $null -foreach ( $policy in $policies.Values ) -{ - if ($null -eq $combinedPolicy) - { - $combinedPolicy = $policy - } - else - { - $combinedPolicy.Merge($policy) - } -} -SaveAppLockerPolicyAsUnicodeXml -ALPolicy $combinedPolicy -xmlFilename $OutputFileName - +<# +.SYNOPSIS +Builds tightly-scoped but forward-compatible AppLocker rules for files in user-writable directories. The rules are intended to be merged into a larger set using Create-Policies.ps1 in the root directory. + +.DESCRIPTION +This script takes a list of one or more file system objects (files and/or directories) and generates rules to allow execution of the corresponding files. + +Rule files generated with this script can be incorporated into comprehensive rule sets using Create-Policies.ps1 in the root directory. + +Publisher rules are generated where possible: +* Publisher rules restrict to a specific binary name, product name, and publisher, and (optionally) the identified version or above. +* Redundant rules are removed; if multiple versions of a specific file are found, the rule allows execution of the lowest-identified version or above. +Hash rules are generated when publisher rules cannot be created. +The script creates rule names and descriptions designed for readability in the Security Policy editor. The RuleNamePrefix option enables you to give each rule in the set a common prefix (e.g., "OneDrive") to make the source of the rule more apparent and so that related rules can be grouped alphabetically by name. +The rules' EnforcementMode is left NotConfigured. (Create-Policies.ps1 takes care of setting EnforcementMode in the larger set.) +(Note that the New-AppLockerPolicy's -Optimize switch "overoptimizes," allowing any file name within a given publisher and product name. Not using that.) + +File system objects can be identified on the command line with -FileSystemPaths, or listed in a file (one object per line) referenced by -FileOfFileSystemObjects. + +This script determines whether each object is a file or a directory. For directories, this script enumerates and identifies EXE, DLL, and Script files based on file extension. Subdirectories are scanned if the -RecurseDirectories switch is specified on the command line. + +The intent of this script is to create fragments of policies that can be incorporated into a "master" policy in a modular way. For example, create a file representing the rules needed to allow OneDrive to run, and separate files for LOB apps. If/when the OneDrive rules need to be updated, they can be updated in isolation and those results incorporated into a new master set. + + +.PARAMETER FileSystemPaths +An array of file paths and/or directory paths to scan. The array can be a comma-separated list of file system paths. +Either FileSystemPaths or FileOfFileSystemPaths must be specified. + +.PARAMETER FileOfFileSystemPaths +The name of a file containing a list of file paths and/or directory paths to scan; one path to a line. +Either FileSystemPaths or FileOfFileSystemPaths must be specified. + +.PARAMETER RecurseDirectories +If this switch is specified, scanning of directories includes subdirectories; otherwise, only files in the named directory are scanned. + +.PARAMETER EnforceMinimumVersion +If this switch is specified, generated publisher rules enforce minimum file version based on versions of the scanned files; otherwise rules do not enforce file versions + +.PARAMETER OutputFileName +Required: the name/path of the XML output file containing the generated rules. + +.PARAMETER RuleNamePrefix +Optional: If specified, all rule names begin with the specified RuleNamePrefix. + +.EXAMPLE +.\BuildRulesForFilesInWritableDirectories.ps1 -FileSystemPaths $env:LOCALAPPDATA\Microsoft\OneDrive -RecurseDirectories -RuleNamePrefix OneDrive -OutputFileName ..\WorkingFiles\OneDriveRules.xml + +Scans the OneDrive directory and subdirectories in the current user's profile. +All generated rule names will begin with "OneDrive". +The generated rules are written to ..\WorkingFiles\OneDriveRules.xml. + +#> + +#################################################################################################### +# Parameters +#################################################################################################### + + +# Must use FileSystemPaths or FileOfFileSystemPaths; you can use RecurseDirectories with either. +param( + # Comma-separated file paths and/or directory paths + [parameter(Mandatory=$true, ParameterSetName="OnCommandLine")] + [String[]] + $FileSystemPaths, + + # Path to a file containing a list of file paths and/or directory paths + [parameter(Mandatory=$true, ParameterSetName="SpecifiedInFile")] + [String] + $FileOfFileSystemPaths, + + # If specified, directories are recursed + [switch] + $RecurseDirectories, + + # If specified, publisher rules enforce minimum file versions; otherwise, generated publisher rules do not restrict based on file version. + [switch] + $EnforceMinimumVersion, + + # Name of output file + [parameter(Mandatory=$true)] + [String] + $OutputFileName, + + # Optional prefix incorporated into each rule name + [parameter(Mandatory=$false)] + [String] + $RuleNamePrefix +) + +#################################################################################################### +# Initialize +#################################################################################################### + +# Depends on global support functions +$thisDir = [System.IO.Path]::GetDirectoryName($MyInvocation.MyCommand.Path) +. $thisDir\SupportFunctions.ps1 + +# Build an absolute path for the output file name +if (![System.IO.Path]::IsPathRooted($OutputFileName)) +{ + $rootDir = [System.IO.Path]::GetDirectoryName($MyInvocation.MyCommand.Path) + $OutputFileName = [System.IO.Path]::Combine($rootDir, $OutputFileName) +} + +# Files/directories to scan are on the command line (FileSystemPaths) or listed in a file (FileOfFileSystemPaths). +# If the latter, populate $FileSystemPaths from that file. +# Otherwise, $FileSystemPaths is already populated. +if ($FileOfFileSystemPaths.Length -gt 0) +{ + # Test path of the file name, verify that it's a file + if ((Test-Path $FileOfFileSystemPaths) -and ((Get-Item $FileOfFileSystemPaths) -is [System.IO.FileInfo])) + { + $FileSystemPaths = Get-Content $FileOfFileSystemPaths + } + else + { + Write-Error -Category InvalidArgument -Message "`nINVALID FILE PATH: $FileOfFileSystemPaths" + return + } +} + +# If RuleNamePrefix specified, append ": " to it before incorporating into rule names +if ($RuleNamePrefix.Length -gt 0) +{ + $RuleNamePrefix += ": " +} + +# Array of AppLocker File Information objects +$arrALFI = @() +# Hash table of rules with redundant entries removed +$policies = @{} + +#################################################################################################### +# Gather file information +#################################################################################################### + +# Build the array of AppLocker File Information objects +foreach($fsp in $FileSystemPaths) +{ + # E.g., in case of blank lines in input file + $fsp = $fsp.Trim() + if ($fsp.Length -gt 0) + { + if (Test-Path $fsp) + { + # Determine whether directory or file + $fspInfo = Get-Item $fsp -Force + if ($fspInfo -is [System.IO.DirectoryInfo]) + { + # Item is a directory; inspect directory (possibly with recursion) + # Note: dependent on file extensions + # Get-AppLockerFileInformation -Directory inspects files with these extensions: + # .com, .exe, .dll, .ocx, .msi, .msp, .mst, .bat, .cmd, .js, .ps1, .vbs, .appx + # But this script drops .msi, .msp, .mst, and .appx + if ($RecurseDirectories) + { + $arrALFI += Get-AppLockerFileInformation -FileType Exe,Dll,Script -Directory $fsp -Recurse + } + else + { + $arrALFI += Get-AppLockerFileInformation -FileType Exe,Dll,Script -Directory $fsp + } + } + elseif ($fspInfo -is [System.IO.FileInfo]) + { + # Item is a file; get applocker information for the file + $arrALFI += Get-AppLockerFileInformation -Path $fsp + } + else + { + # Specified object exists and is not a file or a directory. + # Display a warning but continue. + $msg = "UNEXPECTED OBJECT TYPE FOR $fsp`n" + $fspInfo.GetType().FullName + Write-Warning -Message $msg + } + } + else + { + # Specified object does not exist. + # Display a warning but continue. + Write-Warning -Message "FILE SYSTEM OBJECT DOES NOT EXIST: $fsp" + } + } +} + +# If no valid items captured, quit now. +if ($arrALFI.Length -eq 0) +{ + Write-Warning -Message "NO FILES SCANNED." + return +} + +#################################################################################################### +# Build rules +#################################################################################################### + +# Convert the AppLockerFileInformation objects into AppLockerPolicy objects. +# Add them to collection if duplicate information not already present. +foreach($alfi in $arrALFI) +{ + # Favor publisher rule; hash rule otherwise + $pol = New-AppLockerPolicy -FileInformation $alfi -RuleType Publisher,Hash + + foreach ($ruleCollection in $pol.RuleCollections) + { + $rtype = $ruleCollection.RuleCollectionType + foreach($rule in $ruleCollection) + { + # Publisher rule - file is signed and has required PE version information + if ($rule -is [Microsoft.Security.ApplicationId.PolicyManagement.PolicyModel.FilePublisherRule]) + { + $pubInfo = $rule.PublisherConditions + # Key on file name, product name, and publisher name; don't incorporate version number into the key + $key = $pubInfo.BinaryName + "|" + $pubInfo.ProductName + "|" + $pubInfo.PublisherName + # Build new rule name and description + $rule.Description = "Product: " + $pubInfo.ProductName + "`r`n" + "Publisher: " + $pubInfo.PublisherName + "`r`n" + "Original path: " + $alfi.Path.Path + $rule.Name = $RuleNamePrefix + $pubInfo.BinaryName + $pubInfo.BinaryVersionRange.HighSection = $null + if ($EnforceMinimumVersion) + { + # Allow scanned version and above + $rule.Name += ", v" + $pubInfo.BinaryVersionRange.LowSection.ToString() + " and above" + } + else + { + $pubInfo.BinaryVersionRange.LowSection = $null + } + if (!$policies.ContainsKey($key)) + { + # Add this publisher rule to the collection + #DBG "PUBLISHER RULE (" + $rtype + "): ADDING " + $key + $policies.Add($key, $pol) + } + elseif ($EnforceMinimumVersion) + { + # File already seen; see whether the newly-scanned file has a lower file version that needs to be allowed + $rulesPrev = $policies[$key] + foreach ( $rcPrev in $rulesPrev.RuleCollections ) { foreach($rulePrev in $rcPrev) { + # Get the previously-scanned file version; compare to the new one + $verPrev = $rulePrev.PublisherConditions.BinaryVersionRange.LowSection + $verCurr = $pubInfo.BinaryVersionRange.LowSection + if ($verCurr.CompareTo($verPrev) -lt 0) + { + # The new one is a lower file version; replace the rule we had with the new one. + #DBG $pubInfo.BinaryName + " REPLACE WITH EARLIER VERSION, FROM " + $verPrev.ToString() + " TO " + $verCurr.ToString() + $policies[$key] = $pol + } + else + { + #DBG $pubInfo.BinaryName + " KEEPING VERSION " + $verCurr.ToString() + " IN FAVOR OF " + $verPrev.ToString() + } + } } + } + } + elseif ($rule -is [Microsoft.Security.ApplicationId.PolicyManagement.PolicyModel.FileHashRule]) + { + # Hash rule - file is missing signature and/or PE version information + # Record the full path into the policy + $hashInfo = $rule.HashConditions.Hashes + # Key on file name and hash + $key = $hashInfo.SourceFileName + "|" + $hashInfo.HashDataString + if (!$policies.ContainsKey($key)) + { + # Default rule name is just the file name; append "HASH RULE" + # Set the rule description to the full path. + # If the same file appears in multiple locations, one path will be picked; it doesn't matter which + $rule.Name = $RuleNamePrefix + $rule.Name + " - HASH RULE" + $rule.Description = "Identified in: " + $alfi.Path.Path + # Add this hash rule to the collection + #DBG "HASH RULE (" + $rtype + "): ADDING " + $key + $policies.Add($key, $pol) + } + else + { + # Saw an identical file already + # "HASH RULE (" + $rtype + "): ALREADY HAVE " + $key + } + } + #else + #{ + # "WHAT KIND OF RULE IS THIS?" + # $rule + #} + } + } +} + +#################################################################################################### +# Build output +#################################################################################################### + +# Combine all the rules into one policy and save it as XML. +$combinedPolicy = $null +foreach ( $policy in $policies.Values ) +{ + if ($null -eq $combinedPolicy) + { + $combinedPolicy = $policy + } + else + { + $combinedPolicy.Merge($policy) + } +} +SaveAppLockerPolicyAsUnicodeXml -ALPolicy $combinedPolicy -xmlFilename $OutputFileName + diff --git a/AaronLocker/Support/Config.ps1 b/AaronLockerScriptBased/AaronLocker/Support/Config.ps1 similarity index 98% rename from AaronLocker/Support/Config.ps1 rename to AaronLockerScriptBased/AaronLocker/Support/Config.ps1 index 7c8476b..e4055d5 100644 --- a/AaronLocker/Support/Config.ps1 +++ b/AaronLockerScriptBased/AaronLocker/Support/Config.ps1 @@ -1,86 +1,86 @@ -<# -.SYNOPSIS -Defines variables for path names and other configuration settings. Intended to be dot-sourced into other scripts, and not run directly. -Also loads global support functions. - -.DESCRIPTION -Defines variables for path names and other configuration settings. Intended to be dot-sourced into other scripts, and not run directly. -Also loads global support functions. - -Variable $rootDir must already have been set prior to calling this script. -#> - -# Verify that $rootDir has been defined and is an existing directory. -if ($null -eq $rootDir -or !(Test-Path($rootDir))) -{ - Write-Error ('Script error: variable $rootDir is not defined prior to invoking ' + $MyInvocation.MyCommand.Path) - return -} - -####### Establish directory paths -$customizationInputsDir = [System.IO.Path]::Combine($rootDir, "CustomizationInputs") -$mergeRulesDynamicDir = [System.IO.Path]::Combine($rootDir, "MergeRules-Dynamic") -$mergeRulesStaticDir = [System.IO.Path]::Combine($rootDir, "MergeRules-Static") -$outputsDir = [System.IO.Path]::Combine($rootDir, "Outputs") -$supportDir = [System.IO.Path]::Combine($rootDir, "Support") -$scanResultsDir = [System.IO.Path]::Combine($rootDir, "ScanResults") - -####### INPUTS - -# Script inputs -$ps1_GetExeFilesToBlacklist = [System.IO.Path]::Combine($customizationInputsDir, "GetExeFilesToBlacklist.ps1") -$ps1_GetSafePathsToAllow = [System.IO.Path]::Combine($customizationInputsDir, "GetSafePathsToAllow.ps1") -$ps1_UnsafePathsToBuildRulesFor = [System.IO.Path]::Combine($customizationInputsDir, "UnsafePathsToBuildRulesFor.ps1") -$fname_TrustedSigners = "TrustedSigners.ps1" -$ps1_TrustedSigners = [System.IO.Path]::Combine($customizationInputsDir, $fname_TrustedSigners) -$ps1_HashRuleData = [System.IO.Path]::Combine($customizationInputsDir, "HashRuleData.ps1") -$ps1_KnownAdmins = [System.IO.Path]::Combine($customizationInputsDir, "KnownAdmins.ps1") - -# Path to results from scanning files listed in GetExeFilesToBlacklist -$ExeBlacklistData = [System.IO.Path]::Combine($scanResultsDir, "ExeBlacklistData.txt") -# Paths to "full" results of all user-writable directories under Windir and the ProgramFiles directories. -# Written to when Rescan enabled; used to create the next set of files -$windirFullXml = [System.IO.Path]::Combine($scanResultsDir, "Writable_Full_windir.xml") -$PfFullXml = [System.IO.Path]::Combine($scanResultsDir, "Writable_Full_PF.xml") -$Pf86FullXml = [System.IO.Path]::Combine($scanResultsDir, "Writable_Full_PF86.xml") -# Paths to filtered results with redundancies removed. -# Written to when Rescan enabled; read from when building rule set. -$windirTxt = [System.IO.Path]::Combine($scanResultsDir, "Writable_windir.txt") -$PfTxt = [System.IO.Path]::Combine($scanResultsDir, "Writable_PF.txt") -$Pf86Txt = [System.IO.Path]::Combine($scanResultsDir, "Writable_PF86.txt") - - -####### SUPPORT -$defRulesXml = [System.IO.Path]::Combine($supportDir, "DefaultRulesWithPlaceholders.xml") -$ps1_EnumWritableDirs = [System.IO.Path]::Combine($supportDir, "Enum-WritableDirs.ps1") -$ps1_BuildRulesForFilesInWritableDirectories = [System.IO.Path]::Combine($supportDir, "BuildRulesForFilesInWritableDirectories.ps1") -$ps1_ExportPolicyToCSV = [System.IO.Path]::Combine($supportDir, "ExportPolicy-ToCsv.ps1") -$ps1_ExportPolicyToExcel = [System.IO.Path]::Combine($rootDir, "ExportPolicy-ToExcel.ps1") - - -####### OUTPUTS -# Paths to result files containing AppLocker policy rules. -# Policy rules file have timestamp embedded into file name so previous ones don't get overwritten and so that alphabetic sort shows which is newest. -# Example filenames: -# AppLockerRules-20180518-1151-Audit.xml -# AppLockerRules-20180518-1151-Enforce.xml -$strTimestamp = [datetime]::Now.ToString("yyyyMMdd-HHmm") -$rulesFileBase = "AppLockerRules-" -$rulesFileAuditSuffix = "-Audit.xml" -$rulesFileEnforceSuffix = "-Enforce.xml" -$rulesFileAuditNew = [System.IO.Path]::Combine($outputsDir, $rulesFileBase + $strTimestamp + $rulesFileAuditSuffix) -$rulesFileEnforceNew = [System.IO.Path]::Combine($outputsDir, $rulesFileBase + $strTimestamp + $rulesFileEnforceSuffix) -# Get latest audit and enforce policy files, or $null if none found. -function RulesFileAuditLatest() -{ - Get-ChildItem $([System.IO.Path]::Combine($outputsDir, $rulesFileBase + "*" + $rulesFileAuditSuffix)) | foreach { $_.FullName } | Sort-Object | Select-Object -Last 1 -} -function RulesFileEnforceLatest() -{ - Get-ChildItem $([System.IO.Path]::Combine($outputsDir, $rulesFileBase + "*" + $rulesFileEnforceSuffix)) | foreach { $_.FullName } | Sort-Object | Select-Object -Last 1 -} - - -####### GLOBAL FUNCTIONS -# Incorporate global support functions -. $rootDir\Support\SupportFunctions.ps1 +<# +.SYNOPSIS +Defines variables for path names and other configuration settings. Intended to be dot-sourced into other scripts, and not run directly. +Also loads global support functions. + +.DESCRIPTION +Defines variables for path names and other configuration settings. Intended to be dot-sourced into other scripts, and not run directly. +Also loads global support functions. + +Variable $rootDir must already have been set prior to calling this script. +#> + +# Verify that $rootDir has been defined and is an existing directory. +if ($null -eq $rootDir -or !(Test-Path($rootDir))) +{ + Write-Error ('Script error: variable $rootDir is not defined prior to invoking ' + $MyInvocation.MyCommand.Path) + return +} + +####### Establish directory paths +$customizationInputsDir = [System.IO.Path]::Combine($rootDir, "CustomizationInputs") +$mergeRulesDynamicDir = [System.IO.Path]::Combine($rootDir, "MergeRules-Dynamic") +$mergeRulesStaticDir = [System.IO.Path]::Combine($rootDir, "MergeRules-Static") +$outputsDir = [System.IO.Path]::Combine($rootDir, "Outputs") +$supportDir = [System.IO.Path]::Combine($rootDir, "Support") +$scanResultsDir = [System.IO.Path]::Combine($rootDir, "ScanResults") + +####### INPUTS + +# Script inputs +$ps1_GetExeFilesToBlacklist = [System.IO.Path]::Combine($customizationInputsDir, "GetExeFilesToBlacklist.ps1") +$ps1_GetSafePathsToAllow = [System.IO.Path]::Combine($customizationInputsDir, "GetSafePathsToAllow.ps1") +$ps1_UnsafePathsToBuildRulesFor = [System.IO.Path]::Combine($customizationInputsDir, "UnsafePathsToBuildRulesFor.ps1") +$fname_TrustedSigners = "TrustedSigners.ps1" +$ps1_TrustedSigners = [System.IO.Path]::Combine($customizationInputsDir, $fname_TrustedSigners) +$ps1_HashRuleData = [System.IO.Path]::Combine($customizationInputsDir, "HashRuleData.ps1") +$ps1_KnownAdmins = [System.IO.Path]::Combine($customizationInputsDir, "KnownAdmins.ps1") + +# Path to results from scanning files listed in GetExeFilesToBlacklist +$ExeBlacklistData = [System.IO.Path]::Combine($scanResultsDir, "ExeBlacklistData.txt") +# Paths to "full" results of all user-writable directories under Windir and the ProgramFiles directories. +# Written to when Rescan enabled; used to create the next set of files +$windirFullXml = [System.IO.Path]::Combine($scanResultsDir, "Writable_Full_windir.xml") +$PfFullXml = [System.IO.Path]::Combine($scanResultsDir, "Writable_Full_PF.xml") +$Pf86FullXml = [System.IO.Path]::Combine($scanResultsDir, "Writable_Full_PF86.xml") +# Paths to filtered results with redundancies removed. +# Written to when Rescan enabled; read from when building rule set. +$windirTxt = [System.IO.Path]::Combine($scanResultsDir, "Writable_windir.txt") +$PfTxt = [System.IO.Path]::Combine($scanResultsDir, "Writable_PF.txt") +$Pf86Txt = [System.IO.Path]::Combine($scanResultsDir, "Writable_PF86.txt") + + +####### SUPPORT +$defRulesXml = [System.IO.Path]::Combine($supportDir, "DefaultRulesWithPlaceholders.xml") +$ps1_EnumWritableDirs = [System.IO.Path]::Combine($supportDir, "Enum-WritableDirs.ps1") +$ps1_BuildRulesForFilesInWritableDirectories = [System.IO.Path]::Combine($supportDir, "BuildRulesForFilesInWritableDirectories.ps1") +$ps1_ExportPolicyToCSV = [System.IO.Path]::Combine($supportDir, "ExportPolicy-ToCsv.ps1") +$ps1_ExportPolicyToExcel = [System.IO.Path]::Combine($rootDir, "ExportPolicy-ToExcel.ps1") + + +####### OUTPUTS +# Paths to result files containing AppLocker policy rules. +# Policy rules file have timestamp embedded into file name so previous ones don't get overwritten and so that alphabetic sort shows which is newest. +# Example filenames: +# AppLockerRules-20180518-1151-Audit.xml +# AppLockerRules-20180518-1151-Enforce.xml +$strTimestamp = [datetime]::Now.ToString("yyyyMMdd-HHmm") +$rulesFileBase = "AppLockerRules-" +$rulesFileAuditSuffix = "-Audit.xml" +$rulesFileEnforceSuffix = "-Enforce.xml" +$rulesFileAuditNew = [System.IO.Path]::Combine($outputsDir, $rulesFileBase + $strTimestamp + $rulesFileAuditSuffix) +$rulesFileEnforceNew = [System.IO.Path]::Combine($outputsDir, $rulesFileBase + $strTimestamp + $rulesFileEnforceSuffix) +# Get latest audit and enforce policy files, or $null if none found. +function RulesFileAuditLatest() +{ + Get-ChildItem $([System.IO.Path]::Combine($outputsDir, $rulesFileBase + "*" + $rulesFileAuditSuffix)) | foreach { $_.FullName } | Sort-Object | Select-Object -Last 1 +} +function RulesFileEnforceLatest() +{ + Get-ChildItem $([System.IO.Path]::Combine($outputsDir, $rulesFileBase + "*" + $rulesFileEnforceSuffix)) | foreach { $_.FullName } | Sort-Object | Select-Object -Last 1 +} + + +####### GLOBAL FUNCTIONS +# Incorporate global support functions +. $rootDir\Support\SupportFunctions.ps1 diff --git a/AaronLocker/Support/DefaultRulesWithPlaceholders.xml b/AaronLockerScriptBased/AaronLocker/Support/DefaultRulesWithPlaceholders.xml similarity index 100% rename from AaronLocker/Support/DefaultRulesWithPlaceholders.xml rename to AaronLockerScriptBased/AaronLocker/Support/DefaultRulesWithPlaceholders.xml diff --git a/AaronLocker/Support/DownloadAccesschk.ps1 b/AaronLockerScriptBased/AaronLocker/Support/DownloadAccesschk.ps1 similarity index 97% rename from AaronLocker/Support/DownloadAccesschk.ps1 rename to AaronLockerScriptBased/AaronLocker/Support/DownloadAccesschk.ps1 index e7891e2..5c67927 100644 --- a/AaronLocker/Support/DownloadAccesschk.ps1 +++ b/AaronLockerScriptBased/AaronLocker/Support/DownloadAccesschk.ps1 @@ -1,15 +1,15 @@ -<# -.SYNOPSIS -Download Sysinternals accesschk.exe into the parent directory above this script's directory. - -TODO: Maybe add a required -AcceptEula switch -#> - -# Identify the directory above this script's directory (presumably the main "AaronLocker" directory). -$thisDir = [System.IO.Path]::GetDirectoryName($MyInvocation.MyCommand.Path) -$targetDir = [System.IO.Directory]::GetParent($thisDir).FullName - -Invoke-WebRequest -Uri https://live.sysinternals.com/accesschk.exe -OutFile (Join-Path $targetDir "accesschk.exe") -#TODO: Verify that Invoke-Request succeeded. - -#TODO: Set the LastWriteTime to match +<# +.SYNOPSIS +Download Sysinternals accesschk.exe into the parent directory above this script's directory. + +TODO: Maybe add a required -AcceptEula switch +#> + +# Identify the directory above this script's directory (presumably the main "AaronLocker" directory). +$thisDir = [System.IO.Path]::GetDirectoryName($MyInvocation.MyCommand.Path) +$targetDir = [System.IO.Directory]::GetParent($thisDir).FullName + +Invoke-WebRequest -Uri https://live.sysinternals.com/accesschk.exe -OutFile (Join-Path $targetDir "accesschk.exe") +#TODO: Verify that Invoke-Request succeeded. + +#TODO: Set the LastWriteTime to match diff --git a/AaronLocker/Support/Enum-WritableDirs.ps1 b/AaronLockerScriptBased/AaronLocker/Support/Enum-WritableDirs.ps1 similarity index 97% rename from AaronLocker/Support/Enum-WritableDirs.ps1 rename to AaronLockerScriptBased/AaronLocker/Support/Enum-WritableDirs.ps1 index 5731a56..0d2b7cb 100644 --- a/AaronLocker/Support/Enum-WritableDirs.ps1 +++ b/AaronLockerScriptBased/AaronLocker/Support/Enum-WritableDirs.ps1 @@ -1,241 +1,241 @@ -<# -.SYNOPSIS -Enumerates "user-writable" subdirectories. - -.DESCRIPTION -Enumerates subdirectories that are writable by accounts other than a set of -known admin or admin-equivalent entities (including members of the local -Administrators group). The goal is to list user-writable directories in -which end user program execution should be disallowed via AppLocker. -You should run this script with administrative rights to avoid access- -denied errors. - -NOTE: Requires Sysinternals AccessChk.exe: - https://technet.microsoft.com/sysinternals/accesschk - https://download.sysinternals.com/files/AccessChk.zip -NOTE: Requires Windows PowerShell 5.1 or newer (relies on Get-LocalGroup and -Get-LocalGroupMember cmdlets). - -Note: this script does not discover user-writable files. A user-writable -file in a non-writable directory presents a similar risk, as a non-admin -can overwrite it with arbitrary content and execute it. - -.LINK -Sysinternals AccessChk available here: - https://technet.microsoft.com/sysinternals/accesschk - https://download.sysinternals.com/files/AccessChk.zip - https://live.sysinternals.com/accesschk.exe -or run .\Support\DownloadAccesschk.ps1, which downloads AccessChk.exe to the main AaronLocker directory. - -.PARAMETER RootDirectory -The starting directory for the permission enumeration. - -.PARAMETER ShowGrantees -If set, output includes the names of the non-admin entities that have write -permissions - -.PARAMETER DontFilterNTService -By default, this script ignores access granted to NT SERVICE\ accounts (SID -beginning with S-1-5-80-). If this switch is set, this script does not -ignore that access, except for access granted to NT SERVICE\TrustedInstaller. - -.PARAMETER OutputXML -If set, output is formatted as XML. - -.PARAMETER KnownAdmins -Optional: additional list of known administrative users and groups. - -.EXAMPLE - -.\Enum-WritableDirs.ps1 C:\Windows\System32 - -Output: -C:\Windows\System32\FxsTmp -C:\Windows\System32\Tasks -C:\Windows\System32\Com\dmp -C:\Windows\System32\Microsoft\Crypto\RSA\MachineKeys -C:\Windows\System32\spool\PRINTERS -C:\Windows\System32\spool\SERVERS -C:\Windows\System32\spool\drivers\color -C:\Windows\System32\Tasks\Microsoft IT Diagnostics Utility -C:\Windows\System32\Tasks\Microsoft IT VPN -C:\Windows\System32\Tasks\WPD -C:\Windows\System32\Tasks\Microsoft\Windows\RemoteApp and Desktop Connections Update -C:\Windows\System32\Tasks\Microsoft\Windows\SyncCenter -C:\Windows\System32\Tasks\Microsoft\Windows\WCM -C:\Windows\System32\Tasks\Microsoft\Windows\PLA\System - -.EXAMPLE -.\Enum-WritableDirs.ps1 C:\Windows\System32 -ShowGrantees - -Output: -C:\Windows\system32\FxsTmp - BUILTIN\Users -C:\Windows\system32\Tasks - NT AUTHORITY\Authenticated Users -C:\Windows\system32\Com\dmp - BUILTIN\Users -C:\Windows\system32\Microsoft\Crypto\RSA\MachineKeys - Everyone -C:\Windows\system32\spool\PRINTERS - BUILTIN\Users -C:\Windows\system32\spool\SERVERS - BUILTIN\Users -C:\Windows\system32\spool\drivers\color - BUILTIN\Users -C:\Windows\system32\Tasks\Microsoft IT Diagnostics Utility - NT AUTHORITY\Authenticated Users -C:\Windows\system32\Tasks\Microsoft IT VPN - NT AUTHORITY\Authenticated Users -C:\Windows\system32\Tasks\WPD - NT AUTHORITY\Authenticated Users - aaronmar5\aaronmaradmin -C:\Windows\system32\Tasks\Microsoft\Windows\RemoteApp and Desktop Connections Update - NT AUTHORITY\Authenticated Users -C:\Windows\system32\Tasks\Microsoft\Windows\SyncCenter - BUILTIN\Users -C:\Windows\system32\Tasks\Microsoft\Windows\WCM - BUILTIN\Users -C:\Windows\system32\Tasks\Microsoft\Windows\PLA\System - Everyone - -.EXAMPLE -$x = [xml](.\Enum-WritableDirs.ps1 C:\Windows\System32 -ShowGrantees -OutputXML) -$x.root.dir | Sort-Object name - -Output: -name Grantee ----- ------- -C:\Windows\System32\Com\dmp BUILTIN\Users -C:\Windows\System32\FxsTmp BUILTIN\Users -C:\Windows\System32\Microsoft\Crypto\RSA\MachineKeys Everyone -C:\Windows\System32\spool\drivers\color BUILTIN\Users -C:\Windows\System32\spool\PRINTERS BUILTIN\Users -C:\Windows\System32\spool\SERVERS BUILTIN\Users -C:\Windows\System32\Tasks NT AUTHORITY\Authenticated Users -C:\Windows\System32\Tasks\Microsoft IT Diagnostics Utility NT AUTHORITY\Authenticated Users -C:\Windows\System32\Tasks\Microsoft IT VPN NT AUTHORITY\Authenticated Users -C:\Windows\System32\Tasks\Microsoft\Windows\PLA\System Everyone -C:\Windows\System32\Tasks\Microsoft\Windows\RemoteApp and Desktop Connection... NT AUTHORITY\Authenticated Users -C:\Windows\System32\Tasks\Microsoft\Windows\SyncCenter BUILTIN\Users -C:\Windows\System32\Tasks\Microsoft\Windows\WCM BUILTIN\Users -C:\Windows\System32\Tasks\WPD {NT AUTHORITY\Authenticated Users, vm-t2408\admin} - -#> - -param( - # Name of root directory for permission search - [parameter(Mandatory=$true)] - [String] - $RootDirectory, - - # If -ShowGrantees on the command line, shows which non-admin accounts - # are granted access - [switch] - $ShowGrantees = $false, - - # If -DontFilterNTService set, does not filter out access granted to - # NT SERVICE\ accounts (other than TrustedInstaller) - [switch] - $DontFilterNTService = $false, - - # If -OutputXML, output is XML - [switch] - $OutputXML = $false, - - # If provided, adds list of known administrative users/groups to ignore write permissions granted to - [parameter(Mandatory=$false)] - [String[]] - $KnownAdmins -) - -# If RootDirectory has a trailing backslash, remove it (AccessChk doesn't handle it correctly). -if ($RootDirectory.EndsWith("\")) { $RootDirectory = $RootDirectory.Substring(0, $RootDirectory.Length - 1) } - -# Entities for which to ignore write permissions. -# TrustedInstaller is always ignored; other NT SERVICE\ accounts are filtered -# out later (too many to list and too many unknown). -# The Package SIDs below (S-1-15-2-*) are associated with microsoft.windows.fontdrvhost and -# are not a problem. AppContainers never grant additional access; they only reduce access. -$FilterOut0 = @" -S-1-3-0 -S-1-5-18 -S-1-5-19 -S-1-5-20 -S-1-5-32-544 -S-1-5-32-549 -S-1-5-32-550 -S-1-5-32-551 -S-1-5-32-577 -S-1-5-32-559 -S-1-5-32-568 -NT SERVICE\TrustedInstaller -S-1-15-2-1430448594-2639229838-973813799-439329657-1197984847-4069167804-1277922394 -S-1-15-2-95739096-486727260-2033287795-3853587803-1685597119-444378811-2746676523 -"@ -# Filter all the above plus caller-supplied "known admins" -$FilterOut = ($FilterOut0.Split("`n`r") + $KnownAdmins | Where-Object { $_.Length -gt 0 }) -join "," -# Add all members of the local Administrators group, as the Effective Permissions -# APIs consider them to be administrators also. -# For some reason, Get-LocalGroup/Get-LocalGroupMember aren't available on WMFv5.0 on Win7; -# Verify whether command exists before using it. The commands are available on Win7 in v5.1. -if ($null -ne (Get-Command Get-LocalGroupMember -ErrorAction SilentlyContinue)) -{ - #TODO: Detect and handle case where this cmdlet fails - disconnected and the admins group contains domain SIDs that can't be resolved. - #FWIW, NET LOCALGROUP Administrators doesn't report these entries either. - #Also fails on AAD-joined, with unresolved SIDs beginning with S-1-12-1-... - Get-LocalGroupMember -SID S-1-5-32-544 -ErrorAction SilentlyContinue | ForEach-Object { $FilterOut += "," + $_.SID.Value } -} - -$currfile = "" - -if ($OutputXML) { "" } - -$bInElem = $false - -AccessChk.exe /accepteula -nobanner -w -d -s -f $FilterOut $RootDirectory | ForEach-Object { - if ($_.StartsWith(" ") -or $_.Length -eq 0) - { - if ($_.StartsWith(" RW ") -or $_.StartsWith(" W ")) - { - $grantee = $_.Substring(5).Trim() - if ($DontFilterNTService -or (!$grantee.StartsWith("NT SERVICE\") -and !$grantee.StartsWith("S-1-5-80-"))) - { - if ($currfile.Length -gt 0) - { - if ($OutputXML) - { - # Path name has to be escaped for XML - "" - } - else - { - $currfile - } - $currfile = "" - $bInElem = $true - } - if ($ShowGrantees) - { - if ($OutputXML) - { - "" + $grantee + "" - } - else - { - " " + $grantee - } - } - } - } - } - else - { - if ($bInElem -and $OutputXML) { "" } - $currfile = $_ - $bInElem = $false - } -} - -if ($bInElem -and $OutputXML) { "" } -if ($OutputXML) { "" } +<# +.SYNOPSIS +Enumerates "user-writable" subdirectories. + +.DESCRIPTION +Enumerates subdirectories that are writable by accounts other than a set of +known admin or admin-equivalent entities (including members of the local +Administrators group). The goal is to list user-writable directories in +which end user program execution should be disallowed via AppLocker. +You should run this script with administrative rights to avoid access- +denied errors. + +NOTE: Requires Sysinternals AccessChk.exe: + https://technet.microsoft.com/sysinternals/accesschk + https://download.sysinternals.com/files/AccessChk.zip +NOTE: Requires Windows PowerShell 5.1 or newer (relies on Get-LocalGroup and +Get-LocalGroupMember cmdlets). + +Note: this script does not discover user-writable files. A user-writable +file in a non-writable directory presents a similar risk, as a non-admin +can overwrite it with arbitrary content and execute it. + +.LINK +Sysinternals AccessChk available here: + https://technet.microsoft.com/sysinternals/accesschk + https://download.sysinternals.com/files/AccessChk.zip + https://live.sysinternals.com/accesschk.exe +or run .\Support\DownloadAccesschk.ps1, which downloads AccessChk.exe to the main AaronLocker directory. + +.PARAMETER RootDirectory +The starting directory for the permission enumeration. + +.PARAMETER ShowGrantees +If set, output includes the names of the non-admin entities that have write +permissions + +.PARAMETER DontFilterNTService +By default, this script ignores access granted to NT SERVICE\ accounts (SID +beginning with S-1-5-80-). If this switch is set, this script does not +ignore that access, except for access granted to NT SERVICE\TrustedInstaller. + +.PARAMETER OutputXML +If set, output is formatted as XML. + +.PARAMETER KnownAdmins +Optional: additional list of known administrative users and groups. + +.EXAMPLE + +.\Enum-WritableDirs.ps1 C:\Windows\System32 + +Output: +C:\Windows\System32\FxsTmp +C:\Windows\System32\Tasks +C:\Windows\System32\Com\dmp +C:\Windows\System32\Microsoft\Crypto\RSA\MachineKeys +C:\Windows\System32\spool\PRINTERS +C:\Windows\System32\spool\SERVERS +C:\Windows\System32\spool\drivers\color +C:\Windows\System32\Tasks\Microsoft IT Diagnostics Utility +C:\Windows\System32\Tasks\Microsoft IT VPN +C:\Windows\System32\Tasks\WPD +C:\Windows\System32\Tasks\Microsoft\Windows\RemoteApp and Desktop Connections Update +C:\Windows\System32\Tasks\Microsoft\Windows\SyncCenter +C:\Windows\System32\Tasks\Microsoft\Windows\WCM +C:\Windows\System32\Tasks\Microsoft\Windows\PLA\System + +.EXAMPLE +.\Enum-WritableDirs.ps1 C:\Windows\System32 -ShowGrantees + +Output: +C:\Windows\system32\FxsTmp + BUILTIN\Users +C:\Windows\system32\Tasks + NT AUTHORITY\Authenticated Users +C:\Windows\system32\Com\dmp + BUILTIN\Users +C:\Windows\system32\Microsoft\Crypto\RSA\MachineKeys + Everyone +C:\Windows\system32\spool\PRINTERS + BUILTIN\Users +C:\Windows\system32\spool\SERVERS + BUILTIN\Users +C:\Windows\system32\spool\drivers\color + BUILTIN\Users +C:\Windows\system32\Tasks\Microsoft IT Diagnostics Utility + NT AUTHORITY\Authenticated Users +C:\Windows\system32\Tasks\Microsoft IT VPN + NT AUTHORITY\Authenticated Users +C:\Windows\system32\Tasks\WPD + NT AUTHORITY\Authenticated Users + aaronmar5\aaronmaradmin +C:\Windows\system32\Tasks\Microsoft\Windows\RemoteApp and Desktop Connections Update + NT AUTHORITY\Authenticated Users +C:\Windows\system32\Tasks\Microsoft\Windows\SyncCenter + BUILTIN\Users +C:\Windows\system32\Tasks\Microsoft\Windows\WCM + BUILTIN\Users +C:\Windows\system32\Tasks\Microsoft\Windows\PLA\System + Everyone + +.EXAMPLE +$x = [xml](.\Enum-WritableDirs.ps1 C:\Windows\System32 -ShowGrantees -OutputXML) +$x.root.dir | Sort-Object name + +Output: +name Grantee +---- ------- +C:\Windows\System32\Com\dmp BUILTIN\Users +C:\Windows\System32\FxsTmp BUILTIN\Users +C:\Windows\System32\Microsoft\Crypto\RSA\MachineKeys Everyone +C:\Windows\System32\spool\drivers\color BUILTIN\Users +C:\Windows\System32\spool\PRINTERS BUILTIN\Users +C:\Windows\System32\spool\SERVERS BUILTIN\Users +C:\Windows\System32\Tasks NT AUTHORITY\Authenticated Users +C:\Windows\System32\Tasks\Microsoft IT Diagnostics Utility NT AUTHORITY\Authenticated Users +C:\Windows\System32\Tasks\Microsoft IT VPN NT AUTHORITY\Authenticated Users +C:\Windows\System32\Tasks\Microsoft\Windows\PLA\System Everyone +C:\Windows\System32\Tasks\Microsoft\Windows\RemoteApp and Desktop Connection... NT AUTHORITY\Authenticated Users +C:\Windows\System32\Tasks\Microsoft\Windows\SyncCenter BUILTIN\Users +C:\Windows\System32\Tasks\Microsoft\Windows\WCM BUILTIN\Users +C:\Windows\System32\Tasks\WPD {NT AUTHORITY\Authenticated Users, vm-t2408\admin} + +#> + +param( + # Name of root directory for permission search + [parameter(Mandatory=$true)] + [String] + $RootDirectory, + + # If -ShowGrantees on the command line, shows which non-admin accounts + # are granted access + [switch] + $ShowGrantees = $false, + + # If -DontFilterNTService set, does not filter out access granted to + # NT SERVICE\ accounts (other than TrustedInstaller) + [switch] + $DontFilterNTService = $false, + + # If -OutputXML, output is XML + [switch] + $OutputXML = $false, + + # If provided, adds list of known administrative users/groups to ignore write permissions granted to + [parameter(Mandatory=$false)] + [String[]] + $KnownAdmins +) + +# If RootDirectory has a trailing backslash, remove it (AccessChk doesn't handle it correctly). +if ($RootDirectory.EndsWith("\")) { $RootDirectory = $RootDirectory.Substring(0, $RootDirectory.Length - 1) } + +# Entities for which to ignore write permissions. +# TrustedInstaller is always ignored; other NT SERVICE\ accounts are filtered +# out later (too many to list and too many unknown). +# The Package SIDs below (S-1-15-2-*) are associated with microsoft.windows.fontdrvhost and +# are not a problem. AppContainers never grant additional access; they only reduce access. +$FilterOut0 = @" +S-1-3-0 +S-1-5-18 +S-1-5-19 +S-1-5-20 +S-1-5-32-544 +S-1-5-32-549 +S-1-5-32-550 +S-1-5-32-551 +S-1-5-32-577 +S-1-5-32-559 +S-1-5-32-568 +NT SERVICE\TrustedInstaller +S-1-15-2-1430448594-2639229838-973813799-439329657-1197984847-4069167804-1277922394 +S-1-15-2-95739096-486727260-2033287795-3853587803-1685597119-444378811-2746676523 +"@ +# Filter all the above plus caller-supplied "known admins" +$FilterOut = ($FilterOut0.Split("`n`r") + $KnownAdmins | Where-Object { $_.Length -gt 0 }) -join "," +# Add all members of the local Administrators group, as the Effective Permissions +# APIs consider them to be administrators also. +# For some reason, Get-LocalGroup/Get-LocalGroupMember aren't available on WMFv5.0 on Win7; +# Verify whether command exists before using it. The commands are available on Win7 in v5.1. +if ($null -ne (Get-Command Get-LocalGroupMember -ErrorAction SilentlyContinue)) +{ + #TODO: Detect and handle case where this cmdlet fails - disconnected and the admins group contains domain SIDs that can't be resolved. + #FWIW, NET LOCALGROUP Administrators doesn't report these entries either. + #Also fails on AAD-joined, with unresolved SIDs beginning with S-1-12-1-... + Get-LocalGroupMember -SID S-1-5-32-544 -ErrorAction SilentlyContinue | ForEach-Object { $FilterOut += "," + $_.SID.Value } +} + +$currfile = "" + +if ($OutputXML) { "" } + +$bInElem = $false + +AccessChk.exe /accepteula -nobanner -w -d -s -f $FilterOut $RootDirectory | ForEach-Object { + if ($_.StartsWith(" ") -or $_.Length -eq 0) + { + if ($_.StartsWith(" RW ") -or $_.StartsWith(" W ")) + { + $grantee = $_.Substring(5).Trim() + if ($DontFilterNTService -or (!$grantee.StartsWith("NT SERVICE\") -and !$grantee.StartsWith("S-1-5-80-"))) + { + if ($currfile.Length -gt 0) + { + if ($OutputXML) + { + # Path name has to be escaped for XML + "" + } + else + { + $currfile + } + $currfile = "" + $bInElem = $true + } + if ($ShowGrantees) + { + if ($OutputXML) + { + "" + $grantee + "" + } + else + { + " " + $grantee + } + } + } + } + } + else + { + if ($bInElem -and $OutputXML) { "" } + $currfile = $_ + $bInElem = $false + } +} + +if ($bInElem -and $OutputXML) { "" } +if ($OutputXML) { "" } diff --git a/AaronLocker/Support/ExportPolicy-ToCsv.ps1 b/AaronLockerScriptBased/AaronLocker/Support/ExportPolicy-ToCsv.ps1 similarity index 97% rename from AaronLocker/Support/ExportPolicy-ToCsv.ps1 rename to AaronLockerScriptBased/AaronLocker/Support/ExportPolicy-ToCsv.ps1 index e45c7a7..aa3bd0f 100644 --- a/AaronLocker/Support/ExportPolicy-ToCsv.ps1 +++ b/AaronLockerScriptBased/AaronLocker/Support/ExportPolicy-ToCsv.ps1 @@ -1,196 +1,196 @@ -<# -.SYNOPSIS -Turn AppLocker policy into more human-readable CSV. - -.DESCRIPTION -Script reads AppLocker policy from local policy, effective policy, or an XML file, and renders it as a tab-delimited CSV that can be pasted into Microsoft Excel, with easy sorting and filtering. - -If neither -AppLockerPolicyFile or -Local is specified, the script processes the current computer's effective policy. - -If -linebreakSeq is not specified, CRLF and LF sequences in attribute values are replaced with "^|^". The linebreak sequence can be replaced after importing results into Excel (in the Find/Replace dialog, replace the sequence with Ctrl+Shift+J). - -.PARAMETER AppLockerPolicyFile -If this optional string parameter is specified, AppLocker policy is read from the specified XML file. - -.PARAMETER Local -If this switch is specified, the script processes the current computer's local policy. - -.PARAMETER linebreakSeq -If this optional string parameter is specified, CRLF and LF sequences in attribute values are replaced with the specified sequence. "^|^" is the default. - -.EXAMPLE - -ExportPolicy-ToCsv.ps1 | clip.exe - -Renders effective AppLocker policy to tab-delimited CSV and writes that output to the clipboard using the built-in Windows clip.exe utility. -Paste the output directly into an Excel spreadsheet, replace "^|^" with Ctrl+Shift+J, add filtering, freeze the top row, and autosize. - -#> - -<# -#TODO: Add option to get AppLocker policy from AD GPO -E.g., -Get-AppLockerPolicy -Domain -LDAP "LDAP://DC13.Contoso.com/CN={31B2F340-016D-11D2-945F-00C04FB984F9},CN=Policies,CN=System,DC=Contoso,DC=com" -Figure out how to tie Get-GPO in with this... - -#> - -param( - # Optional: path to XML file containing AppLocker policy - [parameter(Mandatory=$false)] - [String] - $AppLockerPolicyFile, - - # If specified, inspects local AppLocker policy rather than effective policy or an XML file - [switch] - $Local = $false, - - # Optional: specify character sequence to replace line breaks - [parameter(Mandatory=$false)] - [String] - $linebreakSeq = "^|^" -) - - -$tab = "`t" - -if ($AppLockerPolicyFile.Length -gt 0) -{ - # Get policy from a file - $x = [xml](Get-Content $AppLockerPolicyFile) -} -elseif ($Local) -{ - # Inspect local policy - $x = [xml](Get-AppLockerPolicy -Local -Xml) -} -else -{ - # Inspect effecive policy - $x = [xml](Get-AppLockerPolicy -Effective -Xml) -} - -# CSV Headers -"FileType" + $tab + -"Enforce" + $tab + -"RuleType" + $tab + -"UserOrGroup" + $tab + -"Action" + $tab + -"RuleInfo" + $tab + -"Exceptions" + $tab + -"Name" + $tab + -"Description" - - -$x.AppLockerPolicy.RuleCollection | ForEach-Object { - $filetype = $_.Type - $enforce = $_.EnforcementMode - - if ($_.ChildNodes.Count -eq 0) - { - $filetype + $tab + - $enforce + $tab + - "N/A" + $tab + - "N/A" + $tab + - "N/A" + $tab + - "N/A" + $tab + - "N/A" + $tab + - "N/A" + $tab + - "N/A" - } - else - { - $_.ChildNodes | ForEach-Object { - - $childNode = $_ - switch ( $childNode.LocalName ) - { - - "FilePublisherRule" - { - $ruletype = "Publisher" - $condition = $childNode.Conditions.FilePublisherCondition - $ruleInfo = - "Publisher: " + $condition.PublisherName + $linebreakSeq + - "Product: " + $condition.ProductName + $linebreakSeq + - "BinaryName: " + $condition.BinaryName + $linebreakSeq + - "LowVersion: " + $condition.BinaryVersionRange.LowSection + $linebreakSeq + - "HighVersion: " + $condition.BinaryVersionRange.HighSection - } - - "FilePathRule" - { - $ruletype = "Path" - $ruleInfo = $childNode.Conditions.FilePathCondition.Path - } - - "FileHashRule" - { - $ruletype = "Hash" - $condition = $childNode.Conditions.FileHashCondition.FileHash - $ruleInfo = $condition.SourceFileName + "; length = " + $condition.SourceFileLength - } - - default { $ruletype = $_.LocalName; $condition = $ruleInfo = [string]::Empty; } - - } - - $exceptions = [string]::Empty - if ($null -ne $childNode.Exceptions) - { - # Output exceptions with a designated separator character sequence that can be replaced with line feeds in Excel - $arrExceptions = @() - if ($null -ne $childNode.Exceptions.FilePathCondition) - { - $arrExceptions += "[----- Path exceptions -----]" - $arrExceptions += ($childNode.Exceptions.FilePathCondition.Path | Sort-Object) - } - if ($null -ne $childNode.Exceptions.FilePublisherCondition) - { - $arrExceptions += "[----- Publisher exceptions -----]" - $arrExceptions += ($childNode.Exceptions.FilePublisherCondition | - ForEach-Object { - $s = $_.BinaryName + ": " + $_.PublisherName + "; " + $_.ProductName - $bvrLow = $_.BinaryVersionRange.LowSection - $bvrHigh = $_.BinaryVersionRange.HighSection - if ($bvrLow -ne "*" -or $bvrHigh -ne "*") { $s += "; ver " + $bvrLow + " to " + $bvrHigh } - $s - } | Sort-Object) - } - if ($null -ne $childNode.Exceptions.FileHashCondition) - { - $arrExceptions += "[----- Hash exceptions -----]" - $arrExceptions += ($childNode.Exceptions.FileHashCondition.FileHash | ForEach-Object { $_.SourceFileName + "; length = " + $_.SourceFileLength } | Sort-Object) - } - $exceptions = $arrExceptions -join $linebreakSeq - } - - # Replace CRLF with line-break replacement string; then replace any left-over LF characters with it. - $name = $_.Name.Replace("`r`n", $linebreakSeq).Replace("`n", $linebreakSeq) - $description = $_.Description.Replace("`r`n", $linebreakSeq).Replace("`n", $linebreakSeq) - # Get user/group name if possible; otherwise show SID. #was: $userOrGroup = $_.UserOrGroupSid - $oSID = New-Object System.Security.Principal.SecurityIdentifier($_.UserOrGroupSid) - $oUser = $null - try { $oUser = $oSID.Translate([System.Security.Principal.NTAccount]) } catch {} - if ($null -ne $oUser) - { - $userOrGroup = $oUser.Value - } - else - { - $userOrGroup = $_.UserOrGroupSid - } - $action = $_.Action - - $filetype + $tab + - $enforce + $tab + - $ruletype + $tab + - $userOrGroup + $tab + - $action + $tab + - $ruleInfo + $tab + - $exceptions + $tab + - $name + $tab + - $description - } - } -} +<# +.SYNOPSIS +Turn AppLocker policy into more human-readable CSV. + +.DESCRIPTION +Script reads AppLocker policy from local policy, effective policy, or an XML file, and renders it as a tab-delimited CSV that can be pasted into Microsoft Excel, with easy sorting and filtering. + +If neither -AppLockerPolicyFile or -Local is specified, the script processes the current computer's effective policy. + +If -linebreakSeq is not specified, CRLF and LF sequences in attribute values are replaced with "^|^". The linebreak sequence can be replaced after importing results into Excel (in the Find/Replace dialog, replace the sequence with Ctrl+Shift+J). + +.PARAMETER AppLockerPolicyFile +If this optional string parameter is specified, AppLocker policy is read from the specified XML file. + +.PARAMETER Local +If this switch is specified, the script processes the current computer's local policy. + +.PARAMETER linebreakSeq +If this optional string parameter is specified, CRLF and LF sequences in attribute values are replaced with the specified sequence. "^|^" is the default. + +.EXAMPLE + +ExportPolicy-ToCsv.ps1 | clip.exe + +Renders effective AppLocker policy to tab-delimited CSV and writes that output to the clipboard using the built-in Windows clip.exe utility. +Paste the output directly into an Excel spreadsheet, replace "^|^" with Ctrl+Shift+J, add filtering, freeze the top row, and autosize. + +#> + +<# +#TODO: Add option to get AppLocker policy from AD GPO +E.g., +Get-AppLockerPolicy -Domain -LDAP "LDAP://DC13.Contoso.com/CN={31B2F340-016D-11D2-945F-00C04FB984F9},CN=Policies,CN=System,DC=Contoso,DC=com" +Figure out how to tie Get-GPO in with this... + +#> + +param( + # Optional: path to XML file containing AppLocker policy + [parameter(Mandatory=$false)] + [String] + $AppLockerPolicyFile, + + # If specified, inspects local AppLocker policy rather than effective policy or an XML file + [switch] + $Local = $false, + + # Optional: specify character sequence to replace line breaks + [parameter(Mandatory=$false)] + [String] + $linebreakSeq = "^|^" +) + + +$tab = "`t" + +if ($AppLockerPolicyFile.Length -gt 0) +{ + # Get policy from a file + $x = [xml](Get-Content $AppLockerPolicyFile) +} +elseif ($Local) +{ + # Inspect local policy + $x = [xml](Get-AppLockerPolicy -Local -Xml) +} +else +{ + # Inspect effecive policy + $x = [xml](Get-AppLockerPolicy -Effective -Xml) +} + +# CSV Headers +"FileType" + $tab + +"Enforce" + $tab + +"RuleType" + $tab + +"UserOrGroup" + $tab + +"Action" + $tab + +"RuleInfo" + $tab + +"Exceptions" + $tab + +"Name" + $tab + +"Description" + + +$x.AppLockerPolicy.RuleCollection | ForEach-Object { + $filetype = $_.Type + $enforce = $_.EnforcementMode + + if ($_.ChildNodes.Count -eq 0) + { + $filetype + $tab + + $enforce + $tab + + "N/A" + $tab + + "N/A" + $tab + + "N/A" + $tab + + "N/A" + $tab + + "N/A" + $tab + + "N/A" + $tab + + "N/A" + } + else + { + $_.ChildNodes | ForEach-Object { + + $childNode = $_ + switch ( $childNode.LocalName ) + { + + "FilePublisherRule" + { + $ruletype = "Publisher" + $condition = $childNode.Conditions.FilePublisherCondition + $ruleInfo = + "Publisher: " + $condition.PublisherName + $linebreakSeq + + "Product: " + $condition.ProductName + $linebreakSeq + + "BinaryName: " + $condition.BinaryName + $linebreakSeq + + "LowVersion: " + $condition.BinaryVersionRange.LowSection + $linebreakSeq + + "HighVersion: " + $condition.BinaryVersionRange.HighSection + } + + "FilePathRule" + { + $ruletype = "Path" + $ruleInfo = $childNode.Conditions.FilePathCondition.Path + } + + "FileHashRule" + { + $ruletype = "Hash" + $condition = $childNode.Conditions.FileHashCondition.FileHash + $ruleInfo = $condition.SourceFileName + "; length = " + $condition.SourceFileLength + } + + default { $ruletype = $_.LocalName; $condition = $ruleInfo = [string]::Empty; } + + } + + $exceptions = [string]::Empty + if ($null -ne $childNode.Exceptions) + { + # Output exceptions with a designated separator character sequence that can be replaced with line feeds in Excel + $arrExceptions = @() + if ($null -ne $childNode.Exceptions.FilePathCondition) + { + $arrExceptions += "[----- Path exceptions -----]" + $arrExceptions += ($childNode.Exceptions.FilePathCondition.Path | Sort-Object) + } + if ($null -ne $childNode.Exceptions.FilePublisherCondition) + { + $arrExceptions += "[----- Publisher exceptions -----]" + $arrExceptions += ($childNode.Exceptions.FilePublisherCondition | + ForEach-Object { + $s = $_.BinaryName + ": " + $_.PublisherName + "; " + $_.ProductName + $bvrLow = $_.BinaryVersionRange.LowSection + $bvrHigh = $_.BinaryVersionRange.HighSection + if ($bvrLow -ne "*" -or $bvrHigh -ne "*") { $s += "; ver " + $bvrLow + " to " + $bvrHigh } + $s + } | Sort-Object) + } + if ($null -ne $childNode.Exceptions.FileHashCondition) + { + $arrExceptions += "[----- Hash exceptions -----]" + $arrExceptions += ($childNode.Exceptions.FileHashCondition.FileHash | ForEach-Object { $_.SourceFileName + "; length = " + $_.SourceFileLength } | Sort-Object) + } + $exceptions = $arrExceptions -join $linebreakSeq + } + + # Replace CRLF with line-break replacement string; then replace any left-over LF characters with it. + $name = $_.Name.Replace("`r`n", $linebreakSeq).Replace("`n", $linebreakSeq) + $description = $_.Description.Replace("`r`n", $linebreakSeq).Replace("`n", $linebreakSeq) + # Get user/group name if possible; otherwise show SID. #was: $userOrGroup = $_.UserOrGroupSid + $oSID = New-Object System.Security.Principal.SecurityIdentifier($_.UserOrGroupSid) + $oUser = $null + try { $oUser = $oSID.Translate([System.Security.Principal.NTAccount]) } catch {} + if ($null -ne $oUser) + { + $userOrGroup = $oUser.Value + } + else + { + $userOrGroup = $_.UserOrGroupSid + } + $action = $_.Action + + $filetype + $tab + + $enforce + $tab + + $ruletype + $tab + + $userOrGroup + $tab + + $action + $tab + + $ruleInfo + $tab + + $exceptions + $tab + + $name + $tab + + $description + } + } +} diff --git a/AaronLocker/Support/Set-OutputEncodingToUnicode.ps1 b/AaronLockerScriptBased/AaronLocker/Support/Set-OutputEncodingToUnicode.ps1 similarity index 96% rename from AaronLocker/Support/Set-OutputEncodingToUnicode.ps1 rename to AaronLockerScriptBased/AaronLocker/Support/Set-OutputEncodingToUnicode.ps1 index 3f2fecd..e6aaa73 100644 --- a/AaronLocker/Support/Set-OutputEncodingToUnicode.ps1 +++ b/AaronLockerScriptBased/AaronLocker/Support/Set-OutputEncodingToUnicode.ps1 @@ -1,8 +1,8 @@ -<# -.SYNOPSIS -Sets the output encoding for the current session to Unicode, so that piped output retains Unicode encoding. - -#> - -$global:OutputEncoding = [System.Text.ASCIIEncoding]::Unicode - +<# +.SYNOPSIS +Sets the output encoding for the current session to Unicode, so that piped output retains Unicode encoding. + +#> + +$global:OutputEncoding = [System.Text.ASCIIEncoding]::Unicode + diff --git a/AaronLocker/Support/SupportFunctions.ps1 b/AaronLockerScriptBased/AaronLocker/Support/SupportFunctions.ps1 similarity index 97% rename from AaronLocker/Support/SupportFunctions.ps1 rename to AaronLockerScriptBased/AaronLocker/Support/SupportFunctions.ps1 index 766e483..e85408c 100644 --- a/AaronLocker/Support/SupportFunctions.ps1 +++ b/AaronLockerScriptBased/AaronLocker/Support/SupportFunctions.ps1 @@ -1,280 +1,280 @@ -<# -.SYNOPSIS -Global support functions. Intended to be dot-sourced into other scripts, and not run directly. - -.DESCRIPTION -Global support functions. Intended to be dot-sourced into other scripts, and not run directly. - -Functions to save XML consistently as Unicode: - SaveXmlDocAsUnicode([System.Xml.XmlDocument] $xmlDoc, [string] $xmlFilename) - SaveAppLockerPolicyAsUnicodeXml([Microsoft.Security.ApplicationId.PolicyManagement.PolicyModel.AppLockerPolicy]$ALPolicy, [string]$xmlFilename) - -Functions to create Excel spreadsheets/workbooks: - CreateExcelApplication() - ReleaseExcelApplication() - SelectFirstWorksheet() - SaveWorkbook([string]$filename) - AddNewWorksheet([string]$tabname) - AddWorksheetFromText([string[]]$text, [string]$tabname) - AddWorksheetFromCsvFile([string]$filename, [string]$tabname, [string]$CrLfEncoded) - AddWorksheetFromCsvData([string[]]$csv, [string]$tabname, [string]$CrLfEncoded) - CreateExcelFromCsvFile([string]$filename, [string]$tabname, [string]$CrLfEncoded, [string]$saveAsName) -#> - -#pragma once :-) -if (Test-Path("function:\SaveXmlDocAsUnicode")) -{ - return -} - -#################################################################################################### -# Ensure the AppLocker assembly is loaded. (Scripts sometimes run into TypeNotFound errors if not.) -#################################################################################################### - -[void][System.Reflection.Assembly]::LoadWithPartialName("Microsoft.Security.ApplicationId.PolicyManagement.PolicyModel") - -#################################################################################################### -# Global functions to save XML consistently as Unicode -#################################################################################################### - -# Note that the "#pragma once" thing at the beginning of this file depends on this function name -function SaveXmlDocAsUnicode([System.Xml.XmlDocument] $xmlDoc, [string] $xmlFilename) -{ - $xws = [System.Xml.XmlWriterSettings]::new() - $xws.Encoding = [System.Text.Encoding]::Unicode - $xws.Indent = $true - $xw = [System.Xml.XmlWriter]::Create($xmlFilename, $xws) - $xmlDoc.Save($xw) - $xw.Close() -} - -function SaveAppLockerPolicyAsUnicodeXml([Microsoft.Security.ApplicationId.PolicyManagement.PolicyModel.AppLockerPolicy]$ALPolicy, [string]$xmlFilename) -{ - SaveXmlDocAsUnicode -xmlDoc ([xml]($ALPolicy.ToXml())) -xmlFilename $xmlFilename -} - -#################################################################################################### -# Global functions to create Excel spreadsheets from CSV data -#################################################################################################### - -# Global variable treated as a singleton class instance, because managing this variable is a PITA otherwise. -# Not intended to be used by anything other than the functions defined below. -$ExcelAppInstance = $null - -# Create global instance of Excel application. Call ReleaseExcelApplication when done using it. -function CreateExcelApplication() -{ - Write-Host "Starting Excel..." -ForegroundColor Cyan - $global:ExcelAppInstance = New-Object -ComObject excel.application - if ($null -ne $global:ExcelAppInstance) - { - $global:ExcelAppInstance.Visible = $true - return $true - } - else - { - Write-Error "Apparently Excel is not installed. Can't create an Excel document without it. Exiting..." - return $false - } -} - -# Release global instance of Excel application. Make sure to call after CreateExcelApplication. -function ReleaseExcelApplication() -{ - Write-Host "Releasing Excel..." -ForegroundColor Cyan - $dummy = [System.Runtime.Interopservices.Marshal]::ReleaseComObject($global:ExcelAppInstance) - $global:ExcelAppInstance = $null -} - -function SelectFirstWorksheet() -{ - if ($null -eq $global:ExcelAppInstance) { return } - if ($global:ExcelAppInstance.Workbooks.Count -eq 0) { return } - $dummy = $global:ExcelAppInstance.Workbooks[1].Sheets(1).Select() -} - -function SaveWorkbook([string]$filename) -{ - Write-Host "Saving workbook as `"$filename`"..." -ForegroundColor Cyan - if ($null -eq $global:ExcelAppInstance) { return } - if ($global:ExcelAppInstance.Workbooks.Count -eq 0) { return } - $global:ExcelAppInstance.Workbooks[1].SaveAs($filename) -} - -# Add a new named worksheet with the Excel instance created through CreateExcelApplication -function AddNewWorksheet([string]$tabname) -{ - if ($null -eq $global:ExcelAppInstance) { return $null } - - if ($global:ExcelAppInstance.Workbooks.Count -eq 0) - { - $workbook = $global:ExcelAppInstance.Workbooks.Add(5) - $worksheet = $workbook.Sheets(1) - } - else - { - $workbook = $global:ExcelAppInstance.Workbooks[1] - $worksheet = $workbook.Worksheets.Add([System.Type]::Missing, $workbook.Worksheets[$workbook.Worksheets.Count]) - } - if ($tabname.Length -gt 0) - { - # Excel limits tab names to 31 characters - if ($tabname.Length -gt 31) - { - $tabname = $tabname.Substring(0, 31) - } - $worksheet.Name = $tabname - } - - $worksheet -} - -# Add a new named worksheet from lines of text (not CSV) -# Supports multi-column text; if text has tab characters, splits across cells in the row -# TODO: Add support for more than 26 columns (e.g., AA1, AB1, AA2, ...) -function AddWorksheetFromText([string[]]$text, [string]$tabname) -{ - Write-Host "Populating tab `"$tabname`"..." -ForegroundColor Cyan - - if ($null -eq $global:ExcelAppInstance) { return $null } - - $worksheet = AddNewWorksheet($tabname) - $worksheet.UsedRange.VerticalAlignment = -4160 # xlTop - - $row = [int]1 - foreach($line in $text) - { - $iCol = [int][char]'A' - $lineparts = $line.Split("`t") - foreach ( $part in $lineparts ) - { - $cell = ([char]$iCol).ToString() + $row.ToString() - $worksheet.Range($cell).FormulaR1C1 = $part - $iCol++ - } - $row++ - } - - $dummy = $worksheet.Cells.EntireColumn.AutoFit() - - # Release COM interface references - $dummy = [System.Runtime.Interopservices.Marshal]::ReleaseComObject($worksheet) -} - -# Add a new named worksheet from CSV data in the specified file, optionally replacing encoded CrLf with CrLf. -function AddWorksheetFromCsvFile([string]$filename, [string]$tabname, [string]$CrLfEncoded) -{ - Write-Host "Populating tab `"$tabname`"..." -ForegroundColor Cyan - - if ($null -eq $global:ExcelAppInstance) { return $null } - - $worksheet = AddNewWorksheet($tabname) - - ### Build the QueryTables.Add command - ### QueryTables does the same as when clicking "Data -> From Text" in Excel - $TxtConnector = ("TEXT;" + $filename) - $Connector = $worksheet.QueryTables.add($TxtConnector,$worksheet.Range("A1")) - $query = $worksheet.QueryTables.item($Connector.name) - $query.TextFileTabDelimiter = $true - - ### Execute & delete the import query - $dummy = $query.Refresh() - $query.Delete() - - if ($CrLfEncoded.Length -gt 0) - { - # Replace linebreak-replacement sequence in CSV with CRLF. - $dummy = $worksheet.UsedRange.Replace($CrLfEncoded, "`r`n") - } - - # Formatting: autofilter, font size, vertical alignment, freeze top row - $dummy = $worksheet.Cells.AutoFilter() - $worksheet.Cells.Font.Size = 9.5 - $worksheet.UsedRange.VerticalAlignment = -4160 # xlTop - $global:ExcelAppInstance.ActiveWindow.SplitColumn = 0 - $global:ExcelAppInstance.ActiveWindow.SplitRow = 1 - $global:ExcelAppInstance.ActiveWindow.FreezePanes = $true - $global:ExcelAppInstance.ActiveWindow.Zoom = 80 - - $dummy = $worksheet.Range("A2").Select() - - # Formatting: autosize column widths, then set maximum width (except on last column) - $maxWidth = 40 - $maxHeight = 120 - - $dummy = $worksheet.Cells.EntireColumn.AutoFit() - $ix = 1 - # Do this until the next to last column; don't set max width on the last column - while ( $worksheet.Cells(1, $ix + 1).Text.Length -gt 0) - { - $cells = $worksheet.Cells(1, $ix) - #Write-Host ($cells.Text + "; " + $cells.ColumnWidth) - if ($cells.ColumnWidth -gt $maxWidth) { $cells.ColumnWidth = $maxWidth } - $ix++ - } - - # Formatting: autosize row heights, then set maximum height (if CrLf replacement on) - $dummy = $worksheet.Cells.EntireRow.AutoFit() - # If line breaks added, limit autofit row height to - if ($CrLfEncoded.Length -gt 0) - { - $ix = 1 - while ( $worksheet.Cells($ix, 1).Text.Length -gt 0) - { - $cells = $worksheet.Cells($ix, 1) - #Write-Host ($ix.ToString() + "; " + $cells.RowHeight) - if ($cells.RowHeight -gt $maxHeight) { $cells.RowHeight = $maxHeight } - $ix++ - } - } - - # Release COM interface references - $dummy = [System.Runtime.Interopservices.Marshal]::ReleaseComObject($query) - $dummy = [System.Runtime.Interopservices.Marshal]::ReleaseComObject($Connector) - $dummy = [System.Runtime.Interopservices.Marshal]::ReleaseComObject($worksheet) -} - -# Add a new named worksheet from in-memory CSV data (string array), optionally replacing encoded CrLf with CrLf. -function AddWorksheetFromCsvData([string[]]$csv, [string]$tabname, [string]$CrLfEncoded) -{ - Write-Host "Preparing data for tab `"$tabname`"..." -ForegroundColor Cyan - - if ($null -eq $global:ExcelAppInstance) { return $null } - - if ($null -ne $csv) - { - $OutputEncodingPrevious = $OutputEncoding - $OutputEncoding = [System.Text.ASCIIEncoding]::Unicode - - $tempfile = [System.IO.Path]::GetTempFileName() - - $csv | Out-File $tempfile -Encoding unicode - - AddWorksheetFromCsvFile -filename $tempfile -tabname $tabname -CrLfEncoded $CrLfEncoded - - Remove-Item $tempfile - - $OutputEncoding = $OutputEncodingPrevious - } - else - { - $worksheet = AddNewWorksheet -tabname $tabname - $dummy = [System.Runtime.Interopservices.Marshal]::ReleaseComObject($worksheet) - } -} - -# Create a new Excel workbook with one named worksheet containing CSV data from the specified file, -# optionally replacing encoded CrLf with CrLf. -function CreateExcelFromCsvFile([string]$filename, [string]$tabname, [string]$CrLfEncoded, [string]$saveAsName) -{ - - if (CreateExcelApplication) - { - AddWorksheetFromCsvFile -filename $filename -tabname $tabname -CrLfEncoded $CrLfEncoded - if ($saveAsName.Length -gt 0) - { - SaveWorkbook -filename $saveAsName - } - ReleaseExcelApplication - } -} +<# +.SYNOPSIS +Global support functions. Intended to be dot-sourced into other scripts, and not run directly. + +.DESCRIPTION +Global support functions. Intended to be dot-sourced into other scripts, and not run directly. + +Functions to save XML consistently as Unicode: + SaveXmlDocAsUnicode([System.Xml.XmlDocument] $xmlDoc, [string] $xmlFilename) + SaveAppLockerPolicyAsUnicodeXml([Microsoft.Security.ApplicationId.PolicyManagement.PolicyModel.AppLockerPolicy]$ALPolicy, [string]$xmlFilename) + +Functions to create Excel spreadsheets/workbooks: + CreateExcelApplication() + ReleaseExcelApplication() + SelectFirstWorksheet() + SaveWorkbook([string]$filename) + AddNewWorksheet([string]$tabname) + AddWorksheetFromText([string[]]$text, [string]$tabname) + AddWorksheetFromCsvFile([string]$filename, [string]$tabname, [string]$CrLfEncoded) + AddWorksheetFromCsvData([string[]]$csv, [string]$tabname, [string]$CrLfEncoded) + CreateExcelFromCsvFile([string]$filename, [string]$tabname, [string]$CrLfEncoded, [string]$saveAsName) +#> + +#pragma once :-) +if (Test-Path("function:\SaveXmlDocAsUnicode")) +{ + return +} + +#################################################################################################### +# Ensure the AppLocker assembly is loaded. (Scripts sometimes run into TypeNotFound errors if not.) +#################################################################################################### + +[void][System.Reflection.Assembly]::LoadWithPartialName("Microsoft.Security.ApplicationId.PolicyManagement.PolicyModel") + +#################################################################################################### +# Global functions to save XML consistently as Unicode +#################################################################################################### + +# Note that the "#pragma once" thing at the beginning of this file depends on this function name +function SaveXmlDocAsUnicode([System.Xml.XmlDocument] $xmlDoc, [string] $xmlFilename) +{ + $xws = [System.Xml.XmlWriterSettings]::new() + $xws.Encoding = [System.Text.Encoding]::Unicode + $xws.Indent = $true + $xw = [System.Xml.XmlWriter]::Create($xmlFilename, $xws) + $xmlDoc.Save($xw) + $xw.Close() +} + +function SaveAppLockerPolicyAsUnicodeXml([Microsoft.Security.ApplicationId.PolicyManagement.PolicyModel.AppLockerPolicy]$ALPolicy, [string]$xmlFilename) +{ + SaveXmlDocAsUnicode -xmlDoc ([xml]($ALPolicy.ToXml())) -xmlFilename $xmlFilename +} + +#################################################################################################### +# Global functions to create Excel spreadsheets from CSV data +#################################################################################################### + +# Global variable treated as a singleton class instance, because managing this variable is a PITA otherwise. +# Not intended to be used by anything other than the functions defined below. +$ExcelAppInstance = $null + +# Create global instance of Excel application. Call ReleaseExcelApplication when done using it. +function CreateExcelApplication() +{ + Write-Host "Starting Excel..." -ForegroundColor Cyan + $global:ExcelAppInstance = New-Object -ComObject excel.application + if ($null -ne $global:ExcelAppInstance) + { + $global:ExcelAppInstance.Visible = $true + return $true + } + else + { + Write-Error "Apparently Excel is not installed. Can't create an Excel document without it. Exiting..." + return $false + } +} + +# Release global instance of Excel application. Make sure to call after CreateExcelApplication. +function ReleaseExcelApplication() +{ + Write-Host "Releasing Excel..." -ForegroundColor Cyan + $dummy = [System.Runtime.Interopservices.Marshal]::ReleaseComObject($global:ExcelAppInstance) + $global:ExcelAppInstance = $null +} + +function SelectFirstWorksheet() +{ + if ($null -eq $global:ExcelAppInstance) { return } + if ($global:ExcelAppInstance.Workbooks.Count -eq 0) { return } + $dummy = $global:ExcelAppInstance.Workbooks[1].Sheets(1).Select() +} + +function SaveWorkbook([string]$filename) +{ + Write-Host "Saving workbook as `"$filename`"..." -ForegroundColor Cyan + if ($null -eq $global:ExcelAppInstance) { return } + if ($global:ExcelAppInstance.Workbooks.Count -eq 0) { return } + $global:ExcelAppInstance.Workbooks[1].SaveAs($filename) +} + +# Add a new named worksheet with the Excel instance created through CreateExcelApplication +function AddNewWorksheet([string]$tabname) +{ + if ($null -eq $global:ExcelAppInstance) { return $null } + + if ($global:ExcelAppInstance.Workbooks.Count -eq 0) + { + $workbook = $global:ExcelAppInstance.Workbooks.Add(5) + $worksheet = $workbook.Sheets(1) + } + else + { + $workbook = $global:ExcelAppInstance.Workbooks[1] + $worksheet = $workbook.Worksheets.Add([System.Type]::Missing, $workbook.Worksheets[$workbook.Worksheets.Count]) + } + if ($tabname.Length -gt 0) + { + # Excel limits tab names to 31 characters + if ($tabname.Length -gt 31) + { + $tabname = $tabname.Substring(0, 31) + } + $worksheet.Name = $tabname + } + + $worksheet +} + +# Add a new named worksheet from lines of text (not CSV) +# Supports multi-column text; if text has tab characters, splits across cells in the row +# TODO: Add support for more than 26 columns (e.g., AA1, AB1, AA2, ...) +function AddWorksheetFromText([string[]]$text, [string]$tabname) +{ + Write-Host "Populating tab `"$tabname`"..." -ForegroundColor Cyan + + if ($null -eq $global:ExcelAppInstance) { return $null } + + $worksheet = AddNewWorksheet($tabname) + $worksheet.UsedRange.VerticalAlignment = -4160 # xlTop + + $row = [int]1 + foreach($line in $text) + { + $iCol = [int][char]'A' + $lineparts = $line.Split("`t") + foreach ( $part in $lineparts ) + { + $cell = ([char]$iCol).ToString() + $row.ToString() + $worksheet.Range($cell).FormulaR1C1 = $part + $iCol++ + } + $row++ + } + + $dummy = $worksheet.Cells.EntireColumn.AutoFit() + + # Release COM interface references + $dummy = [System.Runtime.Interopservices.Marshal]::ReleaseComObject($worksheet) +} + +# Add a new named worksheet from CSV data in the specified file, optionally replacing encoded CrLf with CrLf. +function AddWorksheetFromCsvFile([string]$filename, [string]$tabname, [string]$CrLfEncoded) +{ + Write-Host "Populating tab `"$tabname`"..." -ForegroundColor Cyan + + if ($null -eq $global:ExcelAppInstance) { return $null } + + $worksheet = AddNewWorksheet($tabname) + + ### Build the QueryTables.Add command + ### QueryTables does the same as when clicking "Data -> From Text" in Excel + $TxtConnector = ("TEXT;" + $filename) + $Connector = $worksheet.QueryTables.add($TxtConnector,$worksheet.Range("A1")) + $query = $worksheet.QueryTables.item($Connector.name) + $query.TextFileTabDelimiter = $true + + ### Execute & delete the import query + $dummy = $query.Refresh() + $query.Delete() + + if ($CrLfEncoded.Length -gt 0) + { + # Replace linebreak-replacement sequence in CSV with CRLF. + $dummy = $worksheet.UsedRange.Replace($CrLfEncoded, "`r`n") + } + + # Formatting: autofilter, font size, vertical alignment, freeze top row + $dummy = $worksheet.Cells.AutoFilter() + $worksheet.Cells.Font.Size = 9.5 + $worksheet.UsedRange.VerticalAlignment = -4160 # xlTop + $global:ExcelAppInstance.ActiveWindow.SplitColumn = 0 + $global:ExcelAppInstance.ActiveWindow.SplitRow = 1 + $global:ExcelAppInstance.ActiveWindow.FreezePanes = $true + $global:ExcelAppInstance.ActiveWindow.Zoom = 80 + + $dummy = $worksheet.Range("A2").Select() + + # Formatting: autosize column widths, then set maximum width (except on last column) + $maxWidth = 40 + $maxHeight = 120 + + $dummy = $worksheet.Cells.EntireColumn.AutoFit() + $ix = 1 + # Do this until the next to last column; don't set max width on the last column + while ( $worksheet.Cells(1, $ix + 1).Text.Length -gt 0) + { + $cells = $worksheet.Cells(1, $ix) + #Write-Host ($cells.Text + "; " + $cells.ColumnWidth) + if ($cells.ColumnWidth -gt $maxWidth) { $cells.ColumnWidth = $maxWidth } + $ix++ + } + + # Formatting: autosize row heights, then set maximum height (if CrLf replacement on) + $dummy = $worksheet.Cells.EntireRow.AutoFit() + # If line breaks added, limit autofit row height to + if ($CrLfEncoded.Length -gt 0) + { + $ix = 1 + while ( $worksheet.Cells($ix, 1).Text.Length -gt 0) + { + $cells = $worksheet.Cells($ix, 1) + #Write-Host ($ix.ToString() + "; " + $cells.RowHeight) + if ($cells.RowHeight -gt $maxHeight) { $cells.RowHeight = $maxHeight } + $ix++ + } + } + + # Release COM interface references + $dummy = [System.Runtime.Interopservices.Marshal]::ReleaseComObject($query) + $dummy = [System.Runtime.Interopservices.Marshal]::ReleaseComObject($Connector) + $dummy = [System.Runtime.Interopservices.Marshal]::ReleaseComObject($worksheet) +} + +# Add a new named worksheet from in-memory CSV data (string array), optionally replacing encoded CrLf with CrLf. +function AddWorksheetFromCsvData([string[]]$csv, [string]$tabname, [string]$CrLfEncoded) +{ + Write-Host "Preparing data for tab `"$tabname`"..." -ForegroundColor Cyan + + if ($null -eq $global:ExcelAppInstance) { return $null } + + if ($null -ne $csv) + { + $OutputEncodingPrevious = $OutputEncoding + $OutputEncoding = [System.Text.ASCIIEncoding]::Unicode + + $tempfile = [System.IO.Path]::GetTempFileName() + + $csv | Out-File $tempfile -Encoding unicode + + AddWorksheetFromCsvFile -filename $tempfile -tabname $tabname -CrLfEncoded $CrLfEncoded + + Remove-Item $tempfile + + $OutputEncoding = $OutputEncodingPrevious + } + else + { + $worksheet = AddNewWorksheet -tabname $tabname + $dummy = [System.Runtime.Interopservices.Marshal]::ReleaseComObject($worksheet) + } +} + +# Create a new Excel workbook with one named worksheet containing CSV data from the specified file, +# optionally replacing encoded CrLf with CrLf. +function CreateExcelFromCsvFile([string]$filename, [string]$tabname, [string]$CrLfEncoded, [string]$saveAsName) +{ + + if (CreateExcelApplication) + { + AddWorksheetFromCsvFile -filename $filename -tabname $tabname -CrLfEncoded $CrLfEncoded + if ($saveAsName.Length -gt 0) + { + SaveWorkbook -filename $saveAsName + } + ReleaseExcelApplication + } +} diff --git a/Documentation/AaronLocker.docx b/AaronLockerScriptBased/Documentation/AaronLocker.docx similarity index 100% rename from Documentation/AaronLocker.docx rename to AaronLockerScriptBased/Documentation/AaronLocker.docx diff --git a/Documentation/Known Issues.docx b/AaronLockerScriptBased/Documentation/Known Issues.docx similarity index 100% rename from Documentation/Known Issues.docx rename to AaronLockerScriptBased/Documentation/Known Issues.docx diff --git a/build/filesAfter.txt b/build/filesAfter.txt new file mode 100644 index 0000000..3e4d54b --- /dev/null +++ b/build/filesAfter.txt @@ -0,0 +1,5 @@ +# List all files that are loaded in the preimport.ps1 +# In the order they are loaded during preimport + +internal\scripts\variables.ps1 +internal\scripts\resolveFileRule.ps1 \ No newline at end of file diff --git a/build/filesBefore.txt b/build/filesBefore.txt new file mode 100644 index 0000000..48800ce --- /dev/null +++ b/build/filesBefore.txt @@ -0,0 +1,4 @@ +# List all files that are loaded in the postimport.ps1 +# In the order they are loaded during postimport + +bin\types.ps1 \ No newline at end of file diff --git a/build/vsts-build.ps1 b/build/vsts-build.ps1 new file mode 100644 index 0000000..89bf167 --- /dev/null +++ b/build/vsts-build.ps1 @@ -0,0 +1,65 @@ +<# +This script publishes the module to the gallery. +It expects as input an ApiKey authorized to publish the module. + +Insert any build steps you may need to take before publishing it here. +#> +param ( + $ApiKey +) + +# Prepare publish folder +Write-PSFMessage -Level Important -Message "Creating and populating publishing directory" +$publishDir = New-Item -Path $env:SYSTEM_DEFAULTWORKINGDIRECTORY -Name publish -ItemType Directory +Copy-Item -Path "$($env:SYSTEM_DEFAULTWORKINGDIRECTORY)\AaronLocker" -Destination $publishDir.FullName -Recurse -Force + +# Create commands.ps1 +$text = @() +Get-ChildItem -Path "$($publishDir.FullName)\AaronLocker\internal\functions\" -Recurse -File -Filter "*.ps1" | ForEach-Object { + $text += [System.IO.File]::ReadAllText($_.FullName) +} +Get-ChildItem -Path "$($publishDir.FullName)\AaronLocker\functions\" -Recurse -File -Filter "*.ps1" | ForEach-Object { + $text += [System.IO.File]::ReadAllText($_.FullName) +} +$text -join "`n`n" | Set-Content -Path "$($publishDir.FullName)\AaronLocker\commands.ps1" + +# Create resourcesBefore.ps1 +$processed = @() +$text = @() +foreach ($line in (Get-Content "$($PSScriptRoot)\filesBefore.txt" | Where-Object { $_ -notlike "#*" })) +{ + if ([string]::IsNullOrWhiteSpace($line)) { continue } + + $basePath = Join-Path "$($publishDir.FullName)\AaronLocker" $line + foreach ($entry in (Resolve-PSFPath -Path $basePath)) + { + $item = Get-Item $entry + if ($item.PSIsContainer) { continue } + if ($item.FullName -in $processed) { continue } + $text += [System.IO.File]::ReadAllText($item.FullName) + $processed += $item.FullName + } +} +if ($text) { $text -join "`n`n" | Set-Content -Path "$($publishDir.FullName)\AaronLocker\resourcesBefore.ps1" } + +# Create resourcesAfter.ps1 +$processed = @() +$text = @() +foreach ($line in (Get-Content "$($PSScriptRoot)\filesAfter.txt" | Where-Object { $_ -notlike "#*" })) +{ + if ([string]::IsNullOrWhiteSpace($line)) { continue } + + $basePath = Join-Path "$($publishDir.FullName)\AaronLocker" $line + foreach ($entry in (Resolve-PSFPath -Path $basePath)) + { + $item = Get-Item $entry + if ($item.PSIsContainer) { continue } + if ($item.FullName -in $processed) { continue } + $text += [System.IO.File]::ReadAllText($item.FullName) + $processed += $item.FullName + } +} +if ($text) { $text -join "`n`n" | Set-Content -Path "$($publishDir.FullName)\AaronLocker\resourcesAfter.ps1" } + +# Publish to Gallery +Publish-Module -Path "$($publishDir.FullName)\AaronLocker" -NuGetApiKey $ApiKey -Force \ No newline at end of file diff --git a/build/vsts-prerequisites.ps1 b/build/vsts-prerequisites.ps1 new file mode 100644 index 0000000..618d153 --- /dev/null +++ b/build/vsts-prerequisites.ps1 @@ -0,0 +1,4 @@ +Write-Host "Installing Pester" -ForegroundColor Cyan +Install-Module Pester -Force -SkipPublisherCheck +Write-Host "Installing PSFramework" -ForegroundColor Cyan +Install-Module PSFramework -Force -SkipPublisherCheck \ No newline at end of file diff --git a/build/vsts-validate.ps1 b/build/vsts-validate.ps1 new file mode 100644 index 0000000..ef5f6db --- /dev/null +++ b/build/vsts-validate.ps1 @@ -0,0 +1,7 @@ +# Guide for available variables and working with secrets: +# https://docs.microsoft.com/en-us/vsts/build-release/concepts/definitions/build/variables?tabs=powershell + +# Needs to ensure things are Done Right and only legal commits to master get built + +# Run internal pester tests +& "$PSScriptRoot\..\AaronLocker\tests\pester.ps1" \ No newline at end of file diff --git a/install.ps1 b/install.ps1 new file mode 100644 index 0000000..0e0634f --- /dev/null +++ b/install.ps1 @@ -0,0 +1,2414 @@ +<# + .SYNOPSIS + Installs the AaronLocker Module from github + + .DESCRIPTION + This script installs the AaronLocker Module from github. + + It does so by ... + - downloading the specified branch as zip to $env:TEMP + - Unpacking that zip file to a folder in $env:TEMP + - Moving that content to a module folder in either program files (default) or the user profile + + .PARAMETER Branch + The branch to install. Installs master by default. + Unknown branches will terminate the script in error. + + .PARAMETER UserMode + The downloaded module will be moved to the user profile, rather than program files. + + .PARAMETER Force + The install script will overwrite an existing module. +#> +[CmdletBinding()] +Param ( + [string] + $Branch = "master", + + [switch] + $UserMode, + + [switch] + $Force +) + +#region Configuration for cloning script +# Name of the module that is being cloned +$ModuleName = "AaronLocker" + +# Base path to the github repository +$BaseUrl = "https://github.com//AaronLocker" + +# If the module is in a subfolder of the cloned repository, specify relative path here. Empty string to skip. +$SubFolder = "AaronLocker" +#endregion Configuration for cloning script + +#region Utility Functions +function Compress-Archive +{ + <# + .SYNOPSIS + Creates an archive, or zipped file, from specified files and folders. + + .DESCRIPTION + The Compress-Archive cmdlet creates a zipped (or compressed) archive file from one or more specified files or folders. An archive file allows multiple files to be packaged, and optionally compressed, into a single zipped file for easier distribution and storage. An archive file can be compressed by using the compression algorithm specified by the CompressionLevel parameter. + + Because Compress-Archive relies upon the Microsoft .NET Framework API System.IO.Compression.ZipArchive to compress files, the maximum file size that you can compress by using Compress-Archive is currently 2 GB. This is a limitation of the underlying API. + + .PARAMETER Path + Specifies the path or paths to the files that you want to add to the archive zipped file. This parameter can accept wildcard characters. Wildcard characters allow you to add all files in a folder to your zipped archive file. To specify multiple paths, and include files in multiple locations in your output zipped file, use commas to separate the paths. + + .PARAMETER LiteralPath + Specifies the path or paths to the files that you want to add to the archive zipped file. Unlike the Path parameter, the value of LiteralPath is used exactly as it is typed. No characters are interpreted as wildcards. If the path includes escape characters, enclose each escape character in single quotation marks, to instruct Windows PowerShell not to interpret any characters as escape sequences. To specify multiple paths, and include files in multiple locations in your output zipped file, use commas to separate the paths. + + .PARAMETER DestinationPath + Specifies the path to the archive output file. This parameter is required. The specified DestinationPath value should include the desired name of the output zipped file; it specifies either the absolute or relative path to the zipped file. If the file name specified in DestinationPath does not have a .zip file name extension, the cmdlet adds a .zip file name extension. + + .PARAMETER CompressionLevel + Specifies how much compression to apply when you are creating the archive file. Faster compression requires less time to create the file, but can result in larger file sizes. The acceptable values for this parameter are: + + - Fastest. Use the fastest compression method available to decrease processing time; this can result in larger file sizes. + - NoCompression. Do not compress the source files. + - Optimal. Processing time is dependent on file size. + + If this parameter is not specified, the command uses the default value, Optimal. + + .PARAMETER Update + Updates the specified archive by replacing older versions of files in the archive with newer versions of files that have the same names. You can also add this parameter to add files to an existing archive. + + .PARAMETER Force + @{Text=} + + .PARAMETER Confirm + Prompts you for confirmation before running the cmdlet. + + .PARAMETER WhatIf + Shows what would happen if the cmdlet runs. The cmdlet is not run. + + .EXAMPLE + Example 1: Create an archive file + + PS C:\>Compress-Archive -LiteralPath C:\Reference\Draftdoc.docx, C:\Reference\Images\diagram2.vsd -CompressionLevel Optimal -DestinationPath C:\Archives\Draft.Zip + + This command creates a new archive file, Draft.zip, by compressing two files, Draftdoc.docx and diagram2.vsd, specified by the LiteralPath parameter. The compression level specified for this operation is Optimal. + + .EXAMPLE + Example 2: Create an archive with wildcard characters + + PS C:\>Compress-Archive -Path C:\Reference\* -CompressionLevel Fastest -DestinationPath C:\Archives\Draft + + This command creates a new archive file, Draft.zip, in the C:\Archives folder. Note that though the file name extension .zip was not added to the value of the DestinationPath parameter, Windows PowerShell appends this to the specified archive file name automatically. The new archive file contains every file in the C:\Reference folder, because a wildcard character was used in place of specific file names in the Path parameter. The specified compression level is Fastest, which might result in a larger output file, but compresses a large number of files faster. + + .EXAMPLE + Example 3: Update an existing archive file + + PS C:\>Compress-Archive -Path C:\Reference\* -Update -DestinationPath C:\Archives\Draft.Zip + + This command updates an existing archive file, Draft.Zip, in the C:\Archives folder. The command is run to update Draft.Zip with newer versions of existing files that came from the C:\Reference folder, and also to add new files that have been added to C:\Reference since Draft.Zip was initially created. + + .EXAMPLE + Example 4: Create an archive from an entire folder + + PS C:\>Compress-Archive -Path C:\Reference -DestinationPath C:\Archives\Draft + + This command creates an archive from an entire folder, C:\Reference. Note that though the file name extension .zip was not added to the value of the DestinationPath parameter, Windows PowerShell appends this to the specified archive file name automatically. + #> + [CmdletBinding(DefaultParameterSetName = "Path", SupportsShouldProcess = $true, HelpUri = "http://go.microsoft.com/fwlink/?LinkID=393252")] + param + ( + [parameter (mandatory = $true, Position = 0, ParameterSetName = "Path", ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] + [parameter (mandatory = $true, Position = 0, ParameterSetName = "PathWithForce", ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] + [parameter (mandatory = $true, Position = 0, ParameterSetName = "PathWithUpdate", ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] + [ValidateNotNullOrEmpty()] + [string[]] + $Path, + + [parameter (mandatory = $true, ParameterSetName = "LiteralPath", ValueFromPipeline = $false, ValueFromPipelineByPropertyName = $true)] + [parameter (mandatory = $true, ParameterSetName = "LiteralPathWithForce", ValueFromPipeline = $false, ValueFromPipelineByPropertyName = $true)] + [parameter (mandatory = $true, ParameterSetName = "LiteralPathWithUpdate", ValueFromPipeline = $false, ValueFromPipelineByPropertyName = $true)] + [ValidateNotNullOrEmpty()] + [Alias("PSPath")] + [string[]] + $LiteralPath, + + [parameter (mandatory = $true, + Position = 1, + ValueFromPipeline = $false, + ValueFromPipelineByPropertyName = $false)] + [ValidateNotNullOrEmpty()] + [string] + $DestinationPath, + + [parameter ( + mandatory = $false, + ValueFromPipeline = $false, + ValueFromPipelineByPropertyName = $false)] + [ValidateSet("Optimal", "NoCompression", "Fastest")] + [string] + $CompressionLevel = "Optimal", + + [parameter(mandatory = $true, ParameterSetName = "PathWithUpdate", ValueFromPipeline = $false, ValueFromPipelineByPropertyName = $false)] + [parameter(mandatory = $true, ParameterSetName = "LiteralPathWithUpdate", ValueFromPipeline = $false, ValueFromPipelineByPropertyName = $false)] + [switch] + $Update = $false, + + [parameter(mandatory = $true, ParameterSetName = "PathWithForce", ValueFromPipeline = $false, ValueFromPipelineByPropertyName = $false)] + [parameter(mandatory = $true, ParameterSetName = "LiteralPathWithForce", ValueFromPipeline = $false, ValueFromPipelineByPropertyName = $false)] + [switch] + $Force = $false + ) + + BEGIN + { + Add-Type -AssemblyName System.IO.Compression -ErrorAction Ignore + Add-Type -AssemblyName System.IO.Compression.FileSystem -ErrorAction Ignore + + $zipFileExtension = ".zip" + + $LocalizedData = ConvertFrom-StringData @' +PathNotFoundError=The path '{0}' either does not exist or is not a valid file system path. +ExpandArchiveInValidDestinationPath=The path '{0}' is not a valid file system directory path. +InvalidZipFileExtensionError={0} is not a supported archive file format. {1} is the only supported archive file format. +ArchiveFileIsReadOnly=The attributes of the archive file {0} is set to 'ReadOnly' hence it cannot be updated. If you intend to update the existing archive file, remove the 'ReadOnly' attribute on the archive file else use -Force parameter to override and create a new archive file. +ZipFileExistError=The archive file {0} already exists. Use the -Update parameter to update the existing archive file or use the -Force parameter to overwrite the existing archive file. +DuplicatePathFoundError=The input to {0} parameter contains a duplicate path '{1}'. Provide a unique set of paths as input to {2} parameter. +ArchiveFileIsEmpty=The archive file {0} is empty. +CompressProgressBarText=The archive file '{0}' creation is in progress... +ExpandProgressBarText=The archive file '{0}' expansion is in progress... +AppendArchiveFileExtensionMessage=The archive file path '{0}' supplied to the DestinationPath patameter does not include .zip extension. Hence .zip is appended to the supplied DestinationPath path and the archive file would be created at '{1}'. +AddItemtoArchiveFile=Adding '{0}'. +CreateFileAtExpandedPath=Created '{0}'. +InvalidArchiveFilePathError=The archive file path '{0}' specified as input to the {1} parameter is resolving to multiple file system paths. Provide a unique path to the {2} parameter where the archive file has to be created. +InvalidExpandedDirPathError=The directory path '{0}' specified as input to the DestinationPath parameter is resolving to multiple file system paths. Provide a unique path to the Destination parameter where the archive file contents have to be expanded. +FileExistsError=Failed to create file '{0}' while expanding the archive file '{1}' contents as the file '{2}' already exists. Use the -Force parameter if you want to overwrite the existing directory '{3}' contents when expanding the archive file. +DeleteArchiveFile=The partially created archive file '{0}' is deleted as it is not usable. +InvalidDestinationPath=The destination path '{0}' does not contain a valid archive file name. +PreparingToCompressVerboseMessage=Preparing to compress... +PreparingToExpandVerboseMessage=Preparing to expand... +'@ + + #region Utility Functions + function GetResolvedPathHelper + { + param + ( + [string[]] + $path, + + [boolean] + $isLiteralPath, + + [System.Management.Automation.PSCmdlet] + $callerPSCmdlet + ) + + $resolvedPaths = @() + + # null and empty check are are already done on Path parameter at the cmdlet layer. + foreach ($currentPath in $path) + { + try + { + if ($isLiteralPath) + { + $currentResolvedPaths = Resolve-Path -LiteralPath $currentPath -ErrorAction Stop + } + else + { + $currentResolvedPaths = Resolve-Path -Path $currentPath -ErrorAction Stop + } + } + catch + { + $errorMessage = ($LocalizedData.PathNotFoundError -f $currentPath) + $exception = New-Object System.InvalidOperationException $errorMessage, $_.Exception + $errorRecord = CreateErrorRecordHelper "ArchiveCmdletPathNotFound" $null ([System.Management.Automation.ErrorCategory]::InvalidArgument) $exception $currentPath + $callerPSCmdlet.ThrowTerminatingError($errorRecord) + } + + foreach ($currentResolvedPath in $currentResolvedPaths) + { + $resolvedPaths += $currentResolvedPath.ProviderPath + } + } + + $resolvedPaths + } + + function Add-CompressionAssemblies + { + + if ($PSEdition -eq "Desktop") + { + Add-Type -AssemblyName System.IO.Compression + Add-Type -AssemblyName System.IO.Compression.FileSystem + } + } + + function IsValidFileSystemPath + { + param + ( + [string[]] + $path + ) + + $result = $true; + + # null and empty check are are already done on Path parameter at the cmdlet layer. + foreach ($currentPath in $path) + { + if (!([System.IO.File]::Exists($currentPath) -or [System.IO.Directory]::Exists($currentPath))) + { + $errorMessage = ($LocalizedData.PathNotFoundError -f $currentPath) + ThrowTerminatingErrorHelper "PathNotFound" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidArgument) $currentPath + } + } + + return $result; + } + + + function ValidateDuplicateFileSystemPath + { + param + ( + [string] + $inputParameter, + + [string[]] + $path + ) + + $uniqueInputPaths = @() + + # null and empty check are are already done on Path parameter at the cmdlet layer. + foreach ($currentPath in $path) + { + $currentInputPath = $currentPath.ToUpper() + if ($uniqueInputPaths.Contains($currentInputPath)) + { + $errorMessage = ($LocalizedData.DuplicatePathFoundError -f $inputParameter, $currentPath, $inputParameter) + ThrowTerminatingErrorHelper "DuplicatePathFound" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidArgument) $currentPath + } + else + { + $uniqueInputPaths += $currentInputPath + } + } + } + + function CompressionLevelMapper + { + param + ( + [string] + $compressionLevel + ) + + $compressionLevelFormat = [System.IO.Compression.CompressionLevel]::Optimal + + # CompressionLevel format is already validated at the cmdlet layer. + switch ($compressionLevel.ToString()) + { + "Fastest" + { + $compressionLevelFormat = [System.IO.Compression.CompressionLevel]::Fastest + } + "NoCompression" + { + $compressionLevelFormat = [System.IO.Compression.CompressionLevel]::NoCompression + } + } + + return $compressionLevelFormat + } + + function CompressArchiveHelper + { + param + ( + [string[]] + $sourcePath, + + [string] + $destinationPath, + + [string] + $compressionLevel, + + [bool] + $isUpdateMode + ) + + $numberOfItemsArchived = 0 + $sourceFilePaths = @() + $sourceDirPaths = @() + + foreach ($currentPath in $sourcePath) + { + $result = Test-Path -LiteralPath $currentPath -PathType Leaf + if ($result -eq $true) + { + $sourceFilePaths += $currentPath + } + else + { + $sourceDirPaths += $currentPath + } + } + + # The Soure Path contains one or more directory (this directory can have files under it) and no files to be compressed. + if ($sourceFilePaths.Count -eq 0 -and $sourceDirPaths.Count -gt 0) + { + $currentSegmentWeight = 100/[double]$sourceDirPaths.Count + $previousSegmentWeight = 0 + foreach ($currentSourceDirPath in $sourceDirPaths) + { + $count = CompressSingleDirHelper $currentSourceDirPath $destinationPath $compressionLevel $true $isUpdateMode $previousSegmentWeight $currentSegmentWeight + $numberOfItemsArchived += $count + $previousSegmentWeight += $currentSegmentWeight + } + } + + # The Soure Path contains only files to be compressed. + elseIf ($sourceFilePaths.Count -gt 0 -and $sourceDirPaths.Count -eq 0) + { + # $previousSegmentWeight is equal to 0 as there are no prior segments. + # $currentSegmentWeight is set to 100 as all files have equal weightage. + $previousSegmentWeight = 0 + $currentSegmentWeight = 100 + + $numberOfItemsArchived = CompressFilesHelper $sourceFilePaths $destinationPath $compressionLevel $isUpdateMode $previousSegmentWeight $currentSegmentWeight + } + # The Soure Path contains one or more files and one or more directories (this directory can have files under it) to be compressed. + elseif ($sourceFilePaths.Count -gt 0 -and $sourceDirPaths.Count -gt 0) + { + # each directory is considered as an individual segments & all the individual files are clubed in to a separate sgemnet. + $currentSegmentWeight = 100/[double]($sourceDirPaths.Count + 1) + $previousSegmentWeight = 0 + + foreach ($currentSourceDirPath in $sourceDirPaths) + { + $count = CompressSingleDirHelper $currentSourceDirPath $destinationPath $compressionLevel $true $isUpdateMode $previousSegmentWeight $currentSegmentWeight + $numberOfItemsArchived += $count + $previousSegmentWeight += $currentSegmentWeight + } + + $count = CompressFilesHelper $sourceFilePaths $destinationPath $compressionLevel $isUpdateMode $previousSegmentWeight $currentSegmentWeight + $numberOfItemsArchived += $count + } + + return $numberOfItemsArchived + } + + function CompressFilesHelper + { + param + ( + [string[]] + $sourceFilePaths, + + [string] + $destinationPath, + + [string] + $compressionLevel, + + [bool] + $isUpdateMode, + + [double] + $previousSegmentWeight, + + [double] + $currentSegmentWeight + ) + + $numberOfItemsArchived = ZipArchiveHelper $sourceFilePaths $destinationPath $compressionLevel $isUpdateMode $null $previousSegmentWeight $currentSegmentWeight + + return $numberOfItemsArchived + } + + function CompressSingleDirHelper + { + param + ( + [string] + $sourceDirPath, + + [string] + $destinationPath, + + [string] + $compressionLevel, + + [bool] + $useParentDirAsRoot, + + [bool] + $isUpdateMode, + + [double] + $previousSegmentWeight, + + [double] + $currentSegmentWeight + ) + + [System.Collections.Generic.List[System.String]]$subDirFiles = @() + + if ($useParentDirAsRoot) + { + $sourceDirInfo = New-Object -TypeName System.IO.DirectoryInfo -ArgumentList $sourceDirPath + $sourceDirFullName = $sourceDirInfo.Parent.FullName + + # If the directory is present at the drive level the DirectoryInfo.Parent include '\' example: C:\ + # On the other hand if the directory exists at a deper level then DirectoryInfo.Parent + # has just the path (without an ending '\'). example C:\source + if ($sourceDirFullName.Length -eq 3) + { + $modifiedSourceDirFullName = $sourceDirFullName + } + else + { + $modifiedSourceDirFullName = $sourceDirFullName + "\" + } + } + else + { + $sourceDirFullName = $sourceDirPath + $modifiedSourceDirFullName = $sourceDirFullName + "\" + } + + $dirContents = Get-ChildItem -LiteralPath $sourceDirPath -Recurse + foreach ($currentContent in $dirContents) + { + $isContainer = $currentContent -is [System.IO.DirectoryInfo] + if (!$isContainer) + { + $subDirFiles.Add($currentContent.FullName) + } + else + { + # The currentContent points to a directory. + # We need to check if the directory is an empty directory, if so such a + # directory has to be explictly added to the archive file. + # if there are no files in the directory the GetFiles() API returns an empty array. + $files = $currentContent.GetFiles() + if ($files.Count -eq 0) + { + $subDirFiles.Add($currentContent.FullName + "\") + } + } + } + + $numberOfItemsArchived = ZipArchiveHelper $subDirFiles.ToArray() $destinationPath $compressionLevel $isUpdateMode $modifiedSourceDirFullName $previousSegmentWeight $currentSegmentWeight + + return $numberOfItemsArchived + } + + function ZipArchiveHelper + { + param + ( + [System.Collections.Generic.List[System.String]] + $sourcePaths, + + [string] + $destinationPath, + + [string] + $compressionLevel, + + [bool] + $isUpdateMode, + + [string] + $modifiedSourceDirFullName, + + [double] + $previousSegmentWeight, + + [double] + $currentSegmentWeight + ) + + $numberOfItemsArchived = 0 + $fileMode = [System.IO.FileMode]::Create + $result = Test-Path -LiteralPath $DestinationPath -PathType Leaf + if ($result -eq $true) + { + $fileMode = [System.IO.FileMode]::Open + } + + Add-CompressionAssemblies + + try + { + # At this point we are sure that the archive file has write access. + $archiveFileStreamArgs = @($destinationPath, $fileMode) + $archiveFileStream = New-Object -TypeName System.IO.FileStream -ArgumentList $archiveFileStreamArgs + + $zipArchiveArgs = @($archiveFileStream, [System.IO.Compression.ZipArchiveMode]::Update, $false) + $zipArchive = New-Object -TypeName System.IO.Compression.ZipArchive -ArgumentList $zipArchiveArgs + + $currentEntryCount = 0 + $progressBarStatus = ($LocalizedData.CompressProgressBarText -f $destinationPath) + $bufferSize = 4kb + $buffer = New-Object Byte[] $bufferSize + + foreach ($currentFilePath in $sourcePaths) + { + if ($modifiedSourceDirFullName -ne $null -and $modifiedSourceDirFullName.Length -gt 0) + { + $index = $currentFilePath.IndexOf($modifiedSourceDirFullName, [System.StringComparison]::OrdinalIgnoreCase) + $currentFilePathSubString = $currentFilePath.Substring($index, $modifiedSourceDirFullName.Length) + $relativeFilePath = $currentFilePath.Replace($currentFilePathSubString, "").Trim() + } + else + { + $relativeFilePath = [System.IO.Path]::GetFileName($currentFilePath) + } + + # Update mode is selected. + # Check to see if archive file already contains one or more zip files in it. + if ($isUpdateMode -eq $true -and $zipArchive.Entries.Count -gt 0) + { + $entryToBeUpdated = $null + + # Check if the file already exists in the archive file. + # If so replace it with new file from the input source. + # If the file does not exist in the archive file then default to + # create mode and create the entry in the archive file. + + foreach ($currentArchiveEntry in $zipArchive.Entries) + { + if ($currentArchiveEntry.FullName -eq $relativeFilePath) + { + $entryToBeUpdated = $currentArchiveEntry + break + } + } + + if ($entryToBeUpdated -ne $null) + { + $addItemtoArchiveFileMessage = ($LocalizedData.AddItemtoArchiveFile -f $currentFilePath) + $entryToBeUpdated.Delete() + } + } + + $compression = CompressionLevelMapper $compressionLevel + + # If a directory needs to be added to an archive file, + # by convention the .Net API's expect the path of the diretcory + # to end with '\' to detect the path as an directory. + if (!$relativeFilePath.EndsWith("\", [StringComparison]::OrdinalIgnoreCase)) + { + try + { + try + { + $currentFileStream = [System.IO.File]::Open($currentFilePath, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read) + } + catch + { + # Failed to access the file. Write a non terminating error to the pipeline + # and move on with the remaining files. + $exception = $_.Exception + if ($null -ne $_.Exception -and + $null -ne $_.Exception.InnerException) + { + $exception = $_.Exception.InnerException + } + $errorRecord = CreateErrorRecordHelper "CompressArchiveUnauthorizedAccessError" $null ([System.Management.Automation.ErrorCategory]::PermissionDenied) $exception $currentFilePath + Write-Error -ErrorRecord $errorRecord + } + + if ($null -ne $currentFileStream) + { + $srcStream = New-Object System.IO.BinaryReader $currentFileStream + + $currentArchiveEntry = $zipArchive.CreateEntry($relativeFilePath, $compression) + + # Updating the File Creation time so that the same timestamp would be retained after expanding the compressed file. + # At this point we are sure that Get-ChildItem would succeed. + $currentArchiveEntry.LastWriteTime = (Get-Item -LiteralPath $currentFilePath).LastWriteTime + + $destStream = New-Object System.IO.BinaryWriter $currentArchiveEntry.Open() + + while ($numberOfBytesRead = $srcStream.Read($buffer, 0, $bufferSize)) + { + $destStream.Write($buffer, 0, $numberOfBytesRead) + $destStream.Flush() + } + + $numberOfItemsArchived += 1 + $addItemtoArchiveFileMessage = ($LocalizedData.AddItemtoArchiveFile -f $currentFilePath) + } + } + finally + { + If ($null -ne $currentFileStream) + { + $currentFileStream.Dispose() + } + If ($null -ne $srcStream) + { + $srcStream.Dispose() + } + If ($null -ne $destStream) + { + $destStream.Dispose() + } + } + } + else + { + $currentArchiveEntry = $zipArchive.CreateEntry("$relativeFilePath", $compression) + $numberOfItemsArchived += 1 + $addItemtoArchiveFileMessage = ($LocalizedData.AddItemtoArchiveFile -f $currentFilePath) + } + + if ($null -ne $addItemtoArchiveFileMessage) + { + Write-Verbose $addItemtoArchiveFileMessage + } + + $currentEntryCount += 1 + ProgressBarHelper "Compress-Archive" $progressBarStatus $previousSegmentWeight $currentSegmentWeight $sourcePaths.Count $currentEntryCount + } + } + finally + { + If ($null -ne $zipArchive) + { + $zipArchive.Dispose() + } + + If ($null -ne $archiveFileStream) + { + $archiveFileStream.Dispose() + } + + # Complete writing progress. + Write-Progress -Activity "Compress-Archive" -Completed + } + + return $numberOfItemsArchived + } + +<############################################################################################ +# ValidateArchivePathHelper: This is a helper function used to validate the archive file +# path & its file format. The only supported archive file format is .zip +############################################################################################> + function ValidateArchivePathHelper + { + param + ( + [string] + $archiveFile + ) + + if ([System.IO.File]::Exists($archiveFile)) + { + $extension = [system.IO.Path]::GetExtension($archiveFile) + + # Invalid file extension is specifed for the zip file. + if ($extension -ne $zipFileExtension) + { + $errorMessage = ($LocalizedData.InvalidZipFileExtensionError -f $extension, $zipFileExtension) + ThrowTerminatingErrorHelper "NotSupportedArchiveFileExtension" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidArgument) $extension + } + } + else + { + $errorMessage = ($LocalizedData.PathNotFoundError -f $archiveFile) + ThrowTerminatingErrorHelper "PathNotFound" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidArgument) $archiveFile + } + } + +<############################################################################################ +# ExpandArchiveHelper: This is a helper function used to expand the archive file contents +# to the specified directory. +############################################################################################> + function ExpandArchiveHelper + { + param + ( + [string] + $archiveFile, + + [string] + $expandedDir, + + [ref] + $expandedItems, + + [boolean] + $force, + + [boolean] + $isVerbose, + + [boolean] + $isConfirm + ) + + Add-CompressionAssemblies + + try + { + # The existance of archive file has already been validated by ValidateArchivePathHelper + # before calling this helper function. + $archiveFileStreamArgs = @($archiveFile, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read) + $archiveFileStream = New-Object -TypeName System.IO.FileStream -ArgumentList $archiveFileStreamArgs + + $zipArchiveArgs = @($archiveFileStream, [System.IO.Compression.ZipArchiveMode]::Read, $false) + $zipArchive = New-Object -TypeName System.IO.Compression.ZipArchive -ArgumentList $zipArchiveArgs + + if ($zipArchive.Entries.Count -eq 0) + { + $archiveFileIsEmpty = ($LocalizedData.ArchiveFileIsEmpty -f $archiveFile) + Write-Verbose $archiveFileIsEmpty + return + } + + $currentEntryCount = 0 + $progressBarStatus = ($LocalizedData.ExpandProgressBarText -f $archiveFile) + + # The archive entries can either be empty directories or files. + foreach ($currentArchiveEntry in $zipArchive.Entries) + { + $currentArchiveEntryPath = Join-Path -Path $expandedDir -ChildPath $currentArchiveEntry.FullName + $extension = [system.IO.Path]::GetExtension($currentArchiveEntryPath) + + # The current archive entry is an empty directory + # The FullName of the Archive Entry representing a directory would end with a trailing '\'. + if ($extension -eq [string]::Empty -and + $currentArchiveEntryPath.EndsWith("\", [StringComparison]::OrdinalIgnoreCase)) + { + $pathExists = Test-Path -LiteralPath $currentArchiveEntryPath + + # The current archive entry expects an empty directory. + # Check if the existing directory is empty. If its not empty + # then it means that user has added this directory by other means. + if ($pathExists -eq $false) + { + New-Item $currentArchiveEntryPath -ItemType Directory -Confirm:$isConfirm | Out-Null + + if (Test-Path -LiteralPath $currentArchiveEntryPath -PathType Container) + { + $addEmptyDirectorytoExpandedPathMessage = ($LocalizedData.AddItemtoArchiveFile -f $currentArchiveEntryPath) + Write-Verbose $addEmptyDirectorytoExpandedPathMessage + + $expandedItems.Value += $currentArchiveEntryPath + } + } + } + else + { + try + { + $currentArchiveEntryFileInfo = New-Object -TypeName System.IO.FileInfo -ArgumentList $currentArchiveEntryPath + $parentDirExists = Test-Path -LiteralPath $currentArchiveEntryFileInfo.DirectoryName -PathType Container + + # If the Parent directory of the current entry in the archive file does not exist, then create it. + if ($parentDirExists -eq $false) + { + New-Item $currentArchiveEntryFileInfo.DirectoryName -ItemType Directory -Confirm:$isConfirm | Out-Null + + if (!(Test-Path -LiteralPath $currentArchiveEntryFileInfo.DirectoryName -PathType Container)) + { + # The directory referred by $currentArchiveEntryFileInfo.DirectoryName was not successfully created. + # This could be because the user has specified -Confirm paramter when Expand-Archive was invoked + # and authorization was not provided when confirmation was prompted. In such a scenario, + # we skip the current file in the archive and continue with the remaining archive file contents. + Continue + } + + $expandedItems.Value += $currentArchiveEntryFileInfo.DirectoryName + } + + $hasNonTerminatingError = $false + + # Check if the file in to which the current archive entry contents + # would be expanded already exists. + if ($currentArchiveEntryFileInfo.Exists) + { + if ($force) + { + Remove-Item -LiteralPath $currentArchiveEntryFileInfo.FullName -Force -ErrorVariable ev -Verbose:$isVerbose -Confirm:$isConfirm + if ($ev -ne $null) + { + $hasNonTerminatingError = $true + } + + if (Test-Path -LiteralPath $currentArchiveEntryFileInfo.FullName -PathType Leaf) + { + # The file referred by $currentArchiveEntryFileInfo.FullName was not successfully removed. + # This could be because the user has specified -Confirm paramter when Expand-Archive was invoked + # and authorization was not provided when confirmation was prompted. In such a scenario, + # we skip the current file in the archive and continue with the remaining archive file contents. + Continue + } + } + else + { + # Write non-terminating error to the pipeline. + $errorMessage = ($LocalizedData.FileExistsError -f $currentArchiveEntryFileInfo.FullName, $archiveFile, $currentArchiveEntryFileInfo.FullName, $currentArchiveEntryFileInfo.FullName) + $errorRecord = CreateErrorRecordHelper "ExpandArchiveFileExists" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidOperation) $null $currentArchiveEntryFileInfo.FullName + Write-Error -ErrorRecord $errorRecord + $hasNonTerminatingError = $true + } + } + + if (!$hasNonTerminatingError) + { + [System.IO.Compression.ZipFileExtensions]::ExtractToFile($currentArchiveEntry, $currentArchiveEntryPath, $false) + + # Add the expanded file path to the $expandedItems array, + # to keep track of all the expanded files created while expanding the archive file. + # If user enters CTRL + C then at that point of time, all these expanded files + # would be deleted as part of the clean up process. + $expandedItems.Value += $currentArchiveEntryPath + + $addFiletoExpandedPathMessage = ($LocalizedData.CreateFileAtExpandedPath -f $currentArchiveEntryPath) + Write-Verbose $addFiletoExpandedPathMessage + } + } + finally + { + If ($null -ne $destStream) + { + $destStream.Dispose() + } + + If ($null -ne $srcStream) + { + $srcStream.Dispose() + } + } + } + + $currentEntryCount += 1 + # $currentSegmentWeight is Set to 100 giving equal weightage to each file that is getting expanded. + # $previousSegmentWeight is set to 0 as there are no prior segments. + $previousSegmentWeight = 0 + $currentSegmentWeight = 100 + ProgressBarHelper "Expand-Archive" $progressBarStatus $previousSegmentWeight $currentSegmentWeight $zipArchive.Entries.Count $currentEntryCount + } + } + finally + { + If ($null -ne $zipArchive) + { + $zipArchive.Dispose() + } + + If ($null -ne $archiveFileStream) + { + $archiveFileStream.Dispose() + } + + # Complete writing progress. + Write-Progress -Activity "Expand-Archive" -Completed + } + } + +<############################################################################################ +# ProgressBarHelper: This is a helper function used to display progress message. +# This function is used by both Compress-Archive & Expand-Archive to display archive file +# creation/expansion progress. +############################################################################################> + function ProgressBarHelper + { + param + ( + [string] + $cmdletName, + + [string] + $status, + + [double] + $previousSegmentWeight, + + [double] + $currentSegmentWeight, + + [int] + $totalNumberofEntries, + + [int] + $currentEntryCount + ) + + if ($currentEntryCount -gt 0 -and + $totalNumberofEntries -gt 0 -and + $previousSegmentWeight -ge 0 -and + $currentSegmentWeight -gt 0) + { + $entryDefaultWeight = $currentSegmentWeight/[double]$totalNumberofEntries + + $percentComplete = $previousSegmentWeight + ($entryDefaultWeight * $currentEntryCount) + Write-Progress -Activity $cmdletName -Status $status -PercentComplete $percentComplete + } + } + +<############################################################################################ +# CSVHelper: This is a helper function used to append comma after each path specifid by +# the SourcePath array. This helper function is used to display all the user supplied paths +# in the WhatIf message. +############################################################################################> + function CSVHelper + { + param + ( + [string[]] + $sourcePath + ) + + # SourcePath has already been validated by the calling funcation. + if ($sourcePath.Count -gt 1) + { + $sourcePathInCsvFormat = "`n" + for ($currentIndex = 0; $currentIndex -lt $sourcePath.Count; $currentIndex++) + { + if ($currentIndex -eq $sourcePath.Count - 1) + { + $sourcePathInCsvFormat += $sourcePath[$currentIndex] + } + else + { + $sourcePathInCsvFormat += $sourcePath[$currentIndex] + "`n" + } + } + } + else + { + $sourcePathInCsvFormat = $sourcePath + } + + return $sourcePathInCsvFormat + } + +<############################################################################################ +# ThrowTerminatingErrorHelper: This is a helper function used to throw terminating error. +############################################################################################> + function ThrowTerminatingErrorHelper + { + param + ( + [string] + $errorId, + + [string] + $errorMessage, + + [System.Management.Automation.ErrorCategory] + $errorCategory, + + [object] + $targetObject, + + [Exception] + $innerException + ) + + if ($innerException -eq $null) + { + $exception = New-object System.IO.IOException $errorMessage + } + else + { + $exception = New-Object System.IO.IOException $errorMessage, $innerException + } + + $exception = New-Object System.IO.IOException $errorMessage + $errorRecord = New-Object System.Management.Automation.ErrorRecord $exception, $errorId, $errorCategory, $targetObject + $PSCmdlet.ThrowTerminatingError($errorRecord) + } + +<############################################################################################ +# CreateErrorRecordHelper: This is a helper function used to create an ErrorRecord +############################################################################################> + function CreateErrorRecordHelper + { + param + ( + [string] + $errorId, + + [string] + $errorMessage, + + [System.Management.Automation.ErrorCategory] + $errorCategory, + + [Exception] + $exception, + + [object] + $targetObject + ) + + if ($null -eq $exception) + { + $exception = New-Object System.IO.IOException $errorMessage + } + + $errorRecord = New-Object System.Management.Automation.ErrorRecord $exception, $errorId, $errorCategory, $targetObject + return $errorRecord + } + #endregion Utility Functions + + $inputPaths = @() + $destinationParentDir = [system.IO.Path]::GetDirectoryName($DestinationPath) + if ($null -eq $destinationParentDir) + { + $errorMessage = ($LocalizedData.InvalidDestinationPath -f $DestinationPath) + ThrowTerminatingErrorHelper "InvalidArchiveFilePath" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidArgument) $DestinationPath + } + + if ($destinationParentDir -eq [string]::Empty) + { + $destinationParentDir = '.' + } + + $achiveFileName = [system.IO.Path]::GetFileName($DestinationPath) + $destinationParentDir = GetResolvedPathHelper $destinationParentDir $false $PSCmdlet + + if ($destinationParentDir.Count -gt 1) + { + $errorMessage = ($LocalizedData.InvalidArchiveFilePathError -f $DestinationPath, "DestinationPath", "DestinationPath") + ThrowTerminatingErrorHelper "InvalidArchiveFilePath" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidArgument) $DestinationPath + } + + IsValidFileSystemPath $destinationParentDir | Out-Null + $DestinationPath = Join-Path -Path $destinationParentDir -ChildPath $achiveFileName + + # GetExtension API does not validate for the actual existance of the path. + $extension = [system.IO.Path]::GetExtension($DestinationPath) + + # If user does not specify .Zip extension, we append it. + If ($extension -eq [string]::Empty) + { + $DestinationPathWithOutExtension = $DestinationPath + $DestinationPath = $DestinationPathWithOutExtension + $zipFileExtension + $appendArchiveFileExtensionMessage = ($LocalizedData.AppendArchiveFileExtensionMessage -f $DestinationPathWithOutExtension, $DestinationPath) + Write-Verbose $appendArchiveFileExtensionMessage + } + else + { + # Invalid file extension is specified for the zip file to be created. + if ($extension -ne $zipFileExtension) + { + $errorMessage = ($LocalizedData.InvalidZipFileExtensionError -f $extension, $zipFileExtension) + ThrowTerminatingErrorHelper "NotSupportedArchiveFileExtension" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidArgument) $extension + } + } + + $archiveFileExist = Test-Path -LiteralPath $DestinationPath -PathType Leaf + + if ($archiveFileExist -and ($Update -eq $false -and $Force -eq $false)) + { + $errorMessage = ($LocalizedData.ZipFileExistError -f $DestinationPath) + ThrowTerminatingErrorHelper "ArchiveFileExists" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidArgument) $DestinationPath + } + + # If archive file already exists and if -Update is specified, then we check to see + # if we have write access permission to update the existing archive file. + if ($archiveFileExist -and $Update -eq $true) + { + $item = Get-Item -Path $DestinationPath + if ($item.Attributes.ToString().Contains("ReadOnly")) + { + $errorMessage = ($LocalizedData.ArchiveFileIsReadOnly -f $DestinationPath) + ThrowTerminatingErrorHelper "ArchiveFileIsReadOnly" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidOperation) $DestinationPath + } + } + + $isWhatIf = $psboundparameters.ContainsKey("WhatIf") + if (!$isWhatIf) + { + $preparingToCompressVerboseMessage = ($LocalizedData.PreparingToCompressVerboseMessage) + Write-Verbose $preparingToCompressVerboseMessage + + $progressBarStatus = ($LocalizedData.CompressProgressBarText -f $DestinationPath) + ProgressBarHelper "Compress-Archive" $progressBarStatus 0 100 100 1 + } + } + PROCESS + { + if ($PsCmdlet.ParameterSetName -eq "Path" -or + $PsCmdlet.ParameterSetName -eq "PathWithForce" -or + $PsCmdlet.ParameterSetName -eq "PathWithUpdate") + { + $inputPaths += $Path + } + + if ($PsCmdlet.ParameterSetName -eq "LiteralPath" -or + $PsCmdlet.ParameterSetName -eq "LiteralPathWithForce" -or + $PsCmdlet.ParameterSetName -eq "LiteralPathWithUpdate") + { + $inputPaths += $LiteralPath + } + } + END + { + # If archive file already exists and if -Force is specified, we delete the + # existing artchive file and create a brand new one. + if (($PsCmdlet.ParameterSetName -eq "PathWithForce" -or + $PsCmdlet.ParameterSetName -eq "LiteralPathWithForce") -and $archiveFileExist) + { + Remove-Item -Path $DestinationPath -Force -ErrorAction Stop + } + + # Validate Source Path depeding on parameter set being used. + # The specified source path conatins one or more files or directories that needs + # to be compressed. + $isLiteralPathUsed = $false + if ($PsCmdlet.ParameterSetName -eq "LiteralPath" -or + $PsCmdlet.ParameterSetName -eq "LiteralPathWithForce" -or + $PsCmdlet.ParameterSetName -eq "LiteralPathWithUpdate") + { + $isLiteralPathUsed = $true + } + + ValidateDuplicateFileSystemPath $PsCmdlet.ParameterSetName $inputPaths + $resolvedPaths = GetResolvedPathHelper $inputPaths $isLiteralPathUsed $PSCmdlet + IsValidFileSystemPath $resolvedPaths | Out-Null + + $sourcePath = $resolvedPaths; + + # CSVHelper: This is a helper function used to append comma after each path specifid by + # the $sourcePath array. The comma saperated paths are displayed in the -WhatIf message. + $sourcePathInCsvFormat = CSVHelper $sourcePath + if ($pscmdlet.ShouldProcess($sourcePathInCsvFormat)) + { + try + { + # StopProcessing is not avaliable in Script cmdlets. However the pipleline execution + # is terminated when ever 'CTRL + C' is entered by user to terminate the cmdlet execution. + # The finally block is executed whenever pipleline is terminated. + # $isArchiveFileProcessingComplete variable is used to track if 'CTRL + C' is entered by the + # user. + $isArchiveFileProcessingComplete = $false + + $numberOfItemsArchived = CompressArchiveHelper $sourcePath $DestinationPath $CompressionLevel $Update + + $isArchiveFileProcessingComplete = $true + } + finally + { + # The $isArchiveFileProcessingComplete would be set to $false if user has typed 'CTRL + C' to + # terminate the cmdlet execution or if an unhandled exception is thrown. + # $numberOfItemsArchived contains the count of number of files or directories add to the archive file. + # If the newly created archive file is empty then we delete it as its not usable. + if (($isArchiveFileProcessingComplete -eq $false) -or + ($numberOfItemsArchived -eq 0)) + { + $DeleteArchiveFileMessage = ($LocalizedData.DeleteArchiveFile -f $DestinationPath) + Write-Verbose $DeleteArchiveFileMessage + + # delete the partial archive file created. + if (Test-Path $DestinationPath) + { + Remove-Item -LiteralPath $DestinationPath -Force -Recurse -ErrorAction SilentlyContinue + } + } + } + } + } +} + +function Expand-Archive +{ + <# + .SYNOPSIS + Extracts files from a specified archive (zipped) file. + + .DESCRIPTION + The Expand-Archive cmdlet extracts files from a specified zipped archive file to a specified destination folder. An archive file allows multiple files to be packaged, and optionally compressed, into a single zipped file for easier distribution and storage. + + .PARAMETER Path + Specifies the path to the archive file. + + .PARAMETER LiteralPath + Specifies the path to an archive file. Unlike the Path parameter, the value of LiteralPath is used exactly as it is typed. Wildcard characters are not supported. If the path includes escape characters, enclose each escape character in single quotation marks, to instruct Windows PowerShell not to interpret any characters as escape sequences. + + .PARAMETER DestinationPath + Specifies the path to the folder in which you want the command to save extracted files. Enter the path to a folder, but do not specify a file name or file name extension. This parameter is required. + + .PARAMETER Force + Forces the command to run without asking for user confirmation. + + .PARAMETER Confirm + Prompts you for confirmation before running the cmdlet. + + .PARAMETER WhatIf + Shows what would happen if the cmdlet runs. The cmdlet is not run. + + .EXAMPLE + Example 1: Extract the contents of an archive + + PS C:\>Expand-Archive -LiteralPath C:\Archives\Draft.Zip -DestinationPath C:\Reference + + This command extracts the contents of an existing archive file, Draft.zip, into the folder specified by the DestinationPath parameter, C:\Reference. + + .EXAMPLE + Example 2: Extract the contents of an archive in the current folder + + PS C:\>Expand-Archive -Path Draft.Zip -DestinationPath C:\Reference + + This command extracts the contents of an existing archive file in the current folder, Draft.zip, into the folder specified by the DestinationPath parameter, C:\Reference. + #> + [CmdletBinding( + DefaultParameterSetName = "Path", + SupportsShouldProcess = $true, + HelpUri = "http://go.microsoft.com/fwlink/?LinkID=393253")] + param + ( + [parameter ( + mandatory = $true, + Position = 0, + ParameterSetName = "Path", + ValueFromPipeline = $true, + ValueFromPipelineByPropertyName = $true)] + [ValidateNotNullOrEmpty()] + [string] + $Path, + + [parameter ( + mandatory = $true, + ParameterSetName = "LiteralPath", + ValueFromPipelineByPropertyName = $true)] + [ValidateNotNullOrEmpty()] + [Alias("PSPath")] + [string] + $LiteralPath, + + [parameter (mandatory = $false, + Position = 1, + ValueFromPipeline = $false, + ValueFromPipelineByPropertyName = $false)] + [ValidateNotNullOrEmpty()] + [string] + $DestinationPath, + + [parameter (mandatory = $false, + ValueFromPipeline = $false, + ValueFromPipelineByPropertyName = $false)] + [switch] + $Force + ) + + BEGIN + { + Add-Type -AssemblyName System.IO.Compression -ErrorAction Ignore + Add-Type -AssemblyName System.IO.Compression.FileSystem -ErrorAction Ignore + + $zipFileExtension = ".zip" + + $LocalizedData = ConvertFrom-StringData @' +PathNotFoundError=The path '{0}' either does not exist or is not a valid file system path. +ExpandArchiveInValidDestinationPath=The path '{0}' is not a valid file system directory path. +InvalidZipFileExtensionError={0} is not a supported archive file format. {1} is the only supported archive file format. +ArchiveFileIsReadOnly=The attributes of the archive file {0} is set to 'ReadOnly' hence it cannot be updated. If you intend to update the existing archive file, remove the 'ReadOnly' attribute on the archive file else use -Force parameter to override and create a new archive file. +ZipFileExistError=The archive file {0} already exists. Use the -Update parameter to update the existing archive file or use the -Force parameter to overwrite the existing archive file. +DuplicatePathFoundError=The input to {0} parameter contains a duplicate path '{1}'. Provide a unique set of paths as input to {2} parameter. +ArchiveFileIsEmpty=The archive file {0} is empty. +CompressProgressBarText=The archive file '{0}' creation is in progress... +ExpandProgressBarText=The archive file '{0}' expansion is in progress... +AppendArchiveFileExtensionMessage=The archive file path '{0}' supplied to the DestinationPath patameter does not include .zip extension. Hence .zip is appended to the supplied DestinationPath path and the archive file would be created at '{1}'. +AddItemtoArchiveFile=Adding '{0}'. +CreateFileAtExpandedPath=Created '{0}'. +InvalidArchiveFilePathError=The archive file path '{0}' specified as input to the {1} parameter is resolving to multiple file system paths. Provide a unique path to the {2} parameter where the archive file has to be created. +InvalidExpandedDirPathError=The directory path '{0}' specified as input to the DestinationPath parameter is resolving to multiple file system paths. Provide a unique path to the Destination parameter where the archive file contents have to be expanded. +FileExistsError=Failed to create file '{0}' while expanding the archive file '{1}' contents as the file '{2}' already exists. Use the -Force parameter if you want to overwrite the existing directory '{3}' contents when expanding the archive file. +DeleteArchiveFile=The partially created archive file '{0}' is deleted as it is not usable. +InvalidDestinationPath=The destination path '{0}' does not contain a valid archive file name. +PreparingToCompressVerboseMessage=Preparing to compress... +PreparingToExpandVerboseMessage=Preparing to expand... +'@ + + #region Utility Functions + function GetResolvedPathHelper + { + param + ( + [string[]] + $path, + + [boolean] + $isLiteralPath, + + [System.Management.Automation.PSCmdlet] + $callerPSCmdlet + ) + + $resolvedPaths = @() + + # null and empty check are are already done on Path parameter at the cmdlet layer. + foreach ($currentPath in $path) + { + try + { + if ($isLiteralPath) + { + $currentResolvedPaths = Resolve-Path -LiteralPath $currentPath -ErrorAction Stop + } + else + { + $currentResolvedPaths = Resolve-Path -Path $currentPath -ErrorAction Stop + } + } + catch + { + $errorMessage = ($LocalizedData.PathNotFoundError -f $currentPath) + $exception = New-Object System.InvalidOperationException $errorMessage, $_.Exception + $errorRecord = CreateErrorRecordHelper "ArchiveCmdletPathNotFound" $null ([System.Management.Automation.ErrorCategory]::InvalidArgument) $exception $currentPath + $callerPSCmdlet.ThrowTerminatingError($errorRecord) + } + + foreach ($currentResolvedPath in $currentResolvedPaths) + { + $resolvedPaths += $currentResolvedPath.ProviderPath + } + } + + $resolvedPaths + } + + function Add-CompressionAssemblies + { + + if ($PSEdition -eq "Desktop") + { + Add-Type -AssemblyName System.IO.Compression + Add-Type -AssemblyName System.IO.Compression.FileSystem + } + } + + function IsValidFileSystemPath + { + param + ( + [string[]] + $path + ) + + $result = $true; + + # null and empty check are are already done on Path parameter at the cmdlet layer. + foreach ($currentPath in $path) + { + if (!([System.IO.File]::Exists($currentPath) -or [System.IO.Directory]::Exists($currentPath))) + { + $errorMessage = ($LocalizedData.PathNotFoundError -f $currentPath) + ThrowTerminatingErrorHelper "PathNotFound" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidArgument) $currentPath + } + } + + return $result; + } + + + function ValidateDuplicateFileSystemPath + { + param + ( + [string] + $inputParameter, + + [string[]] + $path + ) + + $uniqueInputPaths = @() + + # null and empty check are are already done on Path parameter at the cmdlet layer. + foreach ($currentPath in $path) + { + $currentInputPath = $currentPath.ToUpper() + if ($uniqueInputPaths.Contains($currentInputPath)) + { + $errorMessage = ($LocalizedData.DuplicatePathFoundError -f $inputParameter, $currentPath, $inputParameter) + ThrowTerminatingErrorHelper "DuplicatePathFound" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidArgument) $currentPath + } + else + { + $uniqueInputPaths += $currentInputPath + } + } + } + + function CompressionLevelMapper + { + param + ( + [string] + $compressionLevel + ) + + $compressionLevelFormat = [System.IO.Compression.CompressionLevel]::Optimal + + # CompressionLevel format is already validated at the cmdlet layer. + switch ($compressionLevel.ToString()) + { + "Fastest" + { + $compressionLevelFormat = [System.IO.Compression.CompressionLevel]::Fastest + } + "NoCompression" + { + $compressionLevelFormat = [System.IO.Compression.CompressionLevel]::NoCompression + } + } + + return $compressionLevelFormat + } + + function CompressArchiveHelper + { + param + ( + [string[]] + $sourcePath, + + [string] + $destinationPath, + + [string] + $compressionLevel, + + [bool] + $isUpdateMode + ) + + $numberOfItemsArchived = 0 + $sourceFilePaths = @() + $sourceDirPaths = @() + + foreach ($currentPath in $sourcePath) + { + $result = Test-Path -LiteralPath $currentPath -PathType Leaf + if ($result -eq $true) + { + $sourceFilePaths += $currentPath + } + else + { + $sourceDirPaths += $currentPath + } + } + + # The Soure Path contains one or more directory (this directory can have files under it) and no files to be compressed. + if ($sourceFilePaths.Count -eq 0 -and $sourceDirPaths.Count -gt 0) + { + $currentSegmentWeight = 100/[double]$sourceDirPaths.Count + $previousSegmentWeight = 0 + foreach ($currentSourceDirPath in $sourceDirPaths) + { + $count = CompressSingleDirHelper $currentSourceDirPath $destinationPath $compressionLevel $true $isUpdateMode $previousSegmentWeight $currentSegmentWeight + $numberOfItemsArchived += $count + $previousSegmentWeight += $currentSegmentWeight + } + } + + # The Soure Path contains only files to be compressed. + elseIf ($sourceFilePaths.Count -gt 0 -and $sourceDirPaths.Count -eq 0) + { + # $previousSegmentWeight is equal to 0 as there are no prior segments. + # $currentSegmentWeight is set to 100 as all files have equal weightage. + $previousSegmentWeight = 0 + $currentSegmentWeight = 100 + + $numberOfItemsArchived = CompressFilesHelper $sourceFilePaths $destinationPath $compressionLevel $isUpdateMode $previousSegmentWeight $currentSegmentWeight + } + # The Soure Path contains one or more files and one or more directories (this directory can have files under it) to be compressed. + elseif ($sourceFilePaths.Count -gt 0 -and $sourceDirPaths.Count -gt 0) + { + # each directory is considered as an individual segments & all the individual files are clubed in to a separate sgemnet. + $currentSegmentWeight = 100/[double]($sourceDirPaths.Count + 1) + $previousSegmentWeight = 0 + + foreach ($currentSourceDirPath in $sourceDirPaths) + { + $count = CompressSingleDirHelper $currentSourceDirPath $destinationPath $compressionLevel $true $isUpdateMode $previousSegmentWeight $currentSegmentWeight + $numberOfItemsArchived += $count + $previousSegmentWeight += $currentSegmentWeight + } + + $count = CompressFilesHelper $sourceFilePaths $destinationPath $compressionLevel $isUpdateMode $previousSegmentWeight $currentSegmentWeight + $numberOfItemsArchived += $count + } + + return $numberOfItemsArchived + } + + function CompressFilesHelper + { + param + ( + [string[]] + $sourceFilePaths, + + [string] + $destinationPath, + + [string] + $compressionLevel, + + [bool] + $isUpdateMode, + + [double] + $previousSegmentWeight, + + [double] + $currentSegmentWeight + ) + + $numberOfItemsArchived = ZipArchiveHelper $sourceFilePaths $destinationPath $compressionLevel $isUpdateMode $null $previousSegmentWeight $currentSegmentWeight + + return $numberOfItemsArchived + } + + function CompressSingleDirHelper + { + param + ( + [string] + $sourceDirPath, + + [string] + $destinationPath, + + [string] + $compressionLevel, + + [bool] + $useParentDirAsRoot, + + [bool] + $isUpdateMode, + + [double] + $previousSegmentWeight, + + [double] + $currentSegmentWeight + ) + + [System.Collections.Generic.List[System.String]]$subDirFiles = @() + + if ($useParentDirAsRoot) + { + $sourceDirInfo = New-Object -TypeName System.IO.DirectoryInfo -ArgumentList $sourceDirPath + $sourceDirFullName = $sourceDirInfo.Parent.FullName + + # If the directory is present at the drive level the DirectoryInfo.Parent include '\' example: C:\ + # On the other hand if the directory exists at a deper level then DirectoryInfo.Parent + # has just the path (without an ending '\'). example C:\source + if ($sourceDirFullName.Length -eq 3) + { + $modifiedSourceDirFullName = $sourceDirFullName + } + else + { + $modifiedSourceDirFullName = $sourceDirFullName + "\" + } + } + else + { + $sourceDirFullName = $sourceDirPath + $modifiedSourceDirFullName = $sourceDirFullName + "\" + } + + $dirContents = Get-ChildItem -LiteralPath $sourceDirPath -Recurse + foreach ($currentContent in $dirContents) + { + $isContainer = $currentContent -is [System.IO.DirectoryInfo] + if (!$isContainer) + { + $subDirFiles.Add($currentContent.FullName) + } + else + { + # The currentContent points to a directory. + # We need to check if the directory is an empty directory, if so such a + # directory has to be explictly added to the archive file. + # if there are no files in the directory the GetFiles() API returns an empty array. + $files = $currentContent.GetFiles() + if ($files.Count -eq 0) + { + $subDirFiles.Add($currentContent.FullName + "\") + } + } + } + + $numberOfItemsArchived = ZipArchiveHelper $subDirFiles.ToArray() $destinationPath $compressionLevel $isUpdateMode $modifiedSourceDirFullName $previousSegmentWeight $currentSegmentWeight + + return $numberOfItemsArchived + } + + function ZipArchiveHelper + { + param + ( + [System.Collections.Generic.List[System.String]] + $sourcePaths, + + [string] + $destinationPath, + + [string] + $compressionLevel, + + [bool] + $isUpdateMode, + + [string] + $modifiedSourceDirFullName, + + [double] + $previousSegmentWeight, + + [double] + $currentSegmentWeight + ) + + $numberOfItemsArchived = 0 + $fileMode = [System.IO.FileMode]::Create + $result = Test-Path -LiteralPath $DestinationPath -PathType Leaf + if ($result -eq $true) + { + $fileMode = [System.IO.FileMode]::Open + } + + Add-CompressionAssemblies + + try + { + # At this point we are sure that the archive file has write access. + $archiveFileStreamArgs = @($destinationPath, $fileMode) + $archiveFileStream = New-Object -TypeName System.IO.FileStream -ArgumentList $archiveFileStreamArgs + + $zipArchiveArgs = @($archiveFileStream, [System.IO.Compression.ZipArchiveMode]::Update, $false) + $zipArchive = New-Object -TypeName System.IO.Compression.ZipArchive -ArgumentList $zipArchiveArgs + + $currentEntryCount = 0 + $progressBarStatus = ($LocalizedData.CompressProgressBarText -f $destinationPath) + $bufferSize = 4kb + $buffer = New-Object Byte[] $bufferSize + + foreach ($currentFilePath in $sourcePaths) + { + if ($modifiedSourceDirFullName -ne $null -and $modifiedSourceDirFullName.Length -gt 0) + { + $index = $currentFilePath.IndexOf($modifiedSourceDirFullName, [System.StringComparison]::OrdinalIgnoreCase) + $currentFilePathSubString = $currentFilePath.Substring($index, $modifiedSourceDirFullName.Length) + $relativeFilePath = $currentFilePath.Replace($currentFilePathSubString, "").Trim() + } + else + { + $relativeFilePath = [System.IO.Path]::GetFileName($currentFilePath) + } + + # Update mode is selected. + # Check to see if archive file already contains one or more zip files in it. + if ($isUpdateMode -eq $true -and $zipArchive.Entries.Count -gt 0) + { + $entryToBeUpdated = $null + + # Check if the file already exists in the archive file. + # If so replace it with new file from the input source. + # If the file does not exist in the archive file then default to + # create mode and create the entry in the archive file. + + foreach ($currentArchiveEntry in $zipArchive.Entries) + { + if ($currentArchiveEntry.FullName -eq $relativeFilePath) + { + $entryToBeUpdated = $currentArchiveEntry + break + } + } + + if ($entryToBeUpdated -ne $null) + { + $addItemtoArchiveFileMessage = ($LocalizedData.AddItemtoArchiveFile -f $currentFilePath) + $entryToBeUpdated.Delete() + } + } + + $compression = CompressionLevelMapper $compressionLevel + + # If a directory needs to be added to an archive file, + # by convention the .Net API's expect the path of the diretcory + # to end with '\' to detect the path as an directory. + if (!$relativeFilePath.EndsWith("\", [StringComparison]::OrdinalIgnoreCase)) + { + try + { + try + { + $currentFileStream = [System.IO.File]::Open($currentFilePath, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read) + } + catch + { + # Failed to access the file. Write a non terminating error to the pipeline + # and move on with the remaining files. + $exception = $_.Exception + if ($null -ne $_.Exception -and + $null -ne $_.Exception.InnerException) + { + $exception = $_.Exception.InnerException + } + $errorRecord = CreateErrorRecordHelper "CompressArchiveUnauthorizedAccessError" $null ([System.Management.Automation.ErrorCategory]::PermissionDenied) $exception $currentFilePath + Write-Error -ErrorRecord $errorRecord + } + + if ($null -ne $currentFileStream) + { + $srcStream = New-Object System.IO.BinaryReader $currentFileStream + + $currentArchiveEntry = $zipArchive.CreateEntry($relativeFilePath, $compression) + + # Updating the File Creation time so that the same timestamp would be retained after expanding the compressed file. + # At this point we are sure that Get-ChildItem would succeed. + $currentArchiveEntry.LastWriteTime = (Get-Item -LiteralPath $currentFilePath).LastWriteTime + + $destStream = New-Object System.IO.BinaryWriter $currentArchiveEntry.Open() + + while ($numberOfBytesRead = $srcStream.Read($buffer, 0, $bufferSize)) + { + $destStream.Write($buffer, 0, $numberOfBytesRead) + $destStream.Flush() + } + + $numberOfItemsArchived += 1 + $addItemtoArchiveFileMessage = ($LocalizedData.AddItemtoArchiveFile -f $currentFilePath) + } + } + finally + { + If ($null -ne $currentFileStream) + { + $currentFileStream.Dispose() + } + If ($null -ne $srcStream) + { + $srcStream.Dispose() + } + If ($null -ne $destStream) + { + $destStream.Dispose() + } + } + } + else + { + $currentArchiveEntry = $zipArchive.CreateEntry("$relativeFilePath", $compression) + $numberOfItemsArchived += 1 + $addItemtoArchiveFileMessage = ($LocalizedData.AddItemtoArchiveFile -f $currentFilePath) + } + + if ($null -ne $addItemtoArchiveFileMessage) + { + Write-Verbose $addItemtoArchiveFileMessage + } + + $currentEntryCount += 1 + ProgressBarHelper "Compress-Archive" $progressBarStatus $previousSegmentWeight $currentSegmentWeight $sourcePaths.Count $currentEntryCount + } + } + finally + { + If ($null -ne $zipArchive) + { + $zipArchive.Dispose() + } + + If ($null -ne $archiveFileStream) + { + $archiveFileStream.Dispose() + } + + # Complete writing progress. + Write-Progress -Activity "Compress-Archive" -Completed + } + + return $numberOfItemsArchived + } + +<############################################################################################ +# ValidateArchivePathHelper: This is a helper function used to validate the archive file +# path & its file format. The only supported archive file format is .zip +############################################################################################> + function ValidateArchivePathHelper + { + param + ( + [string] + $archiveFile + ) + + if ([System.IO.File]::Exists($archiveFile)) + { + $extension = [system.IO.Path]::GetExtension($archiveFile) + + # Invalid file extension is specifed for the zip file. + if ($extension -ne $zipFileExtension) + { + $errorMessage = ($LocalizedData.InvalidZipFileExtensionError -f $extension, $zipFileExtension) + ThrowTerminatingErrorHelper "NotSupportedArchiveFileExtension" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidArgument) $extension + } + } + else + { + $errorMessage = ($LocalizedData.PathNotFoundError -f $archiveFile) + ThrowTerminatingErrorHelper "PathNotFound" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidArgument) $archiveFile + } + } + +<############################################################################################ +# ExpandArchiveHelper: This is a helper function used to expand the archive file contents +# to the specified directory. +############################################################################################> + function ExpandArchiveHelper + { + param + ( + [string] + $archiveFile, + + [string] + $expandedDir, + + [ref] + $expandedItems, + + [boolean] + $force, + + [boolean] + $isVerbose, + + [boolean] + $isConfirm + ) + + Add-CompressionAssemblies + + try + { + # The existance of archive file has already been validated by ValidateArchivePathHelper + # before calling this helper function. + $archiveFileStreamArgs = @($archiveFile, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read) + $archiveFileStream = New-Object -TypeName System.IO.FileStream -ArgumentList $archiveFileStreamArgs + + $zipArchiveArgs = @($archiveFileStream, [System.IO.Compression.ZipArchiveMode]::Read, $false) + $zipArchive = New-Object -TypeName System.IO.Compression.ZipArchive -ArgumentList $zipArchiveArgs + + if ($zipArchive.Entries.Count -eq 0) + { + $archiveFileIsEmpty = ($LocalizedData.ArchiveFileIsEmpty -f $archiveFile) + Write-Verbose $archiveFileIsEmpty + return + } + + $currentEntryCount = 0 + $progressBarStatus = ($LocalizedData.ExpandProgressBarText -f $archiveFile) + + # The archive entries can either be empty directories or files. + foreach ($currentArchiveEntry in $zipArchive.Entries) + { + $currentArchiveEntryPath = Join-Path -Path $expandedDir -ChildPath $currentArchiveEntry.FullName + $extension = [system.IO.Path]::GetExtension($currentArchiveEntryPath) + + # The current archive entry is an empty directory + # The FullName of the Archive Entry representing a directory would end with a trailing '\'. + if ($extension -eq [string]::Empty -and + $currentArchiveEntryPath.EndsWith("\", [StringComparison]::OrdinalIgnoreCase)) + { + $pathExists = Test-Path -LiteralPath $currentArchiveEntryPath + + # The current archive entry expects an empty directory. + # Check if the existing directory is empty. If its not empty + # then it means that user has added this directory by other means. + if ($pathExists -eq $false) + { + New-Item $currentArchiveEntryPath -ItemType Directory -Confirm:$isConfirm | Out-Null + + if (Test-Path -LiteralPath $currentArchiveEntryPath -PathType Container) + { + $addEmptyDirectorytoExpandedPathMessage = ($LocalizedData.AddItemtoArchiveFile -f $currentArchiveEntryPath) + Write-Verbose $addEmptyDirectorytoExpandedPathMessage + + $expandedItems.Value += $currentArchiveEntryPath + } + } + } + else + { + try + { + $currentArchiveEntryFileInfo = New-Object -TypeName System.IO.FileInfo -ArgumentList $currentArchiveEntryPath + $parentDirExists = Test-Path -LiteralPath $currentArchiveEntryFileInfo.DirectoryName -PathType Container + + # If the Parent directory of the current entry in the archive file does not exist, then create it. + if ($parentDirExists -eq $false) + { + New-Item $currentArchiveEntryFileInfo.DirectoryName -ItemType Directory -Confirm:$isConfirm | Out-Null + + if (!(Test-Path -LiteralPath $currentArchiveEntryFileInfo.DirectoryName -PathType Container)) + { + # The directory referred by $currentArchiveEntryFileInfo.DirectoryName was not successfully created. + # This could be because the user has specified -Confirm paramter when Expand-Archive was invoked + # and authorization was not provided when confirmation was prompted. In such a scenario, + # we skip the current file in the archive and continue with the remaining archive file contents. + Continue + } + + $expandedItems.Value += $currentArchiveEntryFileInfo.DirectoryName + } + + $hasNonTerminatingError = $false + + # Check if the file in to which the current archive entry contents + # would be expanded already exists. + if ($currentArchiveEntryFileInfo.Exists) + { + if ($force) + { + Remove-Item -LiteralPath $currentArchiveEntryFileInfo.FullName -Force -ErrorVariable ev -Verbose:$isVerbose -Confirm:$isConfirm + if ($ev -ne $null) + { + $hasNonTerminatingError = $true + } + + if (Test-Path -LiteralPath $currentArchiveEntryFileInfo.FullName -PathType Leaf) + { + # The file referred by $currentArchiveEntryFileInfo.FullName was not successfully removed. + # This could be because the user has specified -Confirm paramter when Expand-Archive was invoked + # and authorization was not provided when confirmation was prompted. In such a scenario, + # we skip the current file in the archive and continue with the remaining archive file contents. + Continue + } + } + else + { + # Write non-terminating error to the pipeline. + $errorMessage = ($LocalizedData.FileExistsError -f $currentArchiveEntryFileInfo.FullName, $archiveFile, $currentArchiveEntryFileInfo.FullName, $currentArchiveEntryFileInfo.FullName) + $errorRecord = CreateErrorRecordHelper "ExpandArchiveFileExists" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidOperation) $null $currentArchiveEntryFileInfo.FullName + Write-Error -ErrorRecord $errorRecord + $hasNonTerminatingError = $true + } + } + + if (!$hasNonTerminatingError) + { + [System.IO.Compression.ZipFileExtensions]::ExtractToFile($currentArchiveEntry, $currentArchiveEntryPath, $false) + + # Add the expanded file path to the $expandedItems array, + # to keep track of all the expanded files created while expanding the archive file. + # If user enters CTRL + C then at that point of time, all these expanded files + # would be deleted as part of the clean up process. + $expandedItems.Value += $currentArchiveEntryPath + + $addFiletoExpandedPathMessage = ($LocalizedData.CreateFileAtExpandedPath -f $currentArchiveEntryPath) + Write-Verbose $addFiletoExpandedPathMessage + } + } + finally + { + If ($null -ne $destStream) + { + $destStream.Dispose() + } + + If ($null -ne $srcStream) + { + $srcStream.Dispose() + } + } + } + + $currentEntryCount += 1 + # $currentSegmentWeight is Set to 100 giving equal weightage to each file that is getting expanded. + # $previousSegmentWeight is set to 0 as there are no prior segments. + $previousSegmentWeight = 0 + $currentSegmentWeight = 100 + ProgressBarHelper "Expand-Archive" $progressBarStatus $previousSegmentWeight $currentSegmentWeight $zipArchive.Entries.Count $currentEntryCount + } + } + finally + { + If ($null -ne $zipArchive) + { + $zipArchive.Dispose() + } + + If ($null -ne $archiveFileStream) + { + $archiveFileStream.Dispose() + } + + # Complete writing progress. + Write-Progress -Activity "Expand-Archive" -Completed + } + } + +<############################################################################################ +# ProgressBarHelper: This is a helper function used to display progress message. +# This function is used by both Compress-Archive & Expand-Archive to display archive file +# creation/expansion progress. +############################################################################################> + function ProgressBarHelper + { + param + ( + [string] + $cmdletName, + + [string] + $status, + + [double] + $previousSegmentWeight, + + [double] + $currentSegmentWeight, + + [int] + $totalNumberofEntries, + + [int] + $currentEntryCount + ) + + if ($currentEntryCount -gt 0 -and + $totalNumberofEntries -gt 0 -and + $previousSegmentWeight -ge 0 -and + $currentSegmentWeight -gt 0) + { + $entryDefaultWeight = $currentSegmentWeight/[double]$totalNumberofEntries + + $percentComplete = $previousSegmentWeight + ($entryDefaultWeight * $currentEntryCount) + Write-Progress -Activity $cmdletName -Status $status -PercentComplete $percentComplete + } + } + +<############################################################################################ +# CSVHelper: This is a helper function used to append comma after each path specifid by +# the SourcePath array. This helper function is used to display all the user supplied paths +# in the WhatIf message. +############################################################################################> + function CSVHelper + { + param + ( + [string[]] + $sourcePath + ) + + # SourcePath has already been validated by the calling funcation. + if ($sourcePath.Count -gt 1) + { + $sourcePathInCsvFormat = "`n" + for ($currentIndex = 0; $currentIndex -lt $sourcePath.Count; $currentIndex++) + { + if ($currentIndex -eq $sourcePath.Count - 1) + { + $sourcePathInCsvFormat += $sourcePath[$currentIndex] + } + else + { + $sourcePathInCsvFormat += $sourcePath[$currentIndex] + "`n" + } + } + } + else + { + $sourcePathInCsvFormat = $sourcePath + } + + return $sourcePathInCsvFormat + } + +<############################################################################################ +# ThrowTerminatingErrorHelper: This is a helper function used to throw terminating error. +############################################################################################> + function ThrowTerminatingErrorHelper + { + param + ( + [string] + $errorId, + + [string] + $errorMessage, + + [System.Management.Automation.ErrorCategory] + $errorCategory, + + [object] + $targetObject, + + [Exception] + $innerException + ) + + if ($innerException -eq $null) + { + $exception = New-object System.IO.IOException $errorMessage + } + else + { + $exception = New-Object System.IO.IOException $errorMessage, $innerException + } + + $exception = New-Object System.IO.IOException $errorMessage + $errorRecord = New-Object System.Management.Automation.ErrorRecord $exception, $errorId, $errorCategory, $targetObject + $PSCmdlet.ThrowTerminatingError($errorRecord) + } + +<############################################################################################ +# CreateErrorRecordHelper: This is a helper function used to create an ErrorRecord +############################################################################################> + function CreateErrorRecordHelper + { + param + ( + [string] + $errorId, + + [string] + $errorMessage, + + [System.Management.Automation.ErrorCategory] + $errorCategory, + + [Exception] + $exception, + + [object] + $targetObject + ) + + if ($null -eq $exception) + { + $exception = New-Object System.IO.IOException $errorMessage + } + + $errorRecord = New-Object System.Management.Automation.ErrorRecord $exception, $errorId, $errorCategory, $targetObject + return $errorRecord + } + #endregion Utility Functions + + $isVerbose = $psboundparameters.ContainsKey("Verbose") + $isConfirm = $psboundparameters.ContainsKey("Confirm") + + $isDestinationPathProvided = $true + if ($DestinationPath -eq [string]::Empty) + { + $resolvedDestinationPath = $pwd + $isDestinationPathProvided = $false + } + else + { + $destinationPathExists = Test-Path -Path $DestinationPath -PathType Container + if ($destinationPathExists) + { + $resolvedDestinationPath = GetResolvedPathHelper $DestinationPath $false $PSCmdlet + if ($resolvedDestinationPath.Count -gt 1) + { + $errorMessage = ($LocalizedData.InvalidExpandedDirPathError -f $DestinationPath) + ThrowTerminatingErrorHelper "InvalidDestinationPath" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidArgument) $DestinationPath + } + + # At this point we are sure that the provided path resolves to a valid single path. + # Calling Resolve-Path again to get the underlying provider name. + $suppliedDestinationPath = Resolve-Path -Path $DestinationPath + if ($suppliedDestinationPath.Provider.Name -ne "FileSystem") + { + $errorMessage = ($LocalizedData.ExpandArchiveInValidDestinationPath -f $DestinationPath) + ThrowTerminatingErrorHelper "InvalidDirectoryPath" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidArgument) $DestinationPath + } + } + else + { + $createdItem = New-Item -Path $DestinationPath -ItemType Directory -Confirm:$isConfirm -Verbose:$isVerbose -ErrorAction Stop + if ($createdItem -ne $null -and $createdItem.PSProvider.Name -ne "FileSystem") + { + Remove-Item "$DestinationPath" -Force -Recurse -ErrorAction SilentlyContinue + $errorMessage = ($LocalizedData.ExpandArchiveInValidDestinationPath -f $DestinationPath) + ThrowTerminatingErrorHelper "InvalidDirectoryPath" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidArgument) $DestinationPath + } + + $resolvedDestinationPath = GetResolvedPathHelper $DestinationPath $true $PSCmdlet + } + } + + $isWhatIf = $psboundparameters.ContainsKey("WhatIf") + if (!$isWhatIf) + { + $preparingToExpandVerboseMessage = ($LocalizedData.PreparingToExpandVerboseMessage) + Write-Verbose $preparingToExpandVerboseMessage + + $progressBarStatus = ($LocalizedData.ExpandProgressBarText -f $DestinationPath) + ProgressBarHelper "Expand-Archive" $progressBarStatus 0 100 100 1 + } + } + PROCESS + { + switch ($PsCmdlet.ParameterSetName) + { + "Path" + { + $resolvedSourcePaths = GetResolvedPathHelper $Path $false $PSCmdlet + + if ($resolvedSourcePaths.Count -gt 1) + { + $errorMessage = ($LocalizedData.InvalidArchiveFilePathError -f $Path, $PsCmdlet.ParameterSetName, $PsCmdlet.ParameterSetName) + ThrowTerminatingErrorHelper "InvalidArchiveFilePath" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidArgument) $Path + } + } + "LiteralPath" + { + $resolvedSourcePaths = GetResolvedPathHelper $LiteralPath $true $PSCmdlet + + if ($resolvedSourcePaths.Count -gt 1) + { + $errorMessage = ($LocalizedData.InvalidArchiveFilePathError -f $LiteralPath, $PsCmdlet.ParameterSetName, $PsCmdlet.ParameterSetName) + ThrowTerminatingErrorHelper "InvalidArchiveFilePath" $errorMessage ([System.Management.Automation.ErrorCategory]::InvalidArgument) $LiteralPath + } + } + } + + ValidateArchivePathHelper $resolvedSourcePaths + + if ($pscmdlet.ShouldProcess($resolvedSourcePaths)) + { + $expandedItems = @() + + try + { + # StopProcessing is not avaliable in Script cmdlets. However the pipleline execution + # is terminated when ever 'CTRL + C' is entered by user to terminate the cmdlet execution. + # The finally block is executed whenever pipleline is terminated. + # $isArchiveFileProcessingComplete variable is used to track if 'CTRL + C' is entered by the + # user. + $isArchiveFileProcessingComplete = $false + + # The User has not provided a destination path, hence we use '$pwd\ArchiveFileName' as the directory where the + # archive file contents would be expanded. If the path '$pwd\ArchiveFileName' already exists then we use the + # Windows default mechanism of appending a counter value at the end of the directory name where the contents + # would be expanded. + if (!$isDestinationPathProvided) + { + $archiveFile = New-Object System.IO.FileInfo $resolvedSourcePaths + $resolvedDestinationPath = Join-Path -Path $resolvedDestinationPath -ChildPath $archiveFile.BaseName + $destinationPathExists = Test-Path -LiteralPath $resolvedDestinationPath -PathType Container + + if (!$destinationPathExists) + { + New-Item -Path $resolvedDestinationPath -ItemType Directory -Confirm:$isConfirm -Verbose:$isVerbose -ErrorAction Stop | Out-Null + } + } + + ExpandArchiveHelper $resolvedSourcePaths $resolvedDestinationPath ([ref]$expandedItems) $Force $isVerbose $isConfirm + + $isArchiveFileProcessingComplete = $true + } + finally + { + # The $isArchiveFileProcessingComplete would be set to $false if user has typed 'CTRL + C' to + # terminate the cmdlet execution or if an unhandled exception is thrown. + if ($isArchiveFileProcessingComplete -eq $false) + { + if ($expandedItems.Count -gt 0) + { + # delete the expanded file/directory as the archive + # file was not completly expanded. + $expandedItems | ForEach-Object { Remove-Item $_ -Force -Recurse } + } + } + } + } + } +} + +function Write-LocalMessage +{ + [CmdletBinding()] + Param ( + [string]$Message + ) + + if (Test-Path function:Write-PSFMessage) { Write-PSFMessage -Level Important -Message $Message } + else { Write-Host $Message } +} +#endregion Utility Functions + +try +{ + [System.Net.ServicePointManager]::SecurityProtocol = "Tls12" + + Write-LocalMessage -Message "Downloading repository from '$($BaseUrl)/archive/$($Branch).zip'" + Invoke-WebRequest -Uri "$($BaseUrl)/archive/$($Branch).zip" -UseBasicParsing -OutFile "$($env:TEMP)\$($ModuleName).zip" -ErrorAction Stop + + Write-LocalMessage -Message "Creating temporary project folder: '$($env:TEMP)\$($ModuleName)'" + $null = New-Item -Path $env:TEMP -Name $ModuleName -ItemType Directory -Force -ErrorAction Stop + + Write-LocalMessage -Message "Extracting archive to '$($env:TEMP)\$($ModuleName)'" + Expand-Archive -Path "$($env:TEMP)\$($ModuleName).zip" -DestinationPath "$($env:TEMP)\$($ModuleName)" -ErrorAction Stop + + $basePath = Get-ChildItem "$($env:TEMP)\$($ModuleName)\*" | Select-Object -First 1 + if ($SubFolder) { $basePath = "$($basePath)\$($SubFolder)" } + + # Only needed for PS v5+ but doesn't hurt anyway + $manifest = "$($basePath)\$($ModuleName).psd1" + $manifestData = Invoke-Expression ([System.IO.File]::ReadAllText($manifest)) + $moduleVersion = $manifestData.ModuleVersion + Write-LocalMessage -Message "Download concluded: $($ModuleName) | Branch $($Branch) | Version $($moduleVersion)" + + # Determine output path + $path = "$($env:ProgramFiles)\WindowsPowerShell\Modules\$($ModuleName)" + if ($doUserMode) { $path = "$(Split-Path $profile.CurrentUserAllHosts)\Modules\$($ModuleName)" } + if ($PSVersionTable.PSVersion.Major -ge 5) { $path += "\$moduleVersion" } + + if ((Test-Path $path) -and (-not $Force)) + { + Write-LocalMessage -Message "Module already installed, interrupting installation" + return + } + + Write-LocalMessage -Message "Creating folder: $($path)" + $null = New-Item -Path $path -ItemType Directory -Force -ErrorAction Stop + + Write-LocalMessage -Message "Copying files to $($path)" + foreach ($file in (Get-ChildItem -Path $basePath)) + { + Move-Item -Path $file.FullName -Destination $path -ErrorAction Stop + } + + Write-LocalMessage -Message "Cleaning up temporary files" + Remove-Item -Path "$($env:TEMP)\$($ModuleName)" -Force -Recurse + Remove-Item -Path "$($env:TEMP)\$($ModuleName).zip" -Force + + Write-LocalMessage -Message "Installation of the module $($ModuleName), Branch $($Branch), Version $($moduleVersion) completed successfully!" +} +catch +{ + Write-LocalMessage -Message "Installation of the module $($ModuleName) failed!" + + Write-LocalMessage -Message "Cleaning up temporary files" + Remove-Item -Path "$($env:TEMP)\$($ModuleName)" -Force -Recurse + Remove-Item -Path "$($env:TEMP)\$($ModuleName).zip" -Force + + throw +} \ No newline at end of file diff --git a/library/AaronLocker/AaronLocker.sln b/library/AaronLocker/AaronLocker.sln new file mode 100644 index 0000000..047bfd1 --- /dev/null +++ b/library/AaronLocker/AaronLocker.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.27130.2010 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{4826D597-18BC-45C7-A450-B77690AE9E1C}") = "AaronLocker", "AaronLocker\AaronLocker.csproj", "{FEE52C70-F892-4416-856E-257CF8D5C6C8}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {FEE52C70-F892-4416-856E-257CF8D5C6C8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FEE52C70-F892-4416-856E-257CF8D5C6C8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FEE52C70-F892-4416-856E-257CF8D5C6C8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FEE52C70-F892-4416-856E-257CF8D5C6C8}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {3E80EB38-E3AD-4519-9770-7B64662409BA} + EndGlobalSection +EndGlobal diff --git a/library/AaronLocker/AaronLocker/AaronLocker.csproj b/library/AaronLocker/AaronLocker/AaronLocker.csproj new file mode 100644 index 0000000..873312a --- /dev/null +++ b/library/AaronLocker/AaronLocker/AaronLocker.csproj @@ -0,0 +1,27 @@ + + + + net4.5.2 + + + + ..\..\..\AaronLocker\bin + ..\..\..\AaronLocker\bin\AaronLocker.xml + + + + ..\..\..\AaronLocker\bin + ..\..\..\AaronLocker\bin\AaronLocker.xml + + + + false + + + + + C:\Windows\Microsoft.NET\assembly\GAC_MSIL\System.Management.Automation\v4.0_3.0.0.0__31bf3856ad364e35\System.Management.Automation.dll + + + + diff --git a/library/AaronLocker/AaronLocker/Action.cs b/library/AaronLocker/AaronLocker/Action.cs new file mode 100644 index 0000000..d2f55ee --- /dev/null +++ b/library/AaronLocker/AaronLocker/Action.cs @@ -0,0 +1,18 @@ +namespace AaronLocker +{ + /// + /// Whether to permit or deny execution in a given rule + /// + public enum Action + { + /// + /// Allow execution + /// + Allow, + + /// + /// Deny execution + /// + Deny + } +} diff --git a/library/AaronLocker/AaronLocker/EnforcementMode.cs b/library/AaronLocker/AaronLocker/EnforcementMode.cs new file mode 100644 index 0000000..64d2f34 --- /dev/null +++ b/library/AaronLocker/AaronLocker/EnforcementMode.cs @@ -0,0 +1,23 @@ +namespace AaronLocker +{ + /// + /// Whether a policy should be enforced + /// + public enum EnforcementMode + { + /// + /// The policy has yet to be configured + /// + NotConfigured = 1, + + /// + /// The policy is designed for audit only + /// + AuditOnly = 2, + + /// + /// The policy will be enforced + /// + Enabled = 3 + } +} diff --git a/library/AaronLocker/AaronLocker/HashRule.cs b/library/AaronLocker/AaronLocker/HashRule.cs new file mode 100644 index 0000000..a40debf --- /dev/null +++ b/library/AaronLocker/AaronLocker/HashRule.cs @@ -0,0 +1,83 @@ +using System; +using System.Xml; + +namespace AaronLocker +{ + /// + /// A rule to apply based on hash values + /// + [Serializable] + public class HashRule : RuleBase + { + /// + /// The hash value to apply the rule by + /// + public string HashValue; + + /// + /// The name of the actual file the hash targets + /// + public string FileName; + + /// + /// The original input file's length + /// + public int SourceFileLength; + + /// + /// Attach rule to policy + /// + /// The AppLocker policy to integrate into. + /// The policy object that calls for this integration. + public override void AddToPolicy(XmlDocument Document, Policy Policy) + { + #region Create Element + XmlElement element = Document.CreateElement("FileHashRule"); + if (Id != Guid.Empty) + element.SetAttribute("Id", Id.ToString()); + else + element.SetAttribute("Id", Guid.NewGuid().ToString()); + element.SetAttribute("Name", Label); + element.SetAttribute("Description", Description); + element.SetAttribute("UserOrGroupSid", UserOrGroupSid); + element.SetAttribute("Action", Action.ToString()); + XmlElement condition = Document.CreateElement("Conditions"); + XmlElement filePathCondition = Document.CreateElement("FileHashCondition"); + XmlElement hashCondition = Document.CreateElement("FileHash"); + hashCondition.SetAttribute("Type", "SHA256"); + hashCondition.SetAttribute("Data", HashValue); + hashCondition.SetAttribute("SourceFileName", FileName); + if (SourceFileLength > 0) + hashCondition.SetAttribute("SourceFileLength", SourceFileLength.ToString()); + filePathCondition.AppendChild(hashCondition); + condition.AppendChild(filePathCondition); + element.AppendChild(condition); + + #endregion Create Element + + #region Attach based on Collection + if ((Collection & Scope.AppX) != 0) + Document.SelectNodes("//RuleCollection[@Type='Appx']")[0].AppendChild(element.Clone()); + if ((Collection & Scope.Dll) != 0) + Document.SelectNodes("//RuleCollection[@Type='Dll']")[0].AppendChild(element.Clone()); + if ((Collection & Scope.Exe) != 0) + Document.SelectNodes("//RuleCollection[@Type='Exe']")[0].AppendChild(element.Clone()); + if ((Collection & Scope.Msi) != 0) + Document.SelectNodes("//RuleCollection[@Type='Msi']")[0].AppendChild(element.Clone()); + if ((Collection & Scope.Script) != 0) + Document.SelectNodes("//RuleCollection[@Type='Script']")[0].AppendChild(element.Clone()); + #endregion Attach based on Collection + } + + /// + public override object Clone() + { + HashRule tempRule = new HashRule(); + CopyBaseProperties(tempRule); + tempRule.HashValue = HashValue; + tempRule.FileName = FileName; + tempRule.SourceFileLength = SourceFileLength; + return tempRule; + } + } +} diff --git a/library/AaronLocker/AaronLocker/PathRule.cs b/library/AaronLocker/AaronLocker/PathRule.cs new file mode 100644 index 0000000..585e877 --- /dev/null +++ b/library/AaronLocker/AaronLocker/PathRule.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Generic; +using System.Xml; + +namespace AaronLocker +{ + /// + /// A rule enforcing compliance based on path. + /// + [Serializable] + public class PathRule : RuleBase + { + /// + /// The path to which to apply this rule + /// + public string Path; + + /// + /// Items or folders under the path to exclude from this rule + /// + public List Exceptions = new List(); + + /// + /// Attach rule to policy + /// + /// The AppLocker policy to integrate into. + /// The policy object that calls for this integration. + public override void AddToPolicy(XmlDocument Document, Policy Policy) + { + #region Create Element + XmlElement element = Document.CreateElement("FilePathRule"); + if (Id != Guid.Empty) + element.SetAttribute("Id", Id.ToString()); + else + element.SetAttribute("Id", Guid.NewGuid().ToString()); + element.SetAttribute("Name", Label); + element.SetAttribute("Description", Description); + element.SetAttribute("UserOrGroupSid", UserOrGroupSid); + element.SetAttribute("Action", Action.ToString()); + XmlElement condition = Document.CreateElement("Conditions"); + XmlElement filePathCondition = Document.CreateElement("FilePathCondition"); + filePathCondition.SetAttribute("Path", Path); + condition.AppendChild(filePathCondition); + element.AppendChild(condition); + + if (Exceptions.Count > 0) + { + XmlElement exceptions = Document.CreateElement("Exceptions"); + element.AppendChild(exceptions); + foreach (string pathItem in Exceptions) + { + XmlElement exception = Document.CreateElement("FilePathCondition"); + exception.SetAttribute("Path", pathItem); + exceptions.AppendChild(exception); + } + } + #endregion Create Element + + #region Attach based on Collection + if ((Collection & Scope.AppX) != 0) + Document.SelectNodes("//RuleCollection[@Type='Appx']")[0].AppendChild(element.Clone()); + if ((Collection & Scope.Dll) != 0) + Document.SelectNodes("//RuleCollection[@Type='Dll']")[0].AppendChild(element.Clone()); + if ((Collection & Scope.Exe) != 0) + Document.SelectNodes("//RuleCollection[@Type='Exe']")[0].AppendChild(element.Clone()); + if ((Collection & Scope.Msi) != 0) + Document.SelectNodes("//RuleCollection[@Type='Msi']")[0].AppendChild(element.Clone()); + if ((Collection & Scope.Script) != 0) + Document.SelectNodes("//RuleCollection[@Type='Script']")[0].AppendChild(element.Clone()); + #endregion Attach based on Collection + } + + /// + public override object Clone() + { + PathRule tempRule = new PathRule(); + CopyBaseProperties(tempRule); + tempRule.Path = Path; + tempRule.Exceptions = new List(Exceptions.ToArray()); + return tempRule; + } + } +} diff --git a/library/AaronLocker/AaronLocker/Policy.cs b/library/AaronLocker/AaronLocker/Policy.cs new file mode 100644 index 0000000..3f30114 --- /dev/null +++ b/library/AaronLocker/AaronLocker/Policy.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Xml; + +namespace AaronLocker +{ + /// + /// An AppLocker policy, containing rules and offering tools to convert / integrate into output generation. + /// + [Serializable] + public class Policy + { + /// + /// List of all rules that are part of this policy + /// + public List Rules = new List(); + + /// + /// Number of rules stored in the policy + /// + public int RulesCount + { + get { return Rules.Count; } + set { } + } + + /// + /// An arbitrary name for this policy. Internal use only, to help distinguishing between different policies. + /// + public string Name; + + /// + /// Add a neat description, telling your future self what this was all about + /// + public string Description; + + /// + /// When was the last update to the policy + /// + public DateTime LastUpdate = DateTime.Now; + + /// + /// List of rules that failed to execute during the last compilation effort + /// + public List FailedRules = new List(); + + /// + /// Returns XML string of the finished AppLocker policy + /// + /// How the policy should be enforced + /// XML Text + public string GetXml(EnforcementMode EnforcementMode = EnforcementMode.NotConfigured) + { + FailedRules = new List(); + XmlDocument document = new XmlDocument(); + document.LoadXml(String.Format(@" + + + + + + + +", EnforcementMode.ToString())); + foreach (RuleBase ruleItem in Rules) + ruleItem.AddToPolicy(document, this); + + XmlWriterSettings settings = new XmlWriterSettings(); + settings.NewLineHandling = NewLineHandling.Replace; + settings.NewLineChars = "\r\n"; + settings.Indent = true; + + using (var stringWriter = new StringWriter()) + using (var xmlTextWriter = XmlWriter.Create(stringWriter, settings)) + { + document.WriteTo(xmlTextWriter); + xmlTextWriter.Flush(); + return stringWriter.GetStringBuilder().ToString(); + } + } + } +} diff --git a/library/AaronLocker/AaronLocker/PublisherRule.cs b/library/AaronLocker/AaronLocker/PublisherRule.cs new file mode 100644 index 0000000..f2a922c --- /dev/null +++ b/library/AaronLocker/AaronLocker/PublisherRule.cs @@ -0,0 +1,155 @@ +using System; +using System.Diagnostics; +using System.Security.Cryptography.X509Certificates; +using System.Xml; + +namespace AaronLocker +{ + /// + /// Rule acting based on publisher that signed a file. + /// + [Serializable] + public class PublisherRule : RuleBase + { + #region Full Specs + /// + /// The name of the publisher + /// + public string PublisherName; + + /// + /// Name of the product + /// + public string ProductName; + + /// + /// Name of the file + /// + public string BinaryName; + + /// + /// Minimum version to apply this to + /// + public Version MinimumVersion; + + /// + /// Last version to apply this rule to. + /// + public Version MaximumVersion; + #endregion Full Specs + + #region Exemplar Mode + /// + /// Path to an exampel file to use to generate publisher information + /// + public string Exemplar; + + /// + /// Whether to also use the product information, when recording from an example file. + /// + public bool UseProduct; + #endregion Exemplar Mode + + /// + /// Resolves an Exemplar into the publisher rule relevant data + /// + public void Resolve() + { + if (String.IsNullOrEmpty(Exemplar)) + return; + + X509Certificate certificate = null; + + try { certificate = X509Certificate.CreateFromSignedFile(Exemplar); } + catch (Exception e) { throw new InvalidOperationException(String.Format("Failed to read certificate from signed file. {0}", e.Message), e); } + + PublisherName = certificate.Subject; + + if (!UseProduct) + return; + + FileVersionInfo info = null; + try { info = FileVersionInfo.GetVersionInfo(Exemplar); } + catch (Exception e) { throw new InvalidOperationException(String.Format("Failed to read file info from file. {0}", e.Message), e); } + + ProductName = info.ProductName; + BinaryName = info.FileName; + MinimumVersion = Version.Parse(info.FileVersion); + } + + /// + /// Attach rule to policy + /// + /// The AppLocker policy to integrate into. + /// The policy object that calls for this integration. + public override void AddToPolicy(XmlDocument Document, Policy Policy) + { + if (String.IsNullOrEmpty(PublisherName)) + { + try { Resolve(); } + catch (Exception e) + { + Policy.FailedRules.Add(new RuleFailure(this, e)); + return; + } + } + + #region Create Element + XmlElement element = Document.CreateElement("FilePublisherRule"); + if (Id != Guid.Empty) + element.SetAttribute("Id", Id.ToString()); + else + element.SetAttribute("Id", Guid.NewGuid().ToString()); + element.SetAttribute("Name", Label); + element.SetAttribute("Description", Description); + element.SetAttribute("UserOrGroupSid", UserOrGroupSid); + element.SetAttribute("Action", Action.ToString()); + XmlElement condition = Document.CreateElement("Conditions"); + XmlElement filePublisherCondition = Document.CreateElement("FilePublisherCondition"); + filePublisherCondition.SetAttribute("PublisherName", PublisherName); + filePublisherCondition.SetAttribute("ProductName", ProductName); + filePublisherCondition.SetAttribute("BinaryName", BinaryName); + XmlElement binaryVersionRange = Document.CreateElement("BinaryVersionRange"); + if (MinimumVersion != null) + binaryVersionRange.SetAttribute("LowSection", MinimumVersion.ToString()); + else + binaryVersionRange.SetAttribute("LowSection", "*"); + if (MaximumVersion != null) + binaryVersionRange.SetAttribute("HighSection", MaximumVersion.ToString()); + else + binaryVersionRange.SetAttribute("HighSection", "*"); + filePublisherCondition.AppendChild(binaryVersionRange); + condition.AppendChild(filePublisherCondition); + element.AppendChild(condition); + #endregion Create Element + + #region Attach based on Collection + if ((Collection & Scope.AppX) != 0) + Document.SelectNodes("//RuleCollection[@Type='Appx']")[0].AppendChild(element.Clone()); + if ((Collection & Scope.Dll) != 0) + Document.SelectNodes("//RuleCollection[@Type='Dll']")[0].AppendChild(element.Clone()); + if ((Collection & Scope.Exe) != 0) + Document.SelectNodes("//RuleCollection[@Type='Exe']")[0].AppendChild(element.Clone()); + if ((Collection & Scope.Msi) != 0) + Document.SelectNodes("//RuleCollection[@Type='Msi']")[0].AppendChild(element.Clone()); + if ((Collection & Scope.Script) != 0) + Document.SelectNodes("//RuleCollection[@Type='Script']")[0].AppendChild(element.Clone()); + #endregion Attach based on Collection + } + + /// + public override object Clone() + { + PublisherRule tempRule = new PublisherRule(); + CopyBaseProperties(tempRule); + tempRule.PublisherName = PublisherName; + tempRule.ProductName = ProductName; + tempRule.BinaryName = BinaryName; + tempRule.MinimumVersion = MinimumVersion; + tempRule.MaximumVersion = MaximumVersion; + tempRule.Exemplar = Exemplar; + tempRule.UseProduct = UseProduct; + return tempRule; + } + } +} diff --git a/library/AaronLocker/AaronLocker/RuleBase.cs b/library/AaronLocker/AaronLocker/RuleBase.cs new file mode 100644 index 0000000..6cabea0 --- /dev/null +++ b/library/AaronLocker/AaronLocker/RuleBase.cs @@ -0,0 +1,86 @@ +using System; +using System.Xml; + +namespace AaronLocker +{ + /// + /// Base class for AppLocker rules + /// + [Serializable] + public abstract class RuleBase : ICloneable + { + /// + /// The name of the rule + /// + public string Label; + + /// + /// A description of what this rule is all about + /// + public string Description; + + /// + /// Group or user the rule applies to + /// + public string UserOrGroupSid; + + /// + /// An ID of the rule. Leave this empty, if you do not want to hardcode a specific GUid for a specific rule. + /// + public Guid Id; + + /// + /// What scope does the rule apply to (specifically: Is it designed to affect dlls, executables or scripts). + /// + public Scope Collection = Scope.Default; + + /// + /// What kind of rule is this? + /// + public RuleType Type + { + get + { + if ((this as HashRule) != null) + return RuleType.Hash; + if ((this as PublisherRule) != null) + return RuleType.Publisher; + if ((this as SourcePathRule) != null) + return RuleType.SourcePath; + return RuleType.Path; + } + set { } + } + + /// + /// Whether to allow (Whitelist) or deny (Blacklist) the target of this rule + /// + public Action Action = Action.Allow; + + /// + /// Each rule must be able to attach itself to an XML document representing an AppLocker rule. + /// + /// The AppLocker policy to integrate into. + /// The policy object that calls for this integration. + public abstract void AddToPolicy(XmlDocument Document, Policy Policy); + + /// + /// Clones the current rule + /// + public abstract object Clone(); + + /// + /// Copies the base properties of a rule object. Used by Clone() implementations. + /// + /// The object to copy properties into. + internal void CopyBaseProperties(RuleBase Target) + { + Target.Label = Label; + Target.Description = Description; + Target.UserOrGroupSid = UserOrGroupSid; + Target.Id = Id; + Target.Collection = Collection; + Target.Action = Action; + } + } +} diff --git a/library/AaronLocker/AaronLocker/RuleFailure.cs b/library/AaronLocker/AaronLocker/RuleFailure.cs new file mode 100644 index 0000000..b814eb3 --- /dev/null +++ b/library/AaronLocker/AaronLocker/RuleFailure.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AaronLocker +{ + /// + /// Class representing a rule that failed to properly resolve. + /// Used when resolving rules from a Policy object. + /// + [Serializable] + public class RuleFailure + { + /// + /// The type of rule it was + /// + public RuleType Type { get { return Rule.Type; } } + + /// + /// The label the rule was meant to carry + /// + public string Label { get { return Rule.Label; } } + + /// + /// The source rule object + /// + public RuleBase Rule; + + /// + /// The actual exception that prevented success + /// + public Exception Error; + + /// + /// Creates an empty rule failure + /// + public RuleFailure() { } + + /// + /// Creates a preconfigured rule failure + /// + /// The rule that failed + /// The exception describing the failure + public RuleFailure(RuleBase Rule, Exception Error) + { + this.Rule = Rule; + this.Error = Error; + } + } +} diff --git a/library/AaronLocker/AaronLocker/RuleType.cs b/library/AaronLocker/AaronLocker/RuleType.cs new file mode 100644 index 0000000..a9ba3f8 --- /dev/null +++ b/library/AaronLocker/AaronLocker/RuleType.cs @@ -0,0 +1,28 @@ +namespace AaronLocker +{ + /// + /// The kind of rule a rule is + /// + public enum RuleType + { + /// + /// A rule based on controlling execution of files signed by a specific publisher + /// + Publisher, + + /// + /// A rule that controls execution based on a specific file hash + /// + Hash, + + /// + /// A rule controlling execution based on path content is executed from + /// + Path, + + /// + /// A temporary rule that will be converted into regular rules when realized. + /// + SourcePath + } +} diff --git a/library/AaronLocker/AaronLocker/Scope.cs b/library/AaronLocker/AaronLocker/Scope.cs new file mode 100644 index 0000000..439c1f7 --- /dev/null +++ b/library/AaronLocker/AaronLocker/Scope.cs @@ -0,0 +1,46 @@ +using System; + +namespace AaronLocker +{ + /// + /// The various rule scope types available in an AaronLocker based Applocker Rule + /// + [Flags] + public enum Scope + { + /// + /// The rule applies to executables + /// + Exe = 1, + + /// + /// The rule applies to Dynamic Link Libraries + /// + Dll = 2, + + /// + /// The rule applies to script files + /// + Script = 4, + + /// + /// The default package applies to executables, dlls and script files + /// + Default = 7, + + /// + /// The rule applies to installer files + /// + Msi = 8, + + /// + /// The rule applies to AppX UWP Apps + /// + AppX = 16, + + /// + /// The rule applies to all types of files + /// + All = 31 + } +} diff --git a/library/AaronLocker/AaronLocker/SerializationTypeConverter.cs b/library/AaronLocker/AaronLocker/SerializationTypeConverter.cs new file mode 100644 index 0000000..c5d0956 --- /dev/null +++ b/library/AaronLocker/AaronLocker/SerializationTypeConverter.cs @@ -0,0 +1,234 @@ +using System; +using System.IO; +using System.Management.Automation; +using System.Runtime.Serialization; +using System.Runtime.Serialization.Formatters.Binary; + +namespace AaronLocker +{ + /// + /// Typeconverter that does the heavy lifting of maintaining type integrity across process borders. + /// + public class SerializationTypeConverter : PSTypeConverter + { + private static ResolveEventHandler AssemblyHandler = new ResolveEventHandler(SerializationTypeConverter.CurrentDomain_AssemblyResolve); + + /// + /// Whether the source can be converted to its destination + /// + /// The value to convert + /// The type to convert to + /// Whether this action is possible + public override bool CanConvertFrom(object sourceValue, Type destinationType) + { + byte[] array; + Exception ex; + return this.CanConvert(sourceValue, destinationType, out array, out ex); + } + + /// + /// Converts an object + /// + /// The data to convert + /// The type to convert to + /// This will be ignored, but must be present + /// This will be ignored, but must be present + /// The converted object + public override object ConvertFrom(object sourceValue, Type destinationType, IFormatProvider formatProvider, bool ignoreCase) + { + return this.DeserializeObject(sourceValue, destinationType); + } + + /// + /// Whether the input object can be converted to the Destination type + /// + /// Input value + /// The type to convert to + /// + public override bool CanConvertTo(object sourceValue, Type destinationType) + { + byte[] array; + Exception ex; + return this.CanConvert(sourceValue, destinationType, out array, out ex); + } + + /// + /// Converts an object + /// + /// The data to convert + /// The type to convert to + /// This will be ignored, but must be present + /// This will be ignored, but must be present + /// The converted object + public override object ConvertTo(object sourceValue, Type destinationType, IFormatProvider formatProvider, bool ignoreCase) + { + return this.DeserializeObject(sourceValue, destinationType); + } + private bool CanConvert(object sourceValue, Type destinationType, out byte[] serializationData, out Exception error) + { + serializationData = null; + error = null; + if (destinationType == null) + { + error = new ArgumentNullException("destinationType"); + return false; + } + if (sourceValue == null) + { + error = new ArgumentNullException("sourceValue"); + return false; + } + PSObject pSObject = sourceValue as PSObject; + if (pSObject == null) + { + error = new NotSupportedException(string.Format("Unsupported Source Type: {0}", sourceValue.GetType().FullName)); + return false; + } + if (!SerializationTypeConverter.CanSerialize(destinationType)) + { + error = new NotSupportedException(string.Format("Unsupported Type Conversion: {0}", destinationType.FullName)); + return false; + } + if (typeof(Exception).IsAssignableFrom(destinationType) && pSObject.TypeNames != null && pSObject.TypeNames.Count > 0 && pSObject.TypeNames[0].StartsWith("Deserialized.System.Management.Automation")) + { + foreach (string current in pSObject.TypeNames) + { + if (current.Equals("Deserialized.System.Management.Automation.ParameterBindingException", StringComparison.OrdinalIgnoreCase)) + { + return false; + } + } + } + if (pSObject.Properties["SerializationData"] == null) + { + error = new NotSupportedException("Serialization Data is Absent"); + return false; + } + object value = pSObject.Properties["SerializationData"].Value; + if (!(value is byte[])) + { + error = new NotSupportedException("Unsupported Data Format"); + return false; + } + serializationData = (value as byte[]); + return true; + } + private object DeserializeObject(object sourceValue, Type destinationType) + { + byte[] buffer; + Exception ex; + if (!this.CanConvert(sourceValue, destinationType, out buffer, out ex)) + { + throw ex; + } + object obj; + using (MemoryStream memoryStream = new MemoryStream(buffer)) + { + AppDomain.CurrentDomain.AssemblyResolve += SerializationTypeConverter.AssemblyHandler; + try + { + BinaryFormatter binaryFormatter = new BinaryFormatter(); + obj = binaryFormatter.Deserialize(memoryStream); + IDeserializationCallback deserializationCallback = obj as IDeserializationCallback; + if (deserializationCallback != null) + { + deserializationCallback.OnDeserialization(sourceValue); + } + } + finally + { + AppDomain.CurrentDomain.AssemblyResolve -= SerializationTypeConverter.AssemblyHandler; + } + } + return obj; + } + + /// + /// Registers an assembly resolving event + /// + public static void RegisterAssemblyResolver() + { + AppDomain.CurrentDomain.AssemblyResolve += SerializationTypeConverter.AssemblyHandler; + } + private static System.Reflection.Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args) + { + System.Reflection.Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); + for (int i = 0; i < assemblies.Length; i++) + { + if (assemblies[i].FullName == args.Name) + { + return assemblies[i]; + } + } + return null; + } + + /// + /// Whether an object can be serialized + /// + /// The object to test + /// Whether the object can be serialized + public static bool CanSerialize(object obj) + { + return obj != null && SerializationTypeConverter.CanSerialize(obj.GetType()); + } + + /// + /// Whether a type can be serialized + /// + /// The type to test + /// Whether the specified type can be serialized + public static bool CanSerialize(Type type) + { + return SerializationTypeConverter.TypeIsSerializable(type) && !type.IsEnum || (type.Equals(typeof(Exception)) || type.IsSubclassOf(typeof(Exception))); + } + + /// + /// The validation check on whether a type is serializable + /// + /// The type to test + /// Returns whether that type can be serialized + public static bool TypeIsSerializable(Type type) + { + if (type == null) + { + throw new ArgumentNullException("type"); + } + if (!type.IsSerializable) + { + return false; + } + if (!type.IsGenericType) + { + return true; + } + Type[] genericArguments = type.GetGenericArguments(); + for (int i = 0; i < genericArguments.Length; i++) + { + Type type2 = genericArguments[i]; + if (!SerializationTypeConverter.TypeIsSerializable(type2)) + { + return false; + } + } + return true; + } + + /// + /// Used to obtain the information to write + /// + /// The object to dissect + /// A memory stream. + public static object GetSerializationData(PSObject psObject) + { + object result; + using (MemoryStream memoryStream = new MemoryStream()) + { + BinaryFormatter binaryFormatter = new BinaryFormatter(); + binaryFormatter.Serialize(memoryStream, psObject.BaseObject); + result = memoryStream.ToArray(); + } + return result; + } + } +} \ No newline at end of file diff --git a/library/AaronLocker/AaronLocker/SourcePathRule.cs b/library/AaronLocker/AaronLocker/SourcePathRule.cs new file mode 100644 index 0000000..f62baf5 --- /dev/null +++ b/library/AaronLocker/AaronLocker/SourcePathRule.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using System.Management.Automation; +using System.Xml; + +namespace AaronLocker +{ + /// + /// A rule based on a source file, can be converted to the most constrained rule object type. + /// + [Serializable] + public class SourcePathRule : RuleBase + { + /// + /// The path to the item + /// + public string Path; + + /// + /// Whether the specified path should be resolved recursively + /// + public bool Recurse; + + /// + /// Whether the found version of a product should be enforced, if the rule resolves into a Publisher Rule. + /// + public bool EnforceMinimumVersion; + + /// + /// Processes the path specified and generates rules based on it. + /// + /// Rules that are as restrictive as possible + public List Resolve() + { + if (ResolutionScript == null) + throw new InvalidOperationException("Resolution script has not been assigned! This generally means the module was not imported correctly."); + + List results = new List(); + + foreach (PSObject obj in ResolutionScript.Invoke(this)) + results.Add((RuleBase)obj.BaseObject); + + return results; + } + + /// + /// Scriptblock used to resolve the specified path into rule objects + /// + public static ScriptBlock ResolutionScript; + + /// + /// Attach rule to policy + /// + /// The AppLocker policy to integrate into. + /// The policy object that calls for this integration. + public override void AddToPolicy(XmlDocument Document, Policy Policy) + { + List results = null; + try { results = Resolve(); } + catch (Exception e) + { + Policy.FailedRules.Add(new RuleFailure(this, e)); + return; + } + foreach (RuleBase rule in results) + rule.AddToPolicy(Document, Policy); + } + + /// + public override object Clone() + { + SourcePathRule tempRule = new SourcePathRule(); + CopyBaseProperties(tempRule); + tempRule.Path = Path; + tempRule.Recurse = Recurse; + tempRule.EnforceMinimumVersion = EnforceMinimumVersion; + return tempRule; + } + } +}