Fiewport 2026.8.21

dotnet add package Fiewport --version 2026.8.21
                    
NuGet\Install-Package Fiewport -Version 2026.8.21
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Fiewport" Version="2026.8.21" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Fiewport" Version="2026.8.21" />
                    
Directory.Packages.props
<PackageReference Include="Fiewport" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Fiewport --version 2026.8.21
                    
#r "nuget: Fiewport, 2026.8.21"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Fiewport@2026.8.21
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Fiewport&version=2026.8.21
                    
Install as a Cake Addin
#tool nuget:?package=Fiewport&version=2026.8.21
                    
Install as a Cake Tool

This is Fiewport, an F# PowerView port.

(Yes, the name is silly.)

Fiewport is a library intended for assisting pentesters with enumerating and manipulating information from Microsoft Active Directory environments.

Under the hood, Fiewport speaks LDAP itself: it ships its own wire-protocol implementation, performing BER/TLV encoding and decoding of search requests, results, referrals, and paging, rather than leaning on an external LDAP client. Authentication is handled by the Fauli library, which performs the Kerberos and NTLM SASL binds. This combination lets Fiewport run natively on both Linux and Windows. It also supports LDAPS.

Scripts

Fiewport is intended to be used from inside F# script files (.fsx). The reason comes from Fiewport's roots.

Much Active Directory tooling comes from PowerShell, not least of which is the titular PowerView. Part of the appeal of PowerView is that by leveraging the shell, commands can be piped into one another to sort and manipulate results.

I wanted to directly support that flexibility, and nothing pipes better than F#. (Fight me.)

Configuration

A short demonstration of Fiewport in a script might look like this:

#r "nuget: Fiewport"
#r "nuget: Fauli"
#r "nuget: EluciusFTW.SpectreCoff"
#r "nuget: MessagePack.FSharpExtensions"

open Fiewport

let config =
    { ldapDetails = 
        { properties = [||]
          filter = ""
          ldapDN = "DC=northernkingdoms,DC=local"
          scope = SearchScope.Subtree
          ldapHostname = ""
          ldapIP = "192.168.10.38"
          ldapPort = 389
          useSsl = false }
      credentials = 
      { username = "samwell.tarly@northernkingdoms.local"
        password = "Heartsbane" } }
    

[config]
|> Searcher.getUsers
|> PrettyPrinter.print

As it suggests, this will create a connection to an active directory located at 192.168.10.38 with the provided credentials. Supply ldapHostname (FQDN), ldapIP, or both — use "" when unknown. With both set, Fiewport uses the IP as the Kerberos KDC address and the hostname for the LDAP SPN / session; IP-only falls through to NTLM when no ticket material is available. Fiewport does not assume your computer is joined to the AD you want to examine, so this information is necessary. It also allows you to control what user(s) you choose to enumerate with.

The config.filter is where you may influence the LDAP filter used during the canned searches. Please read the comments on each Searcher method, because filters are sometimes OR-ed, sometimes ANDed, and sometimes ignored completely.

ℹ️ If you want complete control over your search, see Searcher.getDomainObjects

Additionally, putting attributes into the properties array allows you to trim down the attributes that come back from a search. If you know you only care about, say, "adminCount" and the "userAccountControl" attributes, you can place them in the array and dramatically reduce the amount of console output. You can also create arrays of attributes ahead of time and keep them in your scripts.

For example, if you know you only cared about "name", "memberOf" and "primaryGroupID" for the getUsers search:

#r "nuget: Fiewport"
#r "nuget: Fauli"
#r "nuget: EluciusFTW.SpectreCoff"
#r "nuget: MessagePack.FSharpExtensions"

open Fiewport

let config =
    { ldapDetails = 
        { properties = [| "name"; "memberOf"; "primaryGroupID" |]
          filter = ""
          ldapDN = "DC=northernkingdoms,DC=local"
          scope = SearchScope.Subtree
          ldapHostname = ""
          ldapIP = "192.168.10.38"
          ldapPort = 389
          useSsl = false }
      credentials = 
      { username = "samwell.tarly@northernkingdoms.local"
        password = "Heartsbane" } }

[config]
|> Searcher.getUsers
|> PrettyPrinter.print

only these properties

⚠️ Be careful when restricting LDAP searchers to certain properties! If you combine restricted properties with a Filter and you try to filter for a property that isn't present, you'll just get empty results!

Since connection configs aren't global, you can store multiple configs with different LDAP addresses, users, etc. Put them all in a list, and feed them in!

To connect over LDAPS, set useSsl = true and use port 636:

let config =
    { ldapDetails = 
        { properties = [||]
          filter = ""
          ldapDN = "DC=northernkingdoms,DC=local"
          scope = SearchScope.Subtree
          ldapHostname = ""
          ldapIP = "192.168.10.38"
          ldapPort = 636
          useSsl = true }
      credentials = 
      { username = "samwell.tarly@northernkingdoms.local"
        password = "Heartsbane" } }

Searchers

Fiewport exposes a Searcher with many pre-built LDAP queries included. Your IDE of choice should expose all of them to you, and all of them have some documentation of what filter they are using. Searcher.getUsers performs a search to get users from the AD, for example.

All Searchers yield a List. The list can then be used in a variety of ways, explained below.

A typical individual user result from the search might look like this:

typical result

The current built-in searchers are:

getUsers
getComputers
getSites
getOUs
getGroups
getDomainDNSZones
getDNSRecords
getDomainSubnets
getDFSShares
getGroupPolicyObjects
getDomainTrusts
getDomainObjects
getDomainControllers
getHostsTrustedForDelegation
getReportedServersNotDC
getContainers
getUsersWithSPNs
getConstrainedDelegates
getASREPTargets
getKerberoastTargets
getProtectedUsers
getGroupsWithLocalAdminRights
dumpDomainObjects
getGroupMembers
getGMSAs
getUsersWithSidHistory
getUsersWithAdminCount
getMachineAccountQuota
getForestDomains
getForestGlobalCatalogs
getForestTrusts
getDomainSID
getPasswordPolicy
getGroup
getLaps
lapsRights

Prebuilt attribute request lists live in AttributePresets (terse, standard, verbose) for ldapDetails.properties or Filter.showMany. Run PrettyPrinter.help () for a console quick-reference (Searchers, presets, config notes) when editing scripts without a language server.

In addition to the Searcher, Fiewport exposes Filters, Molds Tees and a Serialize function.

Filter

The Filter has a few static methods that will come in handy when you want to...well...filter the results of your search. Rather than specifying very distinct queries using -SearchBase and such as in PowerView, the Filter allows easier and more iteration-friendly workflows.

Specifying properties or filters in the LDAP configuration will cause your results from the authoritative server to include only those results. That might be what you want. It's stealthier to make a generic query and to filter it down client-side. This is what Filter is for. It will reduce the results list down to only the item you care about.

Expanding our previous example, lets add a Filter that requires the "adminCount" attribute to be present:

#r "nuget: Fiewport"
#r "nuget: Fauli"
#r "nuget: EluciusFTW.SpectreCoff"
#r "nuget: MessagePack.FSharpExtensions"

open Fiewport

let config =
    { ldapDetails = 
        { properties = [||]
          filter = ""
          ldapDN = "DC=northernkingdoms,DC=local"
          scope = SearchScope.Subtree
          ldapHostname = ""
          ldapIP = "192.168.10.38"
          ldapPort = 389
          useSsl = false }
      credentials = 
      { username = "samwell.tarly@northernkingdoms.local"
        password = "Heartsbane" } }

[config]
|> Searcher.getUsers
|> Filter.attributePresent "adminCount"
|> PrettyPrinter.print

adminCount example

This reduced results down to just 5 for this AD.

We can chain it together with another Filter that requires an attribute to have a specific value:

#r "nuget: Fiewport"
#r "nuget: Fauli"
#r "nuget: EluciusFTW.SpectreCoff"
#r "nuget: MessagePack.FSharpExtensions"

open Fiewport

let config =
    { ldapDetails = 
        { properties = [||]
          filter = ""
          ldapDN = "DC=northernkingdoms,DC=local"
          scope = SearchScope.Subtree
          ldapHostname = ""
          ldapIP = "192.168.10.38"
          ldapPort = 389
          useSsl = false }
      credentials = 
      { username = "samwell.tarly@northernkingdoms.local"
        password = "Heartsbane" } }

[config]
|> Searcher.getUsers
|> Filter.attributePresent "adminCount"
|> Filter.attributeIsValue "cn" "Administrator"
|> PrettyPrinter.print

This reduces the results to one.

Filter.byConfig lets you isolate results from a specific search config when you've queried multiple domains. Combine it with Tee to fan out results per domain:

[config1; config2]
|> Searcher.getUsers
|> Tee.filter (Filter.byConfig config1 >> Filter.attributeIsValue "objectGUID" "31B2F340-016D-11D2-945F-00C04FB984F9") PrettyPrinter.teePrint
|> Tee.filter (Filter.byConfig config2 >> Filter.attributePresent "adminCount") PrettyPrinter.teePrint
|> ignore

Filter.valueIs filters for entries containing a specific value in any attribute:

[config]
|> Searcher.getUsers
|> Filter.valueIs "robb.stark"
|> PrettyPrinter.print

Molds

Mold is for when you want to get directly at a value from your search results. If your script has some very specific goals and you need direct access to data, Mold is what you want.

Mold has methods for returning all of the keys (attributes) for your search, all of the raw values for your search, and a tupled list of the keys and values.

#r "nuget: Fiewport"
#r "nuget: Fauli"
#r "nuget: EluciusFTW.SpectreCoff"
#r "nuget: MessagePack.FSharpExtensions"

open Fiewport

let config =
    { ldapDetails = 
        { properties = [||]
          filter = ""
          ldapDN = "DC=northernkingdoms,DC=local"
          scope = SearchScope.Subtree
          ldapHostname = ""
          ldapIP = "192.168.10.38"
          ldapPort = 389
          useSsl = false }
      credentials = 
      { username = "samwell.tarly@northernkingdoms.local"
        password = "Heartsbane" } }

[config]
|> Searcher.getUsers
|> Filter.attributePresent "memberOf"
|> Filter.attributeIsValue "cn" "Administrator"
|> Mold.getValues // string list list list
|> doWhateverYouLikeWithYourResults

Mold.extractOccurances pulls a single attribute value across all results into a flat list:

[config]
|> Searcher.getUsers
|> Mold.extractOccurances "distinguishedname"
|> PrettyPrinter.listPrinter "Distinguished Names"

Tee

Tees are for when you want to perform compound actions for one individual search.

While you can certainly chain Filters together, what you get at the end is a reduced set of results. Sometimes that's what you want, but probably not always.

Tee lets you do something like this:

#r "nuget: Fiewport"
#r "nuget: Fauli"
#r "nuget: EluciusFTW.SpectreCoff"
#r "nuget: MessagePack.FSharpExtensions"

open Fiewport

let config =
    { ldapDetails = 
        { properties = [||]
          filter = ""
          ldapDN = "DC=northernkingdoms,DC=local"
          scope = SearchScope.Subtree
          ldapHostname = ""
          ldapIP = "192.168.10.38"
          ldapPort = 389
          useSsl = false }
      credentials = 
      { username = "samwell.tarly@northernkingdoms.local"
        password = "Heartsbane" } }

[config]
|> Searcher.getUsers
|> Tee.filter (Filter.attributePresent "memberOf" >> Filter.attributeIsValue "cn" "Administrator") PrettyPrinter.teePrint
|> Tee.filter (Filter.attributePresent "isCriticalSystemObject" >> Filter.attributeIsValue "primaryGroupID" "513") PrettyPrinter.teePrint
|> ignore

Tee.filter has a function signature of Filter -> FilterAction -> LDAPSearchResult list -> LDAPSearchResult list. This allows you to compose a Filter chain, and then cap the function off with the so-called FilterAction.

Tee.mold works the same way but takes a Mold<'T> instead of a Filter, letting you branch on transformed data:

[config]
|> Searcher.getUsers
|> Tee.mold (Mold.extractOccurances "samaccountname") (PrettyPrinter.listPrinter "User Names")
|> Tee.filter (Filter.attributePresent "adminCount") PrettyPrinter.teePrint
|> ignore

Chain Tees together to do more than one thing with a single search. Out the 'bottom' of the Tee comes the same search results that went in. You can throw them away, as in the example above, or do whatever else you like.

PrettyPrinter.teeDelimiter lets you insert a labeled divider between Tee branches:

[config]
|> Searcher.getUsers
|> Tee.filter (Filter.attributePresent "adminCount") PrettyPrinter.teePrint
|> PrettyPrinter.teeDelimiter "--- Done with adminCount ---"
|> Tee.filter (Filter.attributePresent "servicePrincipalName") PrettyPrinter.teePrint
|> ignore

Right now, the only FilterAction is the one provided by PrettyPrint. A FilterAction is just LDAPSearchResult list -> unit

PrettyPrinter

Does what it says on the tin. Formats data into something that is a bit more readable.

Serializer

Fiewport now supports serializing and deserializing LDAP search results to/from disk using MessagePack with compression, making it quite small. For reference, the dumpDomainObjects call run against the smaller Game of Active Directory environment yielded a 56KB file.

#r "nuget: Fiewport"
#r "nuget: Fauli"
#r "nuget: EluciusFTW.SpectreCoff"
#r "nuget: MessagePack.FSharpExtensions"

open Fiewport

let config =
    { ldapDetails = 
        { properties = [||]
          filter = ""
          ldapDN = "DC=northernkingdoms,DC=local"
          scope = SearchScope.Subtree
          ldapHostname = ""
          ldapIP = "192.168.10.38"
          ldapPort = 389
          useSsl = false }
      credentials = 
      { username = "samwell.tarly@northernkingdoms.local"
        password = "Heartsbane" } }

[config]
|> Searcher.getUsers
|> Serializer.serializeToDisk // writes "DC=sevenkingdoms,DC=local-GetUsers-{<hexsuffix>}-lcache.bin"
|> ignore

Fiewport will write a .bin file to the current working directory, named by the type of search you performed and the DN of the directory you queried.

Additionally, the Serializer will return the results list, so more processing can be done afterwards.

To use the bin file, simply replace the beginning of the pipeline with the deserialize call:

Serializer.deserializeFromDisk """C:\path\to\bin\DC=sevenkingdoms,DC=local-GetUsers-{<hexsuffix>}-lcache.bin"""
|> Filter.valueIs "robb.stark"
|> PrettyPrinter.print

Shortcomings

  • Schema: Fiewport stores all 1507 MS-native AD attributes. However, it is very common for 3rd party software and sysadmins to add custom attributes. Supporting these cleanly means doing some inspection of the schema, and I haven't worked with that yet. It's a planned feature. Until then, those attributes will not show up on their objects. Fixed!
  • Negation filters: Filter has no negations built-in. Planned.
  • Automatic GPO resolution: GPOs are tracked by their GUID. PowerView and others resolve these to human-names automatically (more or less). Planned. Partially fixed — gPCMachineExtensionNames and gPCUserExtensionNames GUIDs are now resolved to human-readable names via a lookup table.
  • Forced color: PrettyPrinter enforces color, which isn't completely compatible or always desirable. I'll provide a no-color option around the time that file export is added.
  • No file operations: Speaking of file export, there isn't any. Of course, these are .fsx files and System.IO is an import away. Planned. Covered by serialization above.
  • No caching controls: The underlying LDAP searcher supports caching results. However, the way Fiewport disposes of searchers means this setting isn't relevant. I think the use of Tees allows for most of the benefits of caching. Fiewport no longer wraps a platform LDAP client (the old System.Directory.Protocols-based caching option is gone with it), but the addition of serializing the results covers it.
  • nuget: No nuget package yet. Planned once churn is way down. Live at nuget.org
  • Feature-parity with Powerview: Likely will never happen. Powerview includes 'offensive' capabilities, but Fiewport is intended to be purely idempotent and safe to run. For the rest, between configs and Filter, there shouldn't be anything major that Powerview can do that Fiewport can't.

Acknowledgements

Obviously, Fiewport is a derivative of PowerView, at least in spirit. I can't really read Powershell very well, honestly, so the only actual 'code' that came from Powerview was just some function/method names. Still, it provided a usability blueprint that I've endeavored to follow closely.

So thanks to @harmj0y for doing the work originally, and many others who have forked and adapted it going forward.

Code of Conduct

I rule with an iron fist. I reject CoCs.

Product Compatible and additional computed target framework versions.
.NET net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
2026.8.21 74 8/22/2026
2026.8.16 77 8/22/2026