Development notes
Notes on the development of Martinet including coding standards, development philosophy, and rationale.
PowerShell returned object types depend on number of items returned
Be wary of PowerShell commands that return different types based on the number of items returned. Get-ADGroupMember, Get-ADObject, Get-ChildItem are amongst the commands which return $null for 0 items returned, an object of item-specific type for 1 item returned, and an array of an item-specific type for 2 or more items returned. Counting the number of items (with .Count) also does not always return an integer (or any other numeric type) depending on the number of items being counted, but .Count does work as expected for arrays. Your code must be sensitive to this difference. One way of handling this is to force the returned value into an array with @(), for example:
try { $subfolders = @(Get-ChildItem -LiteralPath "$folder" -Directory -Name) }
catch { Warn "Cannot read subfolders of folder ($folder)." }
# Now .Count will return a proper value regardless of how many subfolders there are (0, 1, 2, or more).
$subfolders.Count
Comparisons to $null
Comparisons to $null should list $null first—if ($variable -eq $null) should be reversed to if ($null -eq $variable) per Microsoft's PowerShell documentation.
Use try/catch liberally
It's not always obvious which commands will throw an error or return a value indicating an error. try/catch will catch errors and you might need to use both branching plus try/catch to keep the main program running and handling error feedback properly (particularly in non-interactive programs like those run from Windows Task Scheduler).
Report tables of warnable results
Usually there will be a set of results that don't match the specification in the configuration file. If the warnable results number 0: no report is given; 1: return a report on that warnable item; more than 1: return a table of results with a reasonable set of headers.
PowerShell development notes
- Prefer
switchas the main branching mechanism because it is clear. - Properly-ordered evaluation blocks in
switchshould be used to prevent and warn about configuration mistakes—vet malformed wildcard and regular expression specifications with evaluation blocks in switch statements.
switch -Exact ($operator) { { $_ -iin 'not like', 'like' } { try { $null = 'x' -like $argument } catch { Warn "..." ; continue requirement } } { $_ -iin 'not match', 'match' } { try { $null = 'x' -match $argument } catch { Warn "..." ; continue requirement } } { $_ -iin '!=', '=', '>' } { if ($argument -notmatch '^(0|[1-9]\d*)$') { Warn "..." ; continue requirement } } '!=' { if ($subfoldersCount -eq $argument) { Warn "..." } ; continue } '=' { if ($subfoldersCount -ne $argument) { Warn "..." } ; continue } '>' { if ($subfoldersCount -le $argument) { Warn "..." } ; continue } Default { Warn "..." ; continue requirement } }
As long as the try/catch $argument checking happens before any use of the corresponding $argument, the vetting is sufficient. - Don't loop over checking the requirement's
$operatoror$argument—it wastes time to re-check the same$argumentand$operatorby looping over each item in $expandedTargets. Instead, check all of the$expandedTargetswith relevant.Where()queries to make a list of warnable targets that can be examined. The first evaluation block matches operators where arguments need to be checked for syntactical validity before the argument is used to determine whether a warning should be issued. The corresponding action block carries out that syntactical check. The remaining evaluation blocks check argument validity and issue warnings. - Documentation improves and expands with development—user-visible changes to configuration syntax, supported systems, and program behavior will be documented. Therefore documentation goes hand-in-hand with program development; adding new functionality as hidden options is strongly discouraged but might be necessary to show non-developers how a feature could work. Pre-release versions can support features that aren't documented but pre-release versions should not be deployed in production.
Philosophy
- Martinet's capabilities should be based primarily on need. If Martinet can't vet what you want it to, contact Martinet support and mention what you'd like to see Martinet cover. Requests that come with explanations of how you'll use the expanded capability in your Martinet configuration are most useful.
- Martinet evaluates via read-only access—Martinet should not modify state in any covered system in which it evaluates requirements.
- Martinet configuration sections have syntax
[container-type:pointer to container object]. The container type name may not contain a colon but the pointer to a container object may contain a colon (only the leftmost colon is the separator). - Adding new section types and pointers, new requirement syntax, and expanding extant syntax is based on examining high-level needs for inspection; features are not added because they're easy to implement.
- Requirements syntax reuse is highly valued but not a limit on what should be implemented. Martinet requirement syntax usually follows the pattern: contained-object verb operator optional-arguments.
- Martinet requirements often reuse verbs, operators, and argument types.
Object Operators Argument Notes count <positive integer count >,=,!=non-negative integer name is,is nota string taken literally name matches,not matchesa regular expression string evaluated by PowerShell name like,not likea wildcard string evaluated by PowerShell type, scope is,is nota set of mutually-exclusive values Used for specifying one of a set of values where only one makes sense to specify (such as: user/group,allow/deny,domainlocal/global/universal,true/false, etc.). - Martinet requirements also reuse a syntactical structure where one needs to specify multiple property/value pairs. This structure should be reused where the ANDed combination should be matched (typically to find rows in a table):
Object Operators Property Property operators Argument ACE entry is:,is not:AccessControlType is,is notAllow/DenyACE entry is:,is not:IdentityReference is,is notdomain \user literal string, domain\group literal string, a regular expression string evaluated by PowerShell, a wildcard string evaluated by PowerShellACE entry is:,is not:IsInherited is,is notTrue/FalseACE entry is:,is not:InheritanceFlags contains,not containscomma-separated set of rights
Testing
Switch to a more explicitly specified set of functions for requirements, operators, etc.
Work on this proceeds with good results so far – adding new sections, commands, and properties (for compound requirements) is going well. Much of the extant code will need to be moved into blocks and tested. Main loop restructuring to add functionality proceeds with this work.
Look for or write an email replacement for Send-MailMessage
Microsoft has warned, “Send-MailMessage cmdlet is obsolete. This cmdlet doesn't guarantee secure connections to SMTP servers. While there is no immediate replacement available in PowerShell, we recommend you do not use Send-MailMessage”.
Email is convenient, ubiquitous, shows no sign of going away (despite multiple attempts at being replaced by social media-like UIs), and time-honored. Email also scales up and is easily archived & searched.
It would be wise to look for a replacement for Send-MailMessage. Perhaps Microsoft Graph is appropriate, or perhaps there is a PowerShell drop-in replacement for Send-MailMessage which is just as easy to use as Send-MailMessage is.
Section ideas
- Scheduled tasks – possible but needs clear indication of what to look for.
