Quantcast
Channel: Scripting Blog
Viewing all 2129 articles
Browse latest View live

Remoting Week: Remoting Security

$
0
0

Summary: Richard Siddaway explains how to secure Windows PowerShell remoting sessions.

Hey, Scripting Guy! Question Hey, Scripting Guy! I’ve just starting learning Windows PowerShell, and I understand how to use it as a scripting language and shell on the local machine. How do I work with remote machines?

—AP

Hey, Scripting Guy! Answer Hello AP,

Honorary Scripting Guy, Richard Siddaway, here today filling in for my good friend, The Scripting Guy. This is the fourth part of a series of five posts about remoting:

  1. Remoting Recap
  2. Remoting Sessions
  3. Configuring Remoting
  4. Remoting Security
  5. Non-Domain Remoting

You have seen how you can access remote systems by using the ComputerName parameter of various cmdlets. This is very good for ad hoc work, or if you need to access a remote machine to perform a single task. When you need to access multiple machines multiple times, you need to use Windows PowerShell remote sessions to give you a reusable connection that is more efficient overall.

In the third part of this series, you learned about:

  • Overcoming the second hop issue
  • Using HTTPS instead of HTTP for your WSMAN connection
  • Configuring endpoints

These have one thing in common: They can be used to make your remoting session more secure. Please note that I’m not suggesting that the default configuration is insecure. I have heard of many organizations refusing to allow enabling remoting because they think it compromises the security of their servers. Windows PowerShell and remoting does not give you any more permissions than you have already. If you use RDP to remote into a Windows system, you can only perform actions that your credentials allow. Exactly the same is true of Windows PowerShell and remoting—you can only perform those actions for which you have permissions.

That’s not the end of the story. You can use the information on configuring endpoints from last time to create your own custom endpoints. These endpoints can be locked down to add further control for who can access them and what they can do once they have access. This is in addition to the normal Windows security.

You can take any endpoint and remote in to it through Windows PowerShell Web Access. This provides Windows PowerShell in a browser through your endpoint to enable remote access into your environment from the comfort of your own home—or anywhere else you can get an Internet connection.

First we need to create a custom endpoint. As an example, lets create an endpoint that gives access only to the Active Directory cmdlets:

New-PSSessionConfigurationFile -Path c:\scripts\config\RestrictedPSAD.pssc -LanguageMode Restricted -Description "Access to AD cmdlets" -ExecutionPolicy Restricted -ModulesToImport 'Microsoft.PowerShell.Utility', ActiveDirectory -PowerShellVersion 3.0  -SessionType RestrictedRemoteServer -VisibleCmdlets 'Read-Host', *AD*

Things to note about this configuration:

  • LanguageMode Restricted
    You can use cmdlets and functions, but you are not permitted to use script blocks or variables except for the following permitted variables: $PSCulture, $PSUICulture, $True, $False, and $Null. You can use only the basic comparison operators (-eq, -gt, -lt). Assignment statements, property references, and method calls are not permitted.
  • ExecutionPolicy Restricted
    You cannot run any scripts.
  • ModulesToImport
    'Microsoft.PowerShell.Utility', ActiveDirectory means that no other Windows PowerShell modules can be imported into the session.
  • SessionType RestrictedRemoteServer
    Only the following proxy functions are available: Exit-PSSession,Get-Command, Get-FormatData, Get-Help, Measure-Object, Out-Default, and Select-Object
  • VisibleCmdlets
    Further restricts the cmdlets that are available to Read-Host and the Active Directory cmdlets.

You could lock this down so that only Read-Host, Get-AdUser, and Set-ADAccountPassword are available in an endpoint.  This would allow a junior administrator to remote in to reset a user’s password, but not perform any other actions. You can find more details about session configuration files in the cmdlet Help file and the about_session_configuration_files Help file.

You now have to register the configuration to create the endpoint:

Register-PSSessionConfiguration -Path c:\scripts\config\RestrictedPSAD.pssc -Force -Name ADPS

Notice that I’ve used a different name for the endpoint from the file. This is recommended. You can test the endpoint’s existence:

£> Get-PSSessionConfiguration -Name ADPS

Name          : ADPS

PSVersion     : 3.0

StartupScript :

RunAsUser     :

Permission    : BUILTIN\Administrators AccessAllowed, BUILTIN\Remote Management Users AccessAllowed

This endpoint allows administrators to remote into the system and access the Active Directory cmdlets. No other functionality is available to them. This is a good way to create a separation of duties scenario for your admins. In addition to accessing this endpoint remotely, you can access it (or any other remoting endpoint) with Windows PowerShell Web Access.

PowerShell Web Access was introduced in Windows Server 2012 and enhanced in Windows Server 2012 R2. You can access a remoting endpoint through a browser and run Windows PowerShell commands against that remote system.

Sorry, but when you implement Windows PowerShell Web Access, things are just a tiny bit more complicated than that.

The first thing you need to do is install Windows PowerShell Web Access. It’s a Windows feature, and it isn’t installed by default. You also need to ensure that the following features are installed:

  • Internet Information Services (IIS)
  • .NET Framework 4.5
  • Windows PowerShell (4.0 or 3.0, depending on if you are using Windows Server 2012 R2 or Windows Server 2012 respectively)

You can use Server Manager to perform the installation—or better still, use Windows PowerShell:

Install-WindowsFeature -Name WindowsPowerShellWebAccess -IncludeAllSubFeature -IncludeManagementTools
-Restart

The supporting roles and features should be installed for you. If you want to ensure that you have full control over the installation process, you can specifically state what you want to install:

Install-WindowsFeature -Name Web-WebServer, Web-Mgmt-Console, NET-Framework-45-ASPNET, Web-Net-Ext45, Web-ISAPI-Ext, Web-ISAPI-Filter, Web-Default-Doc, Web-Http-Errors, Web-Http-Redirect, Web-Static-Content,
Web-Filtering, WindowsPowerShellWebAccess -Confirm:$false

The installation adds a Windows PowerShell module for managing Windows PowerShell Web Access:

£> Get-Command -Module PowerShellWebAccess

 

CommandType     Name

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

Function        Install-PswaWebApplication

Function        Uninstall-PswaWebApplication

Cmdlet          Add-PswaAuthorizationRule

Cmdlet          Get-PswaAuthorizationRule

Cmdlet          Remove-PswaAuthorizationRule

Cmdlet          Test-PswaAuthorizationRule

Your next step is to create the Windows PowerShell Web Access web application:

Install-PswaWebApplication -WebApplicationName PSG  -UseTestCertificate

Give the application a name, and in this case, use a self-generated test certificate. Do not do this in a production environment—use a proper SSL certificate.

You now need to add a rule to enable a user or group to access a server:

Add-PswaAuthorizationRule -RuleName "RS Server 02 Full" -ComputerName server02.manticore.org  
-UserName manticore\richard -ConfigurationName microsoft.powershell

This command creates a rule that allows a user named Richard to access the default remoting endpoint on an individual server. No other users can access that endpoint and server pairing through Windows PowerShell Web Access until they are explicitly granted the rights to do so.

To minimize administration, you are advised to use groups rather than individuals. If you look at the syntax of the Add-PswaAuthorizationRule, you will see that you can use groups of computers or groups of users:

£> Get-Command Add-PswaAuthorizationRule -Syntax

Add-PswaAuthorizationRule -ComputerGroupName <string> -ConfigurationName <string> -UserGroupName <string[]> [-Credential <pscredential>] [-RuleName <string>] [-Force] [<CommonParameters>]

Add-PswaAuthorizationRule [-UserName] <string[]> -ComputerGroupName <string> -ConfigurationName <string>
[-Credential<pscredential>] [-RuleName <string>] [-Force] [<CommonParameters>]

Add-PswaAuthorizationRule -ComputerName <string> -ConfigurationName <string> -UserGroupName <string[]>
[-Credential<pscredential>] [-RuleName <string>] [-Force] [<CommonParameters>]

Add-PswaAuthorizationRule [-UserName] <string[]> [-ComputerName] <string> [-ConfigurationName] <string>
[-Credential<pscredential>] [-RuleName <string>] [-Force] [<CommonParameters>]

Consult the cmdlet documentation (remember to use Update-Help) for full details.

You can examine the rules enabled on a Windows PowerShell Web Access box:

£> Get-PswaAuthorizationRule | Format-List *

 

Id                : 0

RuleName          : RS Server 02 Full

User              : manticore\richard

UserType          : User

Destination       : manticore\server02

DestinationType   : Computer

ConfigurationName : microsoft.powershell

And you can test these rules:

Test-PswaAuthorizationRule -ComputerName server02 -UserName manticore\richard

This command tests if a particular user can access a particular computer through Windows PowerShell Web Access. If the answer is “Yes,” you see the rule information; otherwise, no data is returned.

I’ve installed this on a computer called Win12R2, so can access Windows PowerShell Web Access like this:

https://win12r2/PSG

…where PSG is the name of the web application that was created earlier. When accessing Windows PowerShell Web Access, you sign in with your domain credentials and provide the name of the server to which you will connect.

Your browser will display a Windows PowerShell console with an area at the bottom to type your commands and a results pane above it.

Note Tab completion doesn’t fully work in the Windows PowerShell Web Access console. It works for cmdlet names, but not for items such as environment variables or cmdlet parameters.

The Windows PowerShell Web Access console has an Exit button in the bottom right corner to close the connection. You can then close the browser or connect to another machine.

So far, you’ve exposed the whole of the functionality available through Windows PowerShell to the user via Windows PowerShell Web Access. You may want to limit the activities the user can perform. In this case, you need to create a constrained endpoint, as you saw in the ADPS endpoint created earlier.

To make this available through Windows PowerShell Web Access, you need to create a rule:

Add-PswaAuthorizationRule -RuleName "server02 AD admin only" -ComputerName server02.manticore.org
-UserName manticore\methul -ConfigurationName ADPS

If you test the rule as follows, you will see that the user is granted access only to the ADPS configuration on server02:

Test-PswaAuthorizationRule  -UserName manticore\methul -ConfigurationName * -ComputerName server02

When users sign in to Windows PowerShell Web Access, they must enter the specific endpoint that they want to access. Running Get-Command shows the limited functionality available through the endpoint.

Windows PowerShell Web Access has more options, and you can discover them in the following topic: Install and Use Windows PowerShell Web Access.

I strongly recommend reading this before you implement Windows PowerShell Web Access. You can discover more about configuring remoting and some other edge cases in Chapter 10 of PowerShell in Depth by myself, Don Jones, and Jeffery Hicks.

Bye for now.

~Richard

Thanks, Richard! I invite you to follow me on Twitter and Facebook. If you have any questions, send email to me at scripter@microsoft.com, or post your questions on the Official Scripting Guys Forum. See you tomorrow. Until then, peace.

Ed Wilson, Microsoft Scripting Guy 


PowerTip: Remove PowerShell Web Access Authorization Rules

$
0
0

Summary: Learn how to remove Windows PowerShell Web Access authorization rules.

Hey, Scripting Guy! Question How can I remove Windows PowerShell Web Access authorization rules that are no longer required?

Hey, Scripting Guy! Answer If you can identify the rule, use Remove-PswaAuthorizationRule from the PowerShellWebAccess module:

Remove-PswaAuthorizationRule -Id 1

~or~

Get-PswaAuthorizationRule -RuleName 'server02 AD admin only' | Remove-PswaAuthorizationRule

To remove all rules for a particular server:

Get-PswaAuthorizationRule | where Destination -eq 'manticore\server02' |
Remove-PswaAuthorizationRule

To remove rules based on the configuration name:

Get-PswaAuthorizationRule | where ConfigurationName -eq 'ADPS' | Remove-PswaAuthorizationRule

Recommendation   Use Get-PswaAuthorizationRule, filter the rule (or rules) you want to remove,
then pipe the results to Remove-PswaAuthorizationRule

Remoting Week: Non-Domain Remoting

$
0
0

Summary: Richard Siddaway explains how to use Windows PowerShell remoting to access machines that aren’t in your domain.

Hey, Scripting Guy! Question Hey, Scripting Guy! I’ve just starting learning Windows PowerShell, and I understand how to use it as a scripting language and shell on the local machine. How do I work with remote machines?

—AP

Hey, Scripting Guy! Answer Hello AP,

Honorary Scripting Guy, Richard Siddaway, here today filling in for my good friend, The Scripting Guy. This is the final part of a series of five posts about remoting. To catch up on the previous posts, see:

  1. Remoting Recap
  2. Remoting Sessions
  3. Configuring Remoting
  4. Remoting Security

So far in this series, you have seen how to perform ad hoc remoting to systems by using the ComputerName parameter on various cmdlets. This leads into Windows PowerShell remoting sessions where you create a persistent connection to one or more remote machines. You can use that connection to run multiple commands, and only set up and tear down the connection once, instead of once per command.

Any remote connectivity system needs to be secure and that is certainly true of Windows PowerShell remoting. In the third part of the series, you saw how HTTPS can be used to secure traffic between the remote system and your administration machine. You also saw how the configuration of the endpoints can be modified to give you more control and lock down the endpoint.

Custom endpoints can be used to control who can access the endpoint and what they can do. This includes the Windows PowerShell language features and the cmdlets that are available. You can control a range; for example, you can make available the full language and all the installed cmdlets, or you can create a situation where the admin who is connecting to that endpoint can’t use the Windows PowerShell language and can only run a hand full of cmdlets. You can connect to that endpoint by using the Windows PowerShell tools, or you can use Windows PowerShell Web Access to give you a Windows PowerShell session that is hosted in your browser by accessing that endpoint.

In all of these examples, the assumption is that you are remoting within the domain. You may think that Windows PowerShell Web Access doesn’t fit this model, but the Windows PowerShell Web Access server may be in the domain, so the remoting sessions are between machines in the same domain.

What about the situation where the machines aren’t in the same domain? You can recognize three broad scenarios for non-domain remoting:

  1. Remoting into a domain
  2. Remoting across domains
  3. Remoting in workgroups

Remember that you need network connectivity to the remote machine over the ports that WSMAN will be using. The defaults are HTTP over port 5985 or HTTPS over port 5986.

You have already seen one way to solve the issues around options 1 and 2: use Windows PowerShell Web Access.  It provides a gateway into the domain that can be used to easily access remoting endpoints within that domain in a secure manner.

If the domain to which you are remoting is in the same forest, your environment may be configured to allow the use of your standard credentials. If not, you are back to a situation that can span all three options: you need to remote to a system that isn’t in your current domain. Even the workgroup situation can be regarded as a special case in this scenario.

So what happens if you try to remote to a machine that isn’t in your domain?

£> $cred = Get-Credential

£> $sess = New-PSSession -ComputerName server02

New-PSSession : [server02] Connecting to remote server server02 failed with the following error message : The WinRM client cannot process the request. If the authentication scheme is different from Kerberos, or if the client computer is not joined to a domain, then HTTPS transport must be used or the destination machine must be added to the TrustedHosts configuration setting. Use winrm.cmd to configure TrustedHosts. Note that computers in the TrustedHosts list might not be authenticated. You can get more information about that by running the following command: winrm help

config. For more information, see the about_Remote_Troubleshooting Help topic.

At line:1 char:9

+ $sess = New-PSSession -ComputerName server02

+         ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    + CategoryInfo          : OpenError: (System.Manageme....RemoteRunspace:RemoteRunspace) [New-PSSession], PSRemotin

   gTransportException

    + FullyQualifiedErrorId : ServerNotTrusted,PSSessionOpenFailed

Trying to use an IP address such as the following will generate a similar error:

$sess = New-PSSession -ComputerName 10.10.54.201 -Credential $cred

The issue is that the client machine and the remote machine are not able to mutually authenticate as they would within a domain. The error messages are useful in that they provide a couple of possible answers:

  • Install a SSL certificate, and use HTTPS to connect to the remote machine.
  • Add the machine to the trusted hosts list.

I discussed using HTTPS in Configuring Remoting, so this time I will solve the issue by using the trusted hosts list. The trusted hosts list effectively bypasses the mutual authentication at the machine level. By placing a system in the list, you are explicitly stating that you trust the system and that it is safe to connect to that system.

You can view the contents of the trusted hosts list by accessing the WSMAN provider:

£> Get-Item -Path WSMan:\localhost\Client\TrustedHosts

   WSManConfig: Microsoft.WSMan.Management\WSMan::localhost\Client

Type            Name                           SourceOfValue   Value

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

System.String   TrustedHosts

In this case, the list is empty. Remember that you can also connect to the WSMAN provider on a remote machine by using Connect-WSMan and examine the settings there. Another way is to use the WSMAN cmdlets:

£> Get-WSManInstance -ResourceURI winrm/config/client

cfg              : http://schemas.microsoft.com/wbem/wsman/1/config/client

lang             : en-GB

NetworkDelayms   : 5000

URLPrefix        : wsman

AllowUnencrypted : false

Auth             : Auth

DefaultPorts     : DefaultPorts

TrustedHosts     :

Set-Item can be used to modify the trusted hosts list:

£> Set-Item -Path WSMan:\localhost\Client\TrustedHosts -Value 'server02'

WinRM Security Configuration.

This command modifies the TrustedHosts list for the WinRM client. The computers in the TrustedHosts list might not be authenticated. The client might send credential information to these computers. Are you sure that you want to modify this list?

[Y] Yes  [N] No  [S] Suspend  [?] Help (default is "Y"): Y

The WSMAN provider adds –Concatenate as dynamic parameter to Set-Item.  You can also use –Force to bypass the confirm message:

£> Get-Item -Path WSMan:\localhost\Client\TrustedHosts | fl Name, Value

Name  : TrustedHosts

Value : server02

£> Set-Item -Path WSMan:\localhost\Client\TrustedHosts -Value '10.10.54.201' -Concatenate -Force

£> Get-Item -Path WSMan:\localhost\Client\TrustedHosts | fl Name, Value

Name  : TrustedHosts

Value : server02,10.10.54.201

Again the WSMAN cmdlets are an alternative:

£> $hosts = 'server02,10.10.54.201'

£> Set-WSManInstance -ResourceURI winrm/config/client -ValueSet @{TrustedHosts=$hosts}

cfg              : http://schemas.microsoft.com/wbem/wsman/1/config/client

lang             : en-GB

NetworkDelayms   : 5000

URLPrefix        : wsman

AllowUnencrypted : false

Auth             : Auth

DefaultPorts     : DefaultPorts

TrustedHosts     : server02,10.10.54.201

When you have the correct values in the trusted host list, you need to define a credential to access that machine. It can be a local domain or machine, depending on the scenario. If it’s a local machine, remember to present UserId as <machine name>/userid. Otherwise, Windows PowerShell will think the credential is on your local machine.

$cred = Get-Credential

You then create a session to that remote machine as normal, and use the credential you created:

$sess = New-PSSession -ComputerName server02 -Credential $cred

Invoke-Command -Session $sess -ScriptBlock{Get-Service}

Normally, you can’t create remoting sessions to IP addresses. But because the IP address is in the trusted hosts list, you can do this:

$sess2 = New-PSSession -ComputerName 10.10.54.201 -Credential $cred

Invoke-Command -Session $sess2 -ScriptBlock{Get-Service}

You may see references on the Internet to having a single value in the trusted hosts list of *. This acts as a wildcard character that says trust every machine, everywhere. I have one simple piece of advice: Don’t do it! I don’t do that in a lab environment, not to mention in production.

One possibility to consider is to script the setting of the trusted hosts list and then script the removal of the values after you have done your work. This reduces the security impact of using the trusted hosts file.

If you are working wholly within your network, using the trusted hosts file is a workable solution. If you are accessing a remote machine from outside your environment, consider using Windows PowerShell Web Access.

This concludes my mini-series about Windows PowerShell remoting. You have seen various ways that you can connect to a remote machine, and I made recommendations as to when particular techniques should be used. More information is available in the Windows PowerShell Help files, and you can discover further details about configuring remoting and some other edge cases in Chapter 10 of PowerShell in Depth by myself, Don Jones, and Jeffery Hicks.

Bye for now.

~Richard

Thanks, Richard! This has been an awesome series.

I invite you to follow me on Twitter and Facebook. If you have any questions, send email to me at scripter@microsoft.com, or post your questions on the Official Scripting Guys Forum. See you tomorrow. Until then, peace.

Ed Wilson, Microsoft Scripting Guy 

PowerTip: Use PowerShell to Clear the Trusted Hosts File

$
0
0

Summary: Use Windows PowerShell to clear the trusted hosts file.

Hey, Scripting Guy! Question How can I use Windows PowerShell to clear all values from my trusted hosts file?

Hey, Scripting Guy! AnswerThe trusted hosts file is part of the WSMAN configuration, and the easiest way to clear it is to use Clear-Item:

£> Get-Item -Path WSMan:\localhost\Client\TrustedHosts | fl Name, Value

Name  : TrustedHosts

Value : server02,10.10.54.201

£> Clear-Item -Path WSMan:\localhost\Client\TrustedHosts -Force

£> Get-Item -Path WSMan:\localhost\Client\TrustedHosts | fl Name, Value

Name  : TrustedHosts

Value :

Note  If you don’t use the –Force parameter on Clear-Item, you must respond to a message asking you to confirm the action.

Weekend Scripter: WMF, PowerShell, and Exchange Server Compatibility

$
0
0

Summary: Microsoft PFE and guest blogger, Mike Pfeiffer, talks about Windows PowerShell and Exchange Server compatability.

Microsoft Scripting Guy, Ed Wilson, is here. Today we have a guest blogger who we haven’t seen for a while. Mike Pfeiffer is a premier field engineer for Microsoft. Since we last had Mike as a guest blogger, he has published a new book: Microsoft Exchange Server 2013 PowerShell Cookbook: Second Edition. Take it away Mike…

As you know, the version of the Windows Management Framework (WMF) that is installed on your server determines which versions of Windows PowerShell you can use. One of the most common questions I’ve been asked since the release of WMF 3.0 is whether it can be used with Exchange Server 2013 and Exchange Server 2010. Now that WMF 4.0 has been released, I’m sure there will be even more questions, so let’s make sure we’re all on the same page.

Exchange Server 2013

The current version of Exchange Server actually requires WMF 3.0, but supports nothing earlier, and nothing later. This has been the case from RTM, to Cumulative Update (CU) 1, and currently with CU 2. Ideally, you’ll run Exchange Server 2013 on Windows Server 2012, which includes WMF 3.0 by default. You also have the option of running Exchange Server 2013 on Windows Server 2008 R2 with WMF 3.0 installed. Support for WMF 4.0 will come for Exchange Server 2013 in a later update.

Exchange Server 2010

Service Pack 3 for Exchange Server 2010 added support for installations on Windows Server 2012. Of course, this version of Windows Server  includes WMF 3.0. However, Exchange Server 2010 SP3 installed on Windows Server 2012 only utilizes Windows PowerShell 2.0. Keep in mind that Exchange Server 2010 SP3 has been verified as compatible only with WMF 3.0 on Windows Server 2012 installations. There has been no testing with ExchangeServer 2010 SP3 and WMF 3.0 installed on Windows Server 2008 R2.

Hopefully this clears things up a bit. Things are moving fast these days, as I’m sure you’ve noticed. Make sure you watch the Exchange team blog for updates. In addition to Exchange, there are other products that are not compatible with WMF 4.0 and WMF 3.0. Take a look at these posts on the Windows PowerShell team blog for full details:

Thank you, Mike, for sharing your time and knowledge.

I invite you to follow me on Twitter and Facebook. If you have any questions, send email to me at scripter@microsoft.com, or post your questions on the Official Scripting Guys Forum. See you tomorrow. Until then, peace.

Ed Wilson, Microsoft Scripting Guy 

PowerTip: Find PowerShell Version

$
0
0

Summary: Easily find your Windows PowerShell version.

Hey, Scripting Guy! Question Is there a single command I can use to find the version of Windows PowerShell I am running, plus the version of
          WSMan and the .NET Framework command-line reference (CLR)?

Hey, Scripting Guy! Answer Use the $PSVersionTable automatic variable to display the Windows PowerShell version,
          WSMan stack information, .NET Framework CLR version information, and other information:

PS C:\> $PSVersionTable

Weekend Scripter: Determine Process that Locks a File

$
0
0

Summary: Guest blogger, Adam Driscoll, talks about using Windows PowerShell to determine which process is locking a file.

Microsoft Scripting Guy, Ed Wilson, is here. Today we have a guest blogger, Adam Driscoll...

It’s quite common to run into the issue where a process is locking a particular file. Attempting to delete or open the file can result in an error message when another process has exclusive access to the file. Unfortunately, Windows PowerShell nor the .NET Framework expose a simple way of determining which process is locking the file. Determine which process is locking the file requires quite a few steps to interrogate the system. This can be accomplished by using a set of Windows API calls and Platform Invoke (PInvoke) functionality. In this post, we’ll take a tour of what it takes to identify the process that is locking a file.

The first step is to understand how Windows manages files and other objects. When opening a file in Windows, the kernel provides a handle to the calling process to identify the opened file. A handle is a kernel-level identifier to an open file or other Windows object. There are handles for all types of Windows objects, including files, events, and mutexes.

To view the open handles on the system, we need to utilize the NtQuerySystemInformation function. This function is largely undocumented, and it requires some intimate knowledge to use correctly. The first step is to call several Win32 functions and structures that we will need for this operation. This can be accomplished by using the Add-Type cmdlet. The functions involved include:

NtQuerySystemInformation: Amongst other things, can query the handles open on the system.

NtQueryObject: Queries additional information about a handle.

OpenProcess: Allows us to get more information about the process that owns the handle.

DuplicateHandle: Creates a copy of a handle in the current process (for example, ISE) so we can perform additional operations on it.

QueryDosDevice: Converts a logical drive (such as drive C) into the DOS device identifier.

SystemHandleEntry: Describes the memory structure of handle information that is returned by NtQuerySystemInformation.

For more information about the signatures that are required to use these functions, refer to PInvoke.net.

Now we need to define the PInvoke functions. This has been eliminated in this document for brevity, but it is included in the script posted in the Script Center Repository: Find-LockingProcess.

The next step is to query the handles that are currently open on the system. The following snippet, which is part of a larger function, queries the open handles on the system. We pass 16 into the NtQuerySystemInformation function to signify that we want it to return open handles. We then pass in IntPtr, which will hold the information return by the system.

The IntPtr object is used to hold the address of a chunk of memory that has been provided or allocated to us. The first time we fall through the while loop, it returns the size of the data that we need and then we allocate it by using the AllocHGlobal function. This returns a segment of memory large enough to hold the data.

The IntPtr object points to the beginning of that segment of memory. The next time we cycle through the loop, we have enough memory available to store all the handle data and NtQuerySystemInformation fills our memory segment with that data.

        while ($true)

        {

            $ptr = [System.Runtime.InteropServices.Marshal]::AllocHGlobal($length)

            $wantedLength = 0

                 $SystemHandleInformation = 16

            $result = [NtDll]::NtQuerySystemInformation($SystemHandleInformation, $ptr, $length, [ref] $wantedLength)

            if ($result -eq [NT_STATUS]::STATUS_INFO_LENGTH_MISMATCH)

            {

                $length = [Math]::Max($length, $wantedLength)

                [System.Runtime.InteropServices.Marshal]::FreeHGlobal($ptr)

                $ptr = [IntPtr]::Zero

            }

            elseif ($result -eq [NT_STATUS]::STATUS_SUCCESS)

                 {

                break

                 }

            else

                 {

                throw (New-Object System.ComponentModel.Win32Exception)

                 }

        }

From here, we can convert the handle information into a .NET structure that can be used easily in Windows PowerShell—much like any other object. We need to loop numerous times because we have multiple SystemHandleEntry structures to create—one for each open handle. We actually create the structures by using the Marshal.PtrToStructure method in the .NET Framework.

You may have noticed that we are incrementing an offset by using the size of the SystemHandleEntry. This is because we have a big blob of data, and we need to move sequentially down it to get each SystemHandleEntry. You can think of this as a big row of Lego blocks. We know the size of one block and instruct the PtrToStructure method to where the beginning of the each block is. It breaks that block off and returns it as a SystemHandleEntry.

Note that I am evaluating whether the ObjectTypeNumber is 31. This is the file object type number for Windows 8. Other operating systems have different identifying numbers. To reduce the size of this post, I’ve eliminated the logic to determine that number, but it is possible to do so dynamically.

$offset = [IntPtr]::Size

$size = [System.Runtime.InteropServices.Marshal]::SizeOf([SystemHandleEntry])

for ($i = 0; $i -lt $handleCount; $i++)

{

[SystemHandleEntry]$FileHandle = [System.Runtime.InteropServices.Marshal]::PtrToStructure([IntPtr]([long]$ptr + $offset), [SystemHandleEntry])

 

if ($FileHandle.ObjectTypeNumber -eq 31)

{

$FileHandle | ConvertTo-HandleHashTable

}

$offset += $size

}

We have also defined a ConvertTo-HandleHashTable function. The purpose of this function is to convert the handle information into a useful hash table that will contain the path to the file and a System.Diagnostics.Process object. This is much more useful than the memory addresses and process IDs that currently populate the SystemHandleEntry structures that we just created.

Inside this function, we need to duplicate the handle returned by NtQuerySystemInformation. If we do not create a copy of the handle, we’ll be unable to query additional information about it. We use OpenProcess to get a handle to the process that owns the original handle. This is necessary for the DuplicateHandle function. The DuplicateHandle function is what provides us with a malleable handle inside our process.

$sourceProcessHandle = [IntPtr]::Zero

$handleDuplicate = [IntPtr]::Zero

$currentProcessHandle = (Get-Process -Id $Pid).Handle

$sourceProcessHandle = [Kernel32]::OpenProcess(0x40, $true, $HandleEntry.OwnerProcessId)

 

if (-not [Kernel32]::DuplicateHandle($sourceProcessHandle,

  [IntPtr]$HandleEntry.Handle,

  $currentProcessHandle,

  [ref]$handleDuplicate,

  0,

  $false,

  2))

{

     return

}

Next, we use the NtQueryObject function to get additional information about the handle. We use the duplicate handle because we will not have the proper rights on the original handle. In this case, we are calling NtQueryObject with the ObjectNameInformation flag. This tells NtQueryObject that we want the name of the handle. In regards to a file handle, this is the path to the file.

$length = 0

[NtDll]::NtQueryObject($handleDuplicate,  

                       [OBJECT_INFORMATION_CLASS]::ObjectNameInformation,

                       [IntPtr]::Zero,

                       0,

                       [ref]$length) | Out-Null

$ptr = [IntPtr]::Zero

 

$ptr = [System.Runtime.InteropServices.Marshal]::AllocHGlobal($length)

if ([NtDll]::NtQueryObject($handleDuplicate,

                           [OBJECT_INFORMATION_CLASS]::ObjectNameInformation,

                           $ptr,

                           $length,

                           [ref]$length) -ne [NT_STATUS]::STATUS_SUCCESS)

{

     return

}

When we have the name as an IntPtr object, we need to convert it into a .NET string. To do this, we use the Marshal.PtrToStringUni method. It converts a memory address into a useable .NET string.

$Path = [System.Runtime.InteropServices.Marshal]::PtrToStringUni([IntPtr]([long]$ptr+ 2 * [IntPtr]::Size))

Finally, we create a new PSCustomObject that contains the path to the file and the Process object that owns the handle in question. You can see that we are using a ConvertTo-RegularFileName function. This is due to the fact that the name returned by NtQueryObject will be in the DOS-style format. This format looks similar to: \Device\HarddiskVolume1\myfile.txt.

Performing a comparison on a path like this would likely fail later. The result of ConvertTo-RegularFileName will instead be in this format: C:\MyFile.txt.

                        [PSCustomObject]@{

                                    Path=(ConvertTo-RegularFileName $Path);

                                    Process=(Get-Process -Id $HandleEntry.OwnerProcessId);

                        }

The ConvertTo-RegularFileName function uses the QueryDosDevice function to convert a logical drive (such as drive C) into a DOS style drive (for example, \Device\HarddiskVolume1\). We use Environment::GetLogicalDrives to return all the drives on the current machine. Then we convert them to DOS style and fix up the raw path that is returned by NtQueryObject.

foreach ($logicalDrive in [Environment]::GetLogicalDrives())

{

       $targetPath = New-Object System.Text.StringBuilder 256

if ([Kernel32]::QueryDosDevice($logicalDrive.Substring(0, 2), $targetPath, 256) -eq 0)

{

       return $targetPath

 }

        $targetPathString = $targetPath.ToString()

        if ($RawFileName.StartsWith($targetPathString))

        {

            $RawFileName = $RawFileName.Replace($targetPathString,           

                                                $logicalDrive.Substring(0, 2))

            break

        }

}

$RawFileName

This all comes together in the most useful of the functions, Find-LockingProcess. This advanced function accepts a file info object (as returned by Get-ChildItem), or a raw path, and it finds handles that are open for that path. When it finds the handle, it returns the Process object for the locking file.

function Find-LockingProcess

{

            [CmdletBinding()]

            param(

            [Parameter(ValueFromPipeline=$true,ParameterSetName="Pipeline",Mandatory)]

            [System.IO.FileInfo]$InputObject,

            [Parameter(ValueFromPipeline=$true,ParameterSetName="Path",Mandatory)]

            [String]$Path

            )

 

            Begin {

            $Handles = Get-FileHandle

            }

 

            Process

            {

                        if ($InputObject)

                        {

                                    $Handles | Where-Object { $_.Path -eq $InputObject.FullName } | Select-Object -ExpandProperty Process

                        }

 

                        if ($Path)

                        {

                                    $Handles| Where-Object { $_.Path -contains $Path } | Select-Object -ExpandProperty Process

                        }

            }

}

To use this function, we could then specify a full file path. We will get output that references all the processes that have a handle open to the file. It’s worth noting that not all the process may be locking the file, but only a process that currently has a handle can be locking it. In the following example, Steam is locking the file and devenv is not.

Find-LockingProcess -Path "E:\Program Files (x86)\Steam\steam.log"

Handles  NPM(K)    PM(K)      WS(K) VM(M)   CPU(s)     Id ProcessName                                                                                                                                 

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

    685      85   218692      37356   536   116.84   4420 Steam                                                                                                                                        

  73285     248   279136     366676   887   550.61   3344 devenv                                                                                                                                       

  74622     249   267796     359196   904   550.68   3344 devenv                                                                                                                                    

  75959     246   282880     369672   881   550.76   3344 devenv                                                                                                                                   

  88123     247   307540     389004   897   551.17   3344 devenv   

The full script can be found in the Script Center Repository: Find-LockingProcess. You will likely need to run your Windows PowerShell host as an administrator to successfully execute this script.

~Adam

Thank you, Adam, for taking your time to share with our readers.

I invite you to follow me on Twitter and Facebook. If you have any questions, send email to me at scripter@microsoft.com, or post your questions on the Official Scripting Guys Forum. See you tomorrow. Until then, peace.

Ed Wilson, Microsoft Scripting Guy

PowerTip: Find User Name for a Process

$
0
0

Summary:  Use Windows PowerShell to find the user who owns a process.

Hey, Scripting Guy! Question How can I use Windows PowerShell 4.0 in Windows 8.1 to determine who owns a process?

Hey, Scripting Guy! Answer Use the –IncludeUserName switch with the Get-Process cmdlet:

Get-Process -IncludeUserName


Getting Started with PowerShell: The Pipeline

$
0
0

Summary: Microsoft Scripting Guy, Ed Wilson, shares an excerpt from his new Windows PowerShell book.

Microsoft Scripting Guy, Ed Wilson, is here. Today, I have an excerpt from my recently published book, Windows PowerShell 3.0 First Steps. This book is published by Microsoft Press.

Image of book cover

The Windows PowerShell pipeline takes the output from one command, and sends it as input to another command. By using the pipeline, you are able to do things such as find all computers in one specific location and restart them. This entails two commands:

  1. Find all the computers in a specific location
  2. Restart each of the computers

Passing the objects from one command to a new command makes Windows PowerShell easy to use inside the console because you do not have to stop to parse the output from the first command before taking action with a second command.

Windows PowerShell passes objects down the pipeline. This is one way that Windows PowerShell becomes very efficient: It takes an object (or group of objects) from the results of running one command, and it passes those objects to the input of another command.

By using the Windows PowerShell pipeline, it is not necessary to store the results of one command into a variable, and then call a method on that object to perform an action. For example, the following command disables all network adapters on my Windows 8 laptop.

Note Windows PowerShell honors the Windows security policy. Therefore, to disable a network adapter, you must run Windows PowerShell with Admin rights.

Get-NetAdapter | Disable-NetAdapter

In addition to disabling all network adapters, you can enable them. To do this, use the Get-NetAdapter cmdlet and pipe the results to the Enable-NetAdapter cmdlet, as shown here:

Get-NetAdapter | Enable-NetAdapter

If you want to start all of the virtual machines on a computer running Windows 8 (or on a server running Windows Server 2012), use the Get-VM cmdlet and pipe the resulting virtual machine objects to the Start-VM cmdlet:

Get-VM | Start-VM

To shut down all of the virtual machines, use the Get-VM cmdlet and pipe the resulting virtual machine objects to the Stop-VM cmdlet:

Get-VM | Stop-VM

In each of the previous commands, an object (or group of objects) that results from one command is piped to another cmdlet for further action.

Sorting output from a cmdlet

The Get-Process cmdlet generates a nice table view of process information to the Windows PowerShell console. The default view appears in ascending alphabetical order by process name. This view is useful for helping find specific process information; but it hides important details, such as which process uses the least, or the most, virtual memory.

To sort the output from the process table, pipe the results from the Get-Process cmdlet to the Sort-Object cmdlet and supply the –Property parameter to the property upon which to sort. The default sort order is ascending (that is, the smallest number appears at the top of the list).

The following command sorts the process output by the amount of virtual memory that is used by each process. The processes that consume the least amount of virtual memory will appear at the top of the list.

Get-Process | Sort-Object -Property VM

If you are interested in which processes consume the most virtual memory, you may want to reverse the default sort order. To do this, use the –Descending switch parameter. This command is shown here:

Get-Process | Sort-Object -Property VM –Descending

The command to produce the sorted list of virtual memory processes, and the associated output from the command are shown in the following image:

Image of command output

It is possible to shorten the length of Windows PowerShell commands that use the Sort-Object cmdlet. The command Sort is an alias for the Sort-Object cmdlet. A cmdlet alias is a shortened form of the cmdlet name that Windows PowerShell recognizes as a substitute for the complete cmdlet name. Some aliases are easily recognizable (such as Sort for Sort-Object or Selectfor Select-Object). Other aliases must be learned (such as ? for the Where-Object—most Windows users expect ? to be an alias for the Get-Help cmdlet). 

In addition to using an alias for the Sort-Object cmdlet name, the –Property parameter is the default parameter that the cmdlet utilizes; therefore, it can be left out of the command. The following command uses the shortened syntax to produce a list of services by status.

Get-Service | sort status

It is possible to sort on more than one property. You need to be careful doing this because at times it is not possible to sort additional properties. With the Service cmdlets, a multiple sort makes sense because there are two broad categories of status: Running and Stopped. It therefore makes sense to attempt to organize the output further to facilitate finding particular stopped or running services.

One way to facilitate finding services is to alphabetically sort the DisplayNameproperty of each service. The following script sorts the service objects obtained via the Get-Service cmdlet by the status, and then by the display namefrom within the status. The output appears in descending order instead of the default ascending list order.

Get-Service | sort status, displayname –Descending

The following image shows the command to sort services by status and display name and the output from the command:

Image of command output

Join me tomorrow when I will have another excerpt from my Windows PowerShell 3.0 First Steps book.

I invite you to follow me on Twitter and Facebook. If you have any questions, send email to me at scripter@microsoft.com, or post your questions on the Official Scripting Guys Forum. See you tomorrow. Until then, peace.

Ed Wilson, Microsoft Scripting Guy 

PowerTip: Use PowerShell to Group Processes in Windows

$
0
0

Summary: Use Windows PowerShell to group the numbers of processes in Windows.

 

Hey, Scripting Guy! Question How can I use Windows PowerShell to determine the number of processes that are running—
          for example, if I notice that there are multiple copies of the same processes running?

Hey, Scripting Guy! Answer Use the Get-Process cmdlet, group the output on the Name property, and sort the count
          where there is more than one instance of the process.

In Windows PowerShell 4.0 or Windows PowerShell 3.0:

Get-Process | group name | sort count -Descending | where count -gt 1

In Windows PowerShell 2.0 or earlier:

Get-Process | group name | sort count -Descending | where {$_.count -gt 1}

Getting Started with PowerShell: Text Files

$
0
0

Summary: Microsoft Scripting Guy, Ed Wilson, talks about using Windows PowerShell to work with text files.

Microsoft Scripting Guy, Ed Wilson, is here. Good morning! Today I have an excerpt from my new book, Windows PowerShell 3.0 First Steps, which was recently published by Microsoft Press.

Image of book cover

One of the easiest methods to store data is in a text file. In the following image, the output from the Get-Volume function displays in the Windows PowerShell console. The output formats nicely in columns, and it contains essential information about the volumes on a Windows 8 laptop. 

Image of command output

Redirect and append

The easiest way to store volume information that is obtained from the Get-Volume function is to redirect the output to a text file. Because several lines of information return from the function, it is best to redirect and append the outputted information. The redirect and append operator is two right arrows, one behind the other with no space between them.

The following command redirects and appends the information from the Get-Volume function to a text file that resides in the folder c:\fso. The file, VolumeInfo.txt, does not have to exist. If it does not exist, it will be created, and the information is written to the file. If the file does exist, the outputted data will append to the file. The command is shown here:

Get-Volume >>c:\fso\volumeinfo.txt

When the command runs, nothing outputs to the Windows PowerShell console. The output, formatted as it appears in the Windows PowerShell console, writes to the target text file. The following image shows the volumeinfo.txt file that was created by redirecting and appending the results of the Get-Volume function from Windows 8.

Image of command output

If you run the command that redirects and appends the information from the Get-Volume function to a text file that resides in the folder c:\fso named volumeinfo.txt a second time, the information from Get-Volume writes to the bottom of the previously created text file—that it, it appends to the file. This is a great way to produce simple logging. The following image shows the volume information appearing twice. In both cases, the values are identical. This shows that between the first time the Get-Volume command ran and the second time Get-Volume ran, nothing changed. 

Image of command output

Redirect and overwrite

If you do not need to maintain a history of prior settings, results, or data, use the redirect operator and do not append. The redirect and overwrite operator is a single right arrow. The following command redirects and overwrites the information from the Get-Volume function to a text file that resides in the folder c:\fso. The file, VolumeInfo.txt, does not have to exist. If it does not exist, it will be created, and the information is written to the file. If the file does exist, the outputted data will overwrite previously existing data when writing to the file. The command is shown here:

Get-Volume >c:\fso\volumeinfo.txt

Comparing the SizeRemainingvalue of drive Cfrom the following image with the SizeRemainingvalue of the drive C in the previous image reveals that the drive suddenly has nearly 4 GB of additional free space. But because the volumeinfo.txt file is overwritten by the redirect and overwrite operator, you would have no way to discover this condition unless you had a backup of the previous volumeinfo.txt file. Knowing when 4 GB of disk space suddenly became available might help the Help Desk when a user calls and says, “All of a sudden, Outlook is not working.”

Image of command output

Join me tomorrow when I will have another excerpt from my Microsoft Press book, Windows PowerShell 3.0 First Steps.

I invite you to follow me on Twitter and Facebook. If you have any questions, send email to me at scripter@microsoft.com, or post your questions on the Official Scripting Guys Forum. See you tomorrow. Until then, peace.

Ed Wilson, Microsoft Scripting Guy 

PowerTip: Use PowerShell to Write Process Information to Text File

$
0
0

Summary: Use Windows PowerShell to write process information to a text file.

Hey, Scripting Guy! Question How can I use Windows PowerShell to document information about currently running processes by writing the information to a text file?

Hey, Scripting Guy! Answer Use the Get-Process cmdlet and pipe the results to the Out-File cmdlet:

Get-Process | Out-File -FilePath c:\fso\process.txt

Getting Started with PowerShell: The Certificate Provider

$
0
0

Summary: Microsoft Scripting Guy, Ed Wilson, talks about using the Windows PowerShell Certificate provider.

Microsoft Scripting Guy, Ed Wilson, is here. Today I have an excerpt from my new Microsoft Press book, Windows PowerShell 3.0 First Steps.

Image of book cover

To find information about the Windows PowerShell Certificate provider, use the Get-Help cmdlet. If you are unsure what topics in Help may be related to certificates, you can use the wildcard character asterisk (*) parameter. This command is shown here:

Get-Help *cer*

The Certificate provider gives you the ability to sign scripts, and it allows Windows PowerShell to work with signed and unsigned scripts. It also gives you the ability to search for, copy, move, and delete certificates. With the Certificate provider, you can open the Certificates Microsoft Management Console (MMC) by using the Invoke-Item cmdlet. The following command illustrates this technique:

Invoke-Item cert:

Note  The Certificate provider does not load by default. The module that contains the Certificate provider, Microsoft.PowerShell.Security, does not automatically import into every session. To use the Cert: drive, use the Import-Module cmdlet to import the module, or run a command that uses the Cert: drive, such as a "Set-Location Cert:" command.

Searching for specific certificates

To search for specific certificates, you may want to examine the Subject property. For example, the following command examines the Subject property of every certificate in the CurrentUserstore, beginning at the rootlevel. It does a recursive search, and returns only the certificates that contain the word test in some form in the Subject property. This command and its associated output are shown here:

PS C:\Users\administrator.IAMMRED> dir Cert:\CurrentUser -Recurse | ? subject -match
'test'

    Directory: Microsoft.PowerShell.Security\Certificate::CurrentUser\Root


Thumbprint                                Subject
----------                                -------
8A334AA8052DD244A647306A76B8178FA215F344  CN=Microsoft Testing Root Certificate A...
2BD63D28D7BCD0E251195AEB519243C13142EBC3  CN=Microsoft Test Root Authority, OU=Mi...

Deleting these testcertificates simply requires piping the results of the previous command to the Remove-Item cmdlet.

Note  When you perform any operation that may alter system state, it is a good idea to use the Whatif parameter to prototype the command prior to actually executing it.

The following command uses the Whatif parameter from Remove-Item to prototype the command to remove all of the certificates from the CurrentUser store that contain the word test in the Subjectproperty. After completion, retrieve the command via the Up arrow and remove the Whatif switched parameter from the command prior to actual execution. This technique is shown here:

PS C:\Users\administrator.IAMMRED> dir Cert:\CurrentUser -Recurse | ? subject -match
'test' | Remove-Item -WhatIf

What if: Performing operation "Remove certificate" on Target "Item: CurrentUser\Root\
8A334AA8052DD244A647306A76B8178FA215F344 ".

What if: Performing operation "Remove certificate" on Target "Item: CurrentUser\Root\
2BD63D28D7BCD0E251195AEB519243C13142EBC3 ".

PS C:\Users\administrator.IAMMRED> dir Cert:\CurrentUser -Recurse | ? subject -match
'test' | Remove-Item

Finding expiring certificates

A common task in companies that use certificates is to identify certificates that have expired or are about to expire. By using the Certificate provider, it is simple to identify expired certificates. To do this, use the NotAfterproperty from the certificate objects that are returned from the certificate drives. One approach is to look for certificates that expire prior to a specific date, as shown here:

PS Cert:\> dir .\\CurrentUser -Recurse | where notafter -lt "5/1/2012"

A more flexible approach is to use the current date. Therefore, each time the command runs, it retrieves expired certificates. This technique is shown here:

PS Cert:\> dir .\\CurrentUser -Recurse | where notafter -lt (Get-Date)

One problem with simply using the Get-ChildItem cmdlet on the CurrentUserstore is that it returns certificate stores in addition to certificates. To obtain only certificates, you must filter out the psiscontainerproperty.

Because you will also need to filter based on date, you can no longer use the simple Where-Object syntax. The following command retrieves the expiration dates, the thumbprints, and the subjects of all expired certificates. It also creates a table that displays the information. (The command is a single logical command, but it is broken at the pipeline character to permit better display in the book.)

PS Cert:\> dir .\\CurrentUser -Recurse |
where { !$_.psiscontainer -AND $_.notafter -lt (Get-Date)}  |
ft notafter, thumbprint, subject -AutoSize –Wrap

Note  All versions of Microsoft Windows ship with expired certificates to permit verification of old executables that were signed with those certificates. Do not arbitrarily delete an expired certificate or you could cause serious damage to your system.

If you want to identify certificates that will expire in the next thirty days, you use the dynamic parameter –ExpiringInDays from the Get-ChildItem cmdlet. This dynamic parameter adds to the Get-ChildItem cmdlet when it is used on the Cert: drive. The command is shown here:

PS Cert:\> Get-ChildItem -Recurse -ExpiringInDays 30

To produce a useful display, select the Subject and the NotAfter parameters and sort by the NotAfter parameter. Then pipe the output to a table that is autosized and wrapped. The command and its output are shown here:

PS Cert:\> gci -ExpiringInDays 30 -r | select subject, notafter | sort notafter | ft
notafter, subject -a -wr

NotAfter             Subject
--------             -------
2/12/2013 6:34:47 PM
2/16/2013 2:56:37 PM CN=KenMyer@microsoft.com
3/4/2013 4:42:09 PM  CN=Microsoft Corporation, OU=MOPR, O=Microsoft Corporation,
                     L=Redmond, S=Washington, C=US
3/4/2013 4:42:09 PM  CN=Microsoft Corporation, OU=MOPR, O=Microsoft Corporation,
                     L=Redmond, S=Washington, C=US

That is all there is to working with the Certificate provider. Join me tomorrow when I will have another excerpt from my Microsoft Press book, Windows PowerShell 3.0 First Steps.

I invite you to follow me on Twitter and Facebook. If you have any questions, send email to me at scripter@microsoft.com, or post your questions on the Official Scripting Guys Forum. See you tomorrow. Until then, peace.

Ed Wilson, Microsoft Scripting Guy 

PowerTip: Use PowerShell to Get List of Authorized Root Certificates

$
0
0

Summary: Use Windows PowerShell to get a list of authorized root certificates for the current user.

Hey, Scripting Guy! Question How can I examine the authorized root certificates for the current user?

Hey, Scripting Guy! Answer Use the Get-ChildItem cmdlet (dir is an alias) and explore the cert:\CurrentUser\AuthRoot folder:

dir Cert:\CurrentUser\AuthRoot

Getting Started with PowerShell: The basics of WMI

$
0
0

Summary: Microsoft Scripting Guy, Ed Wilson, talks about querying WMI in this excerpt of his book, Windows PowerShell 3.0 First Steps.

Microsoft Scripting Guy, Ed Wilson, is here. Today I have the last excerpt from my new Microsoft Press book, Windows PowerShell 3.0 First Steps.

Image of book cover

In most situations, when you use WMI, you are performing some sort of query. Even when you are going to set a particular property, you still need to execute a query to return a dataset that enables you to perform the configuration. (A dataset includes the data that come back to you as the result of a query, that is, it is a set of data.) There are several steps involved in performing a basic WMI query:

  1. Connect to WMI by using the Get-WMIObjectcmdlet.
  2. Specify a valid WMI class name to query.
  3. Specify a value for the namespace—omit the Namespace parameter to use the default root\cimv2 namespace.
  4. Specify a value for the ComputerNameparameter—omit the ComputerNameparameter to use the default value of LocalHost.

Windows PowerShell makes it easy to query WMI. In fact, at its most basic level, the only thing required is gwmi(alias for the Get-WmiObject cmdlet) and the WMI class name. An example of this simple syntax is shown here, along with the associated output:

PS C:\> gwmi win32_bios
SMBIOSBIOSVersion : BAP6710H.86A.0064.2011.0504.1711
Manufacturer      : Intel Corp.
Name              : BIOS Date: 05/04/11 17:11:33 Ver: 04.06.04
SerialNumber      :
Version           : INTEL  - 1072009

However, there are more properties available in the Win32_Bios WMI class than the five displayed in the previous output. The reason for the limited output that is displayed from the command is a custom view of the Win32_Bios class defined in the types.ps1xml file that resides in the Windows PowerShell home directory on your system. The following command uses the Select-String cmdlet to search the Types.ps1xml file to see if there is any reference to the WMI class Win32_Bios.

Select-String -Path $pshome\*.ps1xml -SimpleMatch "Win32_Bios"

In the following image, several Select-String commands display results when a special format exists for a particular WMI class. The last query (for the Win32_CurrentTime WMI class) does not return any results, indicating that no special formatting exists for this class.

Image of command output

The Select-String queries shown in the previous image indicate that there is a special formatting for the Win32_Bios, Win32_DesktopMonitor, and Win32_Service WMI classes. The Types.ps1xml file provides information to Windows PowerShell that tells it how to display a particular WMI class. When an instance of the Win32_Bios WMI class appears, Windows PowerShell uses the DefaultDisplayPropertySetconfiguration to display only five properties. The portion of the Types.ps1xml file that details these five properties is shown here:

                    <PropertySet>
                        <Name>DefaultDisplayPropertySet</Name>
                        <ReferencedProperties>
                            <Name>SMBIOSBIOSVersion</Name>
                            <Name>Manufacturer</Name>
                            <Name>Name</Name>
                            <Name>SerialNumber</Name>
                            <Name>Version</Name>
                        </ReferencedProperties>
                    </PropertySet>

The complete type definition for the Win32_Bios WMI classis shown in following image:

Image of command output

Special formatting instructions for the Win32_Bios WMI class indicate that there is an alternate property set available—a property set that is in addition to the DefaultDisplayPropertySet. This additional property set, named PSStatus, contains the four properties in the PropertySetshown here:

            <PropertySet>
                <Name>PSStatus</Name>
                <ReferencedProperties>
                    <Name>Status</Name>
                    <Name>Name</Name>
                    <Name>Caption</Name>
                    <Name>SMBIOSPresent</Name>
                </ReferencedProperties>
            </PropertySet>

Finding the PSStatusproperty set is more than a simple academic exercise, because it can be used directly with Windows PowerShell cmdlets such as Select-Object (select is an alias), Format-List (fl is an alias),or Format-Table (ft is an alias). The following commands illustrate this technique:

gwmi win32_bios | select psstatus
gwmi win32_bios | fl psstatus
gwmi win32_bios | ft psstatus 

Unfortunately, you cannot use the alternate property set, PSStatus, to select the properties via the Propertyparameter. Therefore, the following command fails.

gwmi win32_bios -Property psstatus

That is all there is to using Windows PowerShell to query WMI.  Join me tomorrow when I will have a guest post from Yuri Diogenes about configuring your device in Windows Server 2012 R2.

I invite you to follow me on Twitter and Facebook. If you have any questions, send email to me at scripter@microsoft.com, or post your questions on the Official Scripting Guys Forum. See you tomorrow. Until then, peace.

Ed Wilson, Microsoft Scripting Guy 


PowerTip: Use Special Formatting from Types XML File in WMI

$
0
0

Summary: Use special formatting from the types .xml file to choose properties in Windows PowerShell and WMI.

Hey, Scripting Guy! Question How can I use Windows PowerShell to find the status of the BIOS on my computer without viewing the default output?

Hey, Scripting Guy! Answer Use the special PSStatus view when you select properties:

Get-WmiObject win32_bios | select psstatus

Security Series: Using PowerShell to Enable BYOD–Part 1

$
0
0

Summary: Guest blogger and security expert, Yuri Diogenes, talks about using Windows PowerShell to enable BYOD.

Microsoft Scripting Guy, Ed Wilson, is here. Today’s guest blogger is Yuri Diogenes, who is bringing us the beginning of a new security series. Yuri is a senior knowledge engineer, and he is a coauthor of the book Windows Server 2012 Security from End to Edge and Beyond. You can follow him on Twitter at @YuriDiogenes.

Image of book cover

Take it away, Yuri…

The explosion of mobile devices and how users are connecting to resources online changed the way users consume information, and this change has arrived in the enterprise model. In the past, you could easily say that user Bob would always sign in from the UserBobPC computer. This allowed IT to control the computer (device) the way they wanted, and to control the user’s profile—hardening and authorizing, based on the need-to-know security principle.

However, with the demand from users to use their own devices to access corporate information, the concept that one user will always use one device, which is managed by IT, doesn’t fit anymore. This new model is called “bring your own device” (BYOD).

This series of posts will focus in three core BYOD scenarios that use Windows Server 2012 R2 technologies that can be enabled by using Windows PowerShell. If you want to know more about BYOD, read the General Considerations Regarding BYOD section in Bring Your Own Device (BYOD) Survival Guide for Microsoft Technologies.

Scenario 1: Know devices before users access resources

One of the biggest challenges that IT faces with the BYOD model is the lack of control of users’ devices. Even worse, IT has no idea who is bringing the device or if the device is rogue. Before you think about a management solution for user-owned devices, you should ask yourself the following questions:

  • How can I track who is bringing an unmanaged device?
  • How can I authenticate the device according to the user that is bringing the device?
  • How can I track this device?

These simple questions can be answered with the capabilities that are built-in to Windows Server 2012 R2, and rapidly enabled by using Windows PowerShell. To know more about the device, you can leverage the Device Registration Service (DRS) in Windows Server 2012 R2. This registration can be performed from devices that are connected within the corporate network (corpnet) or devices that are connecting from an external network, such as the Internet, as shown here:

Image of flow chart

As part of this registration process, a certificate is installed on the device, and a new device object is created in Active Directory. The following image shows an example of a record in Active Directory:

Image of record

This device object establishes a link between a user and a device, making it known to IT. It allows the device to be authenticated, effectively creating a seamless second-factor authentication. In return for registering their devices and making them known to IT, users gain access to corporate resources that were previously not available outside of their domain-joined computers.

Scenario definition

Contoso IT wants to embrace BYOD. As a first step toward this strategy, they want to understand the footprint of the users’ devices. Initially they want to enable device registration only for devices that are connected through the on-premises corporate network.

The steps to enable device registration by using Windows PowerShell are pretty straightforward. However, there are some prerequisites that must be in place before enabling this capability. Ensure that the following actions were completed before you enable DRS:

  • Install a server certificate on the local computer container of the server that will receive the Active Directory Federation Services (AD FS) server role.
  • Install and configure the AD FS role in the server running Windows Server 2012 R2 (by running the Install-AdfsFarm cmdlet).
  • Configure the Domain Name System (DNS) Server service with an alias CNAME resource record for the AD FS farm and a host (A) resource record for the AD FS nodes.

DRS is installed by running the Install-AdfsFarm cmdlet, and it will use the same service account credentials that are used by AD FS. When the environment is ready, you can prepare Active Directory for DRS by running the following command once for the entire ADFS farm:

Initialize-ADDeviceRegistration -ServiceAccountName contoso\<serviceaccountname>

Following is an example of this operation, where the service account name is adfsfarm:

Image of command output

After you finish this operation, you should run the following cmdlet in each AD FS node in the farm:

Enable-AdfsDeviceRegistration

Following is an example of this operation:

Image of command output

Validate the device

After the operation is complete, you can use the Get-AdfsDeviceRegistration cmdlet to verify the ObjectDN and its parameters, as shown here:

Image of command output

At this point, you can connect a device running Windows 8.1 in the corporate network and use the Workplace Join capability to join and register the device. For more information about this operation, see Walkthrough Guide: Workplace Join with a Windows Device.

To set up your environment for this functionality, you can follow the instructions in Set up the lab environment for AD FS in Windows Server 2012 R2. See you next time!

~Yuri

Thank you, Yuri, for your time and knowledge.

I invite you to follow me on Twitter and Facebook. If you have any questions, send email to me at scripter@microsoft.com, or post your questions on the Official Scripting Guys Forum. See you tomorrow. Until then, peace.

Ed Wilson, Microsoft Scripting Guy

Solving Office Holiday Gift-Giving Conundrums with PowerShell

$
0
0

Summary: Microsoft Scripting Guy, Ed Wilson, talks about a project to assign secret pals at work. He ropes one of the managers into the project.

During my one-on-one last week with my manager, Adina, she mentioned doing something to help people who work from remote locations feel more like part of the team. Because I work remotely, I was all ears. She had a great idea: Assign random secret pals. You have to find out all you can about the secret pal, and then send them something that is unique to them—their interests, hobbies, or tastes. Of course, the present is something small, which also makes it a bit of a challenge. Although, maybe not. I mean, anyone who reads the Hey, Scripting Guy! Blog for very long knows that I enjoy different kinds of teas, and I am especially partial to Anzac biscuits—neither of which need be especially expensive.

So it is that kind of thing. And of course, I was in. Then Adina came up with a real kicker, “I wonder if you could write a Windows PowerShell script to do that?”

So I opened my big mouth and said, “Of course, I could write it. But with Windows PowerShell, the task would be so easy, that even a manager could write it.”

Now I had Adina’s attention. We discussed it a bit, and decided that Michel would be perfect for the task because as he has nearly no experience with Windows PowerShell. I offered, of course, to help with pointers. But before I got too involved, Michel had already solved the task at hand. I now turn you over to Michel LaFantano, a senior managing editor on our team, and I will let him tell you about his experience. Take it away Michel…

Our team at Microsoft includes many remote workers—some who work in non-Redmond Microsoft offices, and some who work from their homes around the country. This helps make for a diverse team, and it also lends a certain “open 24 hours” feeling to our group because there is pretty much always someone awake and online.

But it also means that remote workers rarely get the chance to hang-out in person, and we usually miss the morale events held on the Redmond campus. To help remedy that a bit this year, our director decided to hold a remote worker gift exchange. But how do we pick names from the fish bowl when people are dispersed across ten time zones?

Well, we figured a little Windows PowerShell script might help solve the issue. Being a resourceful kind of guy, I figured I would take a look in the Script Repository (or scriptorium, as I like to call it) and see if I could find something similar to leverage in creating our own “remote-gift-giver-partner-picker” script.

What I found was beyond all expectations, because I found a script that one of our awesome users had written for this exact purpose. Our scripter, who goes by the handle “CathersalCathersalCathersal,” wrote it for his family’s Secret Santa event, and it is called the “Virtual Hat.” Take a look:

Virtual Hat Windows PowerShell script

# This script will randomly draw a name out of a virtual "hat."

#

# This script selects two names from an array in a random fashion.

# Because the people will exchange gifts, it is important that no

# person is assigned to themselves.

#

# Instructions: Just change the names below to the names you want and run it.

 

$NameArray = New-Object System.Collections.ArrayList

 

$NameArray.Add("Syed")

$NameArray.Add("Kim")

$NameArray.Add("Sam")

$NameArray.Add("Hazem")

$NameArray.Add("Pilar")

$NameArray.Add("Terry")

$NameArray.Add("Amy")

$NameArray.Add("Pamela")

$NameArray.Add(“Julie")

$NameArray.Add("David")

$NameArray.Add("Robert")

$NameArray.Add("Shai")

$NameArray.Add("Ann")

$NameArray.Add("Mason")

$NameArray.Add("Sharon")

 

$numberOfNames = $namearray.count

$ArrayCopy = $namearray.clone()

 

for($i=0; $i -lt $numberOfNames; $i++)

{

    $NameToRemove = get-random -input $NameArray

    While ( $NameToRemove -eq $ArrayCopy[$i])  # make sure people don't pick themselves.

    {

        $NameToRemove = get-random -input $NameArray

    }

    Write-host $ArrayCopy[$i]"picks $NameToRemove"

    $NameArray.Remove($NameToRemove)  # Remove the name so it doesn't get picked again.

}

When I run the script, the output in the following image appears in the Windows PowerShell ISE:

Image of command output

Simple, yet elegant, don’t you think? And all we had to do to use it was add the names of our remote team members and we were good to go.

We all know that Windows PowerShell is an awesome tool in datacenters and at home, but who knew that it could also save the holidays? Issue solved. Back to you, Ed…

Turn an array into a hash table

While Michel was busy finding a script to use, I had also begun to tackle the issue. I figured it would be pretty simple because the Get-Random cmdlet is a powerful tool. In fact, we use the Get-Random cmdlet when handing out prizes at PowerShell Saturday and at Windows PowerShell User Group meetings. So I am familiar with this tool.

If all I wanted to do was randomize a list of names, that is two lines of code (it could even be one if I do not store the array of names in its own variable). But I wanted a two-column output. At first I thought I could use Format-Wide and specify two columns. But after messing around with it for about a minute, I saw that for some reason, Format-Wide does not like to convert an array into two-column output.

So I decided to convert my array of names into a hash table. Here is the script I wrote to do this:

# ArrayNamesToHashTable.ps1

# ed wilson, msft

# Secret Pals blog posting

# HSG-12-6-13-b

# -----------------------------------------------------------------------------

$names = 'Syed', 'Kim', 'Sam', 'Hazem', 'Pilar', 'Terry', 'Amy', 'Greg',

    'Pamela', 'Julie', 'David', 'Robert', 'Shai', 'Ann', 'Mason', 'Sharon'

 

$rndNames = Get-Random -InputObject $names -Count ($names.count)

$hash = @{} ; $i = 0

Do {

 $hash.Add($rndNames[$i],$rndNames[($i+1)])

 if($i -le $names.count){$i+=2}}

 While ($i -le $names.count-1)

 $hash

When I run it, I get a nice two-column output, as shown in the following image:

Image of command output

So in the end, it took me more time than it did for a manager to “write a script.” All told, I spent about an hour messing around with this script. But I imagine that I had more fun writing my script than Michel did. After all, I used the Windows PowerShell ISE and he used Bing.

You can find the script in the Script Repository: Randomly draw names out of a virtual "hat."

Join me tomorrow when I will have more cool Windows PowerShell stuff.

I invite you to follow me on Twitter and Facebook. If you have any questions, send email to me at scripter@microsoft.com, or post your questions on the Official Scripting Guys Forum. See you tomorrow. Until then, peace.

Ed Wilson, Microsoft Scripting Guy

PowerTip: Use PowerShell to Display Network Adapter Bindings

$
0
0

Summary:  Use Windows PowerShell to display network adapter bindings.

Hey, Scripting Guy! Question How can I use Windows PowerShell in Windows 8 to display network adapter bindings?

Hey, Scripting Guy! Answer Use the Get-NetAdapter cmdlet to retrieve your network adapter, and pipe it to Get-NetAdapterBinding:

Get-NetAdapter -Name ethernet | Get-NetAdapterBinding -AllBindings

Weekend Scripter: Create an HTML Server Report with PowerShell

$
0
0

Summary: Guest blogger, Matthew Kerfoot, talks about using Windows PowerShell to create an HTML server report.

Microsoft Scripting Guy, Ed Wilson, is here. Today I would like to welcome a new guest blogger, Matthew Kerfoot…

Photo of Matthew Kerfoot

I'm a Windows PowerShell enthusiast with strong interests in automating systems administration. I write a blog called The Overnight Admin where I like to share Windows PowerShell scripts, tips, and functions. I also tweet about Windows PowerShell at @mkkerfoot                         

The following function utilizes WMI to retrieve various information from local and remote machines and then outputs that data into a nicely formatted HTML file for easy viewing: Write-HTML.

Keep in mind that PSRemoting must be enabled, and a proper execution policy must be set to run scripts:

Enable-PSRemoting –Force ; Set-ExecutionPolicy RemoteSigned -Force

My Write-HTML function is about 60 lines of code, and I will not be able to touch on everything. My goal is to make the whole script as readable as possible for someone who has little to no experience with Windows PowerShell.

The first line turns this script into a function by bundling all the code into the function Write-HTML{}, which has a Help file. Because this is bundled into a function, all we have to do (after pasting the code into an elevated Windows PowerShell command prompt) is type “Write-HTML” to produce the following report about your local machine:

Image of report

function Write-HTML{

<#

.SYNOPSIS

Creates an HTML file on the Desktop of the local machine full of detailed   system information.

.DESCRIPTION

    Write-HTML utilizes WMI to retrieve information related to the physical hardware of the machine(s), the available `

disk space, when the machine(s) last restarted and bundles all that information up into a colored HTML report.

.EXAMPLE

   Write-HTML -Computername localhost, SRV-2012R2, DC-01, DC-02

   This will create an HTML file on your desktop with information gathered from as many computers as you can access remotely

#>

I’ve added the CmdletBinding attribute to the script, which by itself, allows the use of all common parameters including Verbose, Debug, ErrorAction, ErrorVariable, WarningAction, WarningVariable, OutBuffer, and OutVariable.

However, by adding SupportsShouldProcess=$True into the CmdletBinding attribute, we can now test what this script would do if we ran it—for example, if we type Write-HTML -WhatIf,it won’t make any changes, but it will output to the console what the script would have done.

[CmdletBinding(SupportsShouldProcess=$True)]

param( [Parameter(Mandatory=$false,

ValueFromPipeline=$true)]

[string]$FilePath = "C:\users\$env:USERNAME\desktop\Write-HTML.html",

[string[]]$Computername = $env:COMPUTERNAME,

After some of the more common parameters, there is a parameter called $Css, which is where the CSS code has been added to manipulate the HTML file. This is where we can change the color of the report, and format the table headers and data.

$Css='<style>table{margin:auto; width:98%}

     Body{background-color:Orange; Text-align:Center;}

     th{background-color:black; color:white;}

     td{background-color:Grey; color:Black; Text-align:Center;} </style>' )

The script begins with a verbose output of what this script is going to do.  This will only be shown if the –Verbose or –WhatIf parameter is added after the function. The Begin, Process, and End blocks are not needed, so I like to use these to organize the script. This allows you to break up the sections of your scripts and collapse the sections within the Windows PowerShell ISE.

Begin{Write-Verbose "HTML report will be saved $FilePath"}

Next, the Process section of the script starts. My script has five variables defined within the Process brackets, which allows me to compact all the code after each equals sign into each variable, for example $Hardware, $PercentFree, $Restarted, $Stopped, and $Report. Each variable is accomplishing a specific task and will later be used with the creation of the HTML file.

Let’s examine the first variable within the Process block: $Hardware. First, all WMI information related to Win32_ComputerSystem is queried for each $ComputerName that is specified. By default, this function will only run against the local host, but you can add the -ComputerName  parameter, and specify all the computers you would like the function to run against.

After querying the selected computers, the WMI object is filtered for the objects specified. In this case, it would be: Name, Domain, Manufacture, Model, NumberofLogicalProccessors. But there is one more object that is being specified. This one is a little harder to see—we are also looking for TotalPhysicalMemory.  In this case, I am taking the object and dividing it by 1 GB to give me a more readable output. After it is divided by 1 GB, the output is formatted with {0:N0}, which represents a number in the one’s place or rounded to the nearest whole number (for example, {0:N3} would give you an output like 7.833).

Process{$Hardware=Get-WmiObject -class Win32_ComputerSystem -ComputerName $Computername |

     Select-Object Name,Domain,Manufacturer,Model,NumberOfLogicalProcessors,

     @{ Name = "Installed Memory (GB)" ; Expression = { "{0:N0}" -f( $_.TotalPhysicalMemory / 1gb ) } } |

     ConvertTo-Html -Fragment -As Table -PreContent "<h2>Hardware</h2>" | Out-String

When all the Win32_ComputerSystem information is gathered and filtered with Select-Object, everything is pushed through the pipeline to ConvertTo-Html -Fragment -As Table -PreContent "<h2>Available Disk Space</h2>" | Out-String.

Most of this is pretty straightforward, except the –PreContent parameter, which contains the section’s header information to be displayed above the hardware table. Out-String is the perfect cmdlet for creating HTML documents because it allows Windows PowerShell to convert the objects into an array of strings, yet it will only return one string by default. This gives us the availability to use variables such as $Hardware later in the script within the –Body of the HTML file.

$PercentFree=Get-WmiObject Win32_LogicalDisk -ComputerName $Computername |

     Where-Object { $_.DriveType -eq "3" } | Select-Object SystemName,VolumeName,DeviceID,

     @{ Name = "Size (GB)" ; Expression = { "{0:N1}" -f( $_.Size / 1gb) } },

     @{ Name = "Free Space (GB)" ; Expression = {"{0:N1}" -f( $_.Freespace / 1gb ) } },

     @{ Name = "Percent Free" ; Expression = { "{0:P0}" -f( $_.FreeSpace / $_.Size ) } } |

     ConvertTo-Html -Fragment -As Table -PreContent "<h2>Available Disk Space</h2>" | Out-String

$Restarted=Get-WmiObject -Class Win32_OperatingSystem -ComputerName $Computername | Select-Object Caption,CSName,

     @{ Name = "Last Restarted On" ; Expression = { $_.Converttodatetime($_.LastBootUpTime) } } |

     ConvertTo-Html -Fragment -As Table -PreContent "<h2>Last Boot Up Time</h2>" | Out-String

$Stopped=Get-WmiObject -Class Win32_Service -ComputerName $Computername |

     Where-Object { ($_.StartMode -eq "Auto") -and ($_.State -eq "Stopped") } |

     Select-Object SystemName,DisplayName,Name,StartMode,State,Description |

     ConvertTo-Html -Fragment -PreContent "<h2>Services currently stopped that are set to autostart</h2>" | Out-String

The $Report variable is used to design the body and header of the report. The –Body parameter is where everything comes together—all of the previously created variables are placed within quotation marks so they get resolved. Without quotation marks, the body of the report would say: $Hardware $PercentFree $Restarted $Services $Stopped $Css. This is also where the -Title is defined, which changes what the browser tab will say when you view the report.

$Report=ConvertTo-Html -Title "$Computername" `

     -Head "<h1>PowerShell Reporting<br><br>$Computername</h1><br>This report was ran: $(Get-Date)" `

     -Body "$Hardware $PercentFree $Restarted $Services $Stopped $Css"}

The End block is where everything should come together and get cleaned up or formatted. Here, we’re taking the contents of $Report,which has already been converted to HTML,and piping it to the $FilePath (your desktop) specified at the beginning of the script. When complete, Invoke-Expression $FilePath will open the file in your default browser.

End{$Report | Out-File $Filepath  ; Invoke-Expression $FilePath  } }

Remember to paste the code into an elevated command prompt, and you can test the script first with Write-HTML –WhatIf.

You can download the whole function from the Script Center Repository: How to make an HTML server report with PowerShell or on SkyDrive: Write-HTML.

I invite you to follow me on Twitter, Facebook, and Google +. If you have any questions, feel free to email me at mkkerfoot@gmail.com.

~Matthew

Thanks for sharing your time and knowledge, Matthew. I invite you to follow me on Twitter and Facebook. If you have any questions, send email to me at scripter@microsoft.com, or post your questions on the Official Scripting Guys Forum. See you tomorrow. Until then, peace.

Ed Wilson, Microsoft Scripting Guy 

Viewing all 2129 articles
Browse latest View live


Latest Images

<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>