Writing a Active Directory Audit Module - Getting Forest Info
In the last blog post we covered setting the goals for the project, general guidelines, how I set up a project in GitHub and the creation of the module manifest. In this blog post we will cover some of the API around ActiveDirectory that we can use in Windows PowerShell to access and query it either from a host already in the domain or with alternate credentials against a specific host.
Currently when working in Windows PowerShell there are 4 main ways to interact with Active Directory:
- ActiveDirectory module - gets installed with RSAT or when then Domain Controller role is added to a server. Varies per version of Windows.
- System.DirectoryServices Namespace - it is a .Net wrapper around the ADSI (Active Directory Service Interface) COM object. It represents a specific path or Object in AD allowing for the pulling of information and modification.
- System.DirectoryServices.ActiveDirectory namespace - It provides several .Net classes that abstract AD services. Provides access to manipulating forest, domain, site, subnet, partition, and schema are part of the object model.
- System.DirectoryServices.AccountManagement namespace provides uniform access and manipulation of user, computer, and group security principals
Each one of the namespaces have their own peculiarities and uses. The most powerful one is classes under System.DirectoryServices do to the control it provides but with it comes more complexity, this is why it is used for those cases where the other 2 do not fit a specific role or complex searches of AD are required.
AD is a hierarchical structure where you have a root domain and multiple subdomain forming trees and this collection of trees is called a forest.
In the specific case of getting forest information we will use the System.DirectoryServices.ActiveDirectory namespace in specific the class Forest that has 2 methods called GetForest() and GetCurrentForest(). The GetForest() method will get the forest for a specific "DirectoryContext", the context is identifies a specific directory and the credentials that are used to access the directory. You remember that in our initial blog post of the series we set as one of our requirements that we had to be able to provide alternate credentials and connect from a host not in the domain. The GetCurrentForrest() will get the forest instance for the current context the process is running under so the domain the host is a member of and use the current user credentials.
When we look at the DirectoryContext class we can see that it has several constructor syntaxes we can use to create a instance of it. A context type is required on all. The context type is the type of connection we will do to the directory service. They are shown in the table bellow
We now have some of the basics that we can use to start creating our first function. One of the things that is being adopted by the community is the creation of tests using the Pester module so we will start by using it to create our first function. The module has great documentation in its Wiki but I highly recommend the Pester series in PowerShell Magazine. Pester comes with Windows PowerShell 5, if you are running Windows 7 with any other version of Windows PowerShell above 2.0 you can follow the instructions in the wiki or articles to get started.
Creating the Get-DSForest Function and Tests
We will start by creating the function file and test for it using the New-Fixture cmdelt. We call the cmdlet and give it the name of the function we want to create, in this case Get-DSForest and hit enter. This will create the template function script file and the function test script for us.
We can take a look at both file in the PowerShell ISE, we will see in the Get-DSForest.ps1 that it will contain an empty function with the same name as the file
When we open Get-DSForest.Test.ps1 we get a base test that we will need to modify to satisfy our needs for the specific function.
We can invoke the test by simply running Invoke-Pester in the current folder where the script and test file are present. We can see the test fails.
In Behavior Driven Development (BDD) we set first our test on how our code will behave and then code to that.
Before we modify our test we must define what will the behavior of the function will be. This would be:
- With no parameters get the forest for the current domain the machine is a member of.
- With only the forest name get that forest using the current user access.
- With forest and credentials get the forest using the alternate credentials.
- With a domain controller and credentials specified it will get the forest the domain controller is a member of (remember the consultant with a machine not in the domain).
The constructor does not let me specify a domain controller, alternate credentials and a alternate forest the forest the DC is a member of has has a trust relation with. In all cases I should get back a System.DirectoryServices.ActiveDirectory.Forest object, so my test should assert that any parameter set I choose will return a Forest object.
I personally prefer to start by setting the parameter sets of the script and testing those first. Here I created the parameters I will need based on the behaviors I defined above and set the proper names and if they are mandatory or not.
When creating a credentials parameter you should always initialize it with an empty object and set the [Management.Automation.CredentialAttribute()] so you can specify a username in the parameter and PowerShell will be smart enough to know that it should take that as a username and ask for the password for it if a PSCredetial object is not passed in.
I can test them by dot sourcing the script file and calling help on it to make sure I have 3 different syntaxes.
Now I can create a switch statement inside my process block so as to execute the proper code block depending of the parameters set, this saves me from a bunch of If, ifelse and else statements to validate parameters to decide what code to execute.
On our test script we add the parameters to it without the parameter sets, we want to run a mix of test depending if we pass the appropriate parameters.
Getting Current Forest
Lets start with the case where the machine is joined to a domain and we want to get the forest it belongs to. First we will need to actually check our code is running on a machine that is domain joined, for this we will need to create a simple assembly that we will load to call the Netapi32.dll call for NetGetJoinInformation. This type is added and it will return either a 3 if joined or another value if it could not determine if it is joined to a pointer we specify , if not we return a terminating error.
So we first set our test so we can make sure we will get the results we want by creating 2 test cases one that gets the current forest and another that will test the error message in the case the machine is not domain joined.
So our test checks if the machine is joined or not to the domain if it is it will run the function and check the full name of the object returned to make sure it is a forest object. If the machine is not joined to the domain it will run the function and expect it to throw an error and return a specific error message. One thing to notice is that when testing for value types I put the expression inside of parenthesis and when testing for the message in a error that was thrown I put the expression in a script block. Do keep this in mind when writing other test. Took me a couple almost 20 minutes to figure this problem in a test passing when it was supposed to fail.
We now build the function in the parameter set block for when ran with no parameter and we add a similar logic as with the test.
When we run the function on a machine that is not domain joined we get the error we set
When we run the function on a domain joined machine we get the forest object
If we run our test with Invoke-Pester with no parameters it should return that the test passed.
Remote Connection to Get Forest
We will now connect to a remote domain controller from a machine that is not joined to the domain, provide credentials and connect to to it to get the forest it forms a part of.
Since we know the 2 parameters it need for a remote connection are we can write our test where we check if the 2 parameters have been set and if so run the function and check if the type of the returned object is a Forest object.
I first create an array of the arguments I need to create the context object. Since I;m connecting to a remote domain controller my context type will de one of DirectoryServer. With the context once created I will call the GetForest() method with it.
When I run now the function and specify the Domain Controller and give it the proper credentials I get the forest object
Now one thing we have to be very careful with is that when we specify the domain controller we need to use the fully qualified domain name, if we specify an IP some information is not populated like the sites and if Kerberos is being used it will fail since it is depended on a FQDN for the connection,
When we run the test this time to pass parameters for the script to take action on we must pass a Hash to the -Script parameter with a Path key with a value to the path with the test, Parameters key with a value of a hash table that contains each of the parameters we want to pass.
Alternate Forest
Now we will work on the code to get the a forest object we specify for and use alternate credentials. But first we have to add to out test script the logic to check the parameters and choose the correct way of running the test when the proper credentials are passed. In both cases we are expecting the user to provide a ForestName parameter but the credentials are optional so we check for ForestName and if the Credential parameter are set or not. The execution is similar to previous tests we check that we get the correct object.
On out function we now mimic the same logic as in the test. In the code block of the switch we will check if have a credential with a null user in the object or not so as to setup the proper context object, since this is a forest connection for a host already in the domain the context type is of Forest.
We can test by specifying the forest and by providing credentials.
Lest run our test like before but this time we will run it from a machine in the domain and pass the proper arguments to get a forest by name with alternate credentials and without them.
Adding Help Comment Block
Before we add the loading of the function in to the main module we need. My main goal when creating help for the module is to initially have it as a comment help block and always to include:
- Synopsis
- Description
- Parameters
- Examples
- Output
- Notes (If needed)
Bellow is how it would look.
We can dot source the file and we can query the general help information to ensure it was properly parsed
We can also take a look at the examples
Adding the Function to the Module
Now we add the function to the module. The simplest way is to add it as a dot source to the main module file ADAudit.psm1.
Once we have added the sourcing of the function file we can then go and in the module manifest find the entry for exported functions and remove the * wildcard and enter an array with the function name we want to export.
We can now use Import-Module cmdlet to load the manifest and give it the the verbose parameter to see all actions it is taking, we should see the function being imported. To confirm we can use the Get-Command cmdlet and specify the module name.
As always I hope you have found the blog post useful and in the next one we will expand the information on the existing module by first creating another function that will allow us to use DirectoryEntry to get the extra information we want.