All notable changes to LibreDevOpsHelpers are recorded here.
ConvertTo-LdoDetectionRuleBody: converts an analyst rule (YAML file or parsed object) into the exact Graph detectionRules request body the Terraform module would send: values canonicalised, snake_case keys to the Graph camelCase shape, alert title and description defaulted, and only the first authored MITRE tactic included (the live service rejects more).Test-LdoDetectionRuleDeployment: live API preflight for a rule. Graph has no dry run, so the rule deploys under a randomldo-preflight-*id with status forced to disabled and a marked display name, then deletes itself; create rejections (single tactic, mandatory entity combinations, asset entity requirement, device group existence) surface before Terraform ever runs. Deletion lag and 404s are treated as clean; other delete failures warn with the inert disabled rule named for later cleanup. Quota warning documented (the rules API enforces an hourly per tenant and app quota, roughly 300 calls observed live): a spot check, not a bulk gate.
Remove-LdoDetectionRuleId: strips the server assigned id from exported detection rule files (YAML by precise text surgery so comments survive, JSON by rewrite;-Backupwrites .bak copies). For backups and cross tenant portability, where files should re-create rules as new instead of aligning with existing ones.Export-LdoCustomDetectionRule -ExcludeId: the same intent at export time; the id and its provenance note never enter the files.
- New
LibreDevOpsHelpers.Yamlsubmodule:ConvertTo-LdoYaml, a PowerShell native YAML emitter for analyst facing files (two space indent, literal block scalars for multiline strings such as KQL, minimal quoting, deterministic key order from[ordered]input, inline{}/[]).ConvertFrom-LdoYamlmoved here from the Kql submodule (same export, no caller change). Get-LdoCustomDetectionRule: list or get Defender XDR custom detection rules with nextLink paging; a 403 logs the two unlocks (app roles, or admin consenting the delegated CustomDetection permissions to the Azure CLI application, since the CLI cannot request those scopes itself).Export-LdoCustomDetectionRule: the brownfield half of detections as code. Every rule becomes one file under<OutDir>/<category>/(category = kebab case of the first ATT&CK tactic), converted from Graph camelCase to the terraform-msgraph-xdr-custom-detection-rules snake_case authoring schema. The server assigned rule id is kept on purpose soterraform importaddresses and later plans line up; legacy shape rules (category, mitreTechniques, impactedAssets, responseActions, period strings) convert best endeavours with anything unconvertible emitted as a TODO comment, never silently dropped.-Format Jsonwrites the same spec as JSON (a conversion convenience; export notes go to the log since JSON carries no comments).- New
LibreDevOpsHelpers.Terraform.GraphImportsubmodule, the Graph sibling ofTerraform.AzureImport:Get-LdoTerraformGraphImportResourceId(resolves the msgraph provider's<url>/<id>?api-version=<v>import id, matching planned body id first, then an unambiguous display name) andInvoke-LdoTerraformGraphImportFromPlan(walks a plan's managed msgraph_resource creations, writes a manifest CSV, imports each,-DryRunsupported). Custom detection rules are the first supported collection.
ConvertTo-LdoCanonicalDetectionRule: best endeavours value normalisation for analyst authored detection rules, mirroring the terraform-msgraph-xdr-custom-detection-rules module so the CI schema gate and the Terraform plan never disagree: status, severity and isolation types lowercase; frequencies and technique ids uppercase; tactics resolve case and separator insensitively to the canonical ATT&CK spelling (including British DefenceEvasion to the API's DefenseEvasion).Test-LdoDetectionRuleFileapplies it before the JSON Schema check. Keys stay strict; unknown values still fail with the canonical list named.
- New
LibreDevOpsHelpers.Kqlsubmodule: the detections-as-code validation gate.Install-LdoKustoLanguage: downloads and caches the Microsoft.Azure.Kusto.Language assembly (the library behind the product's own KQL editors) for offline parsing.Test-LdoKqlSyntax: offline KQL syntax validation for queries or files, with labelled diagnostics and a-PassThruobject mode.Test-LdoDefenderHuntingQuery: validates a query against the tenant's real advanced hunting schema viasecurity/runHuntingQuery; queries run verbatim by default, a trailing| take 1is opt in through-AppendTake(appending an operator can interact badly with a query that already ends in one). NeedsThreatHunting.Read.All.ConvertFrom-LdoYaml: YAML parsing through yq when available, with a powershell-yaml fallback, so CI and local runs share one code path.Test-LdoDetectionRuleFileandInvoke-LdoDetectionGate: the per-file and per-directory pull request gate for analyst authored custom detection YAML (parse, optional JSON Schema viaTest-Json, offline KQL syntax, optional remote hunting validation), reporting through the findings summary and failing CI when any rule is broken.
Invoke-LdoDefenderHuntingQuerygains optional-Timespan,-ApiVersion,-MaxRetriesand-Raw(full response with the result schema instead of just rows). Existing callers are unaffected.
- The dance restore restores exactly what was captured. A null captured value means the property
was UNSET at capture time, so the restore now applies the platform default (public network
access Enabled, default action Allow) instead of an invented lockdown. ARM reads an
Allow-with-no-rules Key Vault back as a null
defaultAction, and the old fallback stamped Deny over vaults that were open by design (caught live: the rotation exemplar's vault lost its deliberate public posture after every danced run). The locked-down fallback now applies only to a standaloneRemovethat never had a pairedAdd.
-RuleOnlyonRemove-LdoKeyVaultCurrentIpRuleandRemove-LdoStorageCurrentIpRule: remove just the runner IP rule and leave the network configuration untouched, for runs whose own Terraform apply changes the danced resource (restoring the pre-run capture would overwrite what Terraform wrote).Test-LdoTerraformPlanChangesResource: true when a plan JSON creates, updates, or deletes a resource of a given type and name, so the terraform-azure engine can switch to the rule-only removal automatically.
Add-LdoKeyVaultCurrentIpRulecrashed underSet-StrictModewhenever the vault existed: the state capture readpublicNetworkAccessandnetworkAclsat the top level ofaz keyvault showoutput, but Key Vault nests them underproperties(unlike the flataz storage account show). The capture now shapes the object with a JMESPath--query, which also guarantees every key exists (null when unset). Proven live against a vault that had tripped the crash in CI.- Paired-Remove clobber after a
-SoftFailskip, in both the Key Vault and Storage dances: whenAddskipped because the target did not exist yet (the stack creates it), thefinally-blockRemovefound the freshly created resource, had no cached state, and "restored" the locked-down fallback (public network access Disabled, default action Deny) over the network configuration the run's own apply had just written.Addnow records the skip and the pairedRemoveskips too, leaving the resource exactly as the run applied it.
Invoke-LdoTrivynow discovers a committed ignore file (.trivyignore.yaml,.trivyignore.yml, or.trivyignore) by walking up from the code path to the enclosing git repository root, nearest file first, instead of looking only in the code path itself. A repo-root.trivyignore.yamltherefore covers every stack folder scanned individually (examples/completeand friends) without per-stack copies, and a stack-local file still wins. Without a git root only the code path is searched, so a stray ignore file outside the repository can never silently waive findings.- Documented the path-scoping gotcha for waivers on findings inside downloaded modules: Trivy
reports such paths under the module's source address plus the path relative to the scan target,
so literal repo-relative paths never match; scope with a doublestar glob such as
**/.terraform/modules/key_vault/main.tf.
-SoftFailon the firewall dance functions:Add-LdoKeyVaultCurrentIpRule,Remove-LdoKeyVaultCurrentIpRule,Add-LdoStorageCurrentIpRule,Remove-LdoStorageCurrentIpRule, andAdd-LdoNspCurrentIpRule. Opt-in (never the default): when the target resource does not exist the function logs a warning ("does not exist, so cannot append the runner IP; skipping") and returns instead of failing, so a pipeline whose stack creates the resource itself survives the first run and dances normally from the next run onward. Absence is the ONLY condition softened: authentication, network, or any other failure still throws, so real problems never masquerade as a first run.- The Key Vault probe distinguishes soft-deleted vaults with its own warning: the coming apply resurrects them with their previous network ACLs, so the run behaves like a later run wearing a first-run disguise.
- Pester coverage for the Network Security Perimeter module surface (previously untested) and
for the new
-SoftFailparameter across all three families.
Write-LdoLognow emits structured JSON by default (newline-delimited JSON, one object per line) carrying a UTC ISO-8601timestamp,level,invocationandmessage, ready for ingestion by log aggregators such as Splunk, Elasticsearch or Azure Monitor.Get-LdoLogLevelto read the current minimum log level (companion toSet-LdoLogLevel).-Format Text(per call) andSet-LdoLogFormat/Get-LdoLogFormatto switch between JSON and the previous human-readable coloured line format. AJsonIndentedformat is also available as an opt-in for local debugging (pretty-printed, not newline-delimited). The default can also be set via theLDO_LOG_FORMATenvironment variable, and the minimum level viaLDO_LOG_LEVEL.-Dataparameter onWrite-LdoLogfor merging extra structured properties (for example correlation IDs or resource names) into the JSON record.- New
LibreDevOpsHelpers.Uvmodule wrapping the uv Python package and version manager:Install-LdoUv,Test-LdoUv,Install-LdoUvPython,Get-LdoUvPython,Set-LdoUvPythonPin,New-LdoUvVenv,Invoke-LdoUvSync,Invoke-LdoUvLock,Add-LdoUvPackage,Remove-LdoUvPackage,Invoke-LdoUvRun,Invoke-LdoUvPipInstall,Invoke-LdoUvPipUninstall. - New
LibreDevOpsHelpers.Defendermodule spanning four Microsoft Defender surfaces: Defender for Cloud (az security:Get-LdoDefenderSecureScore,Get-LdoDefenderRecommendation,Get-LdoDefenderPlan,Set-LdoDefenderPlan); Defender for Endpoint / XDR via the Graph Security API and Defender for Endpoint API (Get-LdoDefenderAlert,Invoke-LdoDefenderHuntingQuery,Invoke-LdoDefenderDeviceIsolation,Invoke-LdoDefenderAvScan); Defender Antivirus on Windows (Get-LdoDefenderAvStatus,Start-LdoDefenderAvScan,Update-LdoDefenderAvSignature,Add-LdoDefenderAvExclusion); and Defender for Endpoint on Linux viamdatp(Get-LdoMdatpHealth,Start-LdoMdatpScan,Update-LdoMdatpDefinition,Add-LdoMdatpExclusion). - New
LibreDevOpsHelpers.GitLabmodule wrapping the glab CLI and adding helpers for PowerShell running inside GitLab CI/CD pipelines:Install-LdoGlab,Test-LdoGlab,Connect-LdoGlab,Invoke-LdoGlabPipeline,Get-LdoGlabPipeline,Wait-LdoGlabPipeline,New-LdoGlabMergeRequest,New-LdoGlabRelease,Set-LdoGlabCiVariable,Get-LdoGlabCiVariable,Get-LdoGitLabCiVariable,Set-LdoGitLabCiOutput, andWrite-LdoGitLabCiSection(collapsible/timed CI log sections). Tokens are passed via SecureString and piped through stdin so they never reach the command line or logs.
- INFO and SUCCESS messages now route through
Write-Information(a tagged, capturable information-stream record) in JSON mode instead ofWrite-Host; Text mode keeps colouredWrite-Hostoutput for interactive CLI use. WARN/ERROR/DEBUG stream routing is unchanged. - The default log output is now JSON rather than plain text. Anything that parsed the old
text lines should either parse JSON or opt back in with
-Format Text/Set-LdoLogFormat. - Terraform functions now assert that
terraformis on PATH before running, so a missing CLI fails with a clear message.Invoke-LdoTerraformInitdefaults to-input=falsefor non-interactive CI unless the caller already passes-input.Convert-LdoTerraformPlanToJsonverifies theterraform showexit code before writing, so a failure no longer leaves a corrupt or empty JSON file. Get-LdoPublicIpAddressnow accepts-TimeoutSec(default 15) and validates that the endpoint returned a real IP address, rather than passing an error page through as the result.- Python functions now use the shared
Assert-LdoLastExitCodefor uniform native-failure handling, andInvoke-LdoPytestRunasserts its-PythonExeis on PATH before running. - TerraformDocs functions now use the shared
Assert-LdoCommand/Assert-LdoLastExitCodehelpers and validateCodePath.Update-LdoReadmeWithTerraformDocsverifies theterraform-docsexit code and writes the README in a single step, so a failure no longer leaves a half-written README.Set-LdoTerraformFileContentnow writes UTF-8. Invoke-LdoTerraformImportFromPlanis hardened: it asserts the Azure CLI/terraform are present and signed in up front, checks the exit code of everyterraform import(a failed import is no longer reported as success and the run now throws if any import fails), and drops the broken--%token from the import call.Get-LdoTerraformImportResourceIdnow scopes itsaz resource listand Resource Graph fallbacks by resource group/subscription so a same-named resource elsewhere can't be matched and imported by mistake, and it validates the resolved subscription id.
- Every public command was renamed to use the
Ldoprefix (for exampleInvoke-TerraformPlanis nowInvoke-LdoTerraformPlan). There are no backwards-compatible aliases. Callers must update to the new names. - Minimum supported PowerShell is now 7.2.
FunctionsToExportin the manifest is now an explicit list of all exported commands.
- Comment-based help,
[CmdletBinding()],[OutputType()], and parameter validation across every function. - A Pester test suite covering every module, plus an analyzer-clean PSScriptAnalyzer pass run by
Invoke-Tests.ps1and a GitHub Actions lint and test workflow. - Shared
LibreDevOpsHelpers.UtilshelpersAssert-LdoLastExitCodeandGet-LdoPublicIpAddress, now reused across the Azure resource and tooling modules. - New and completed functionality: split temporary IP access rules into explicit add/remove
functions for Key Vault, storage, NSG, and function apps;
Connect-LdoAzureCliManagedIdentity; secure-string handling for Azure DevOps PATs and Docker registry credentials; and Pester custom Should operators withRegister-LdoPesterAssertion.
- Numerous correctness bugs surfaced during the rewrite, including
Invoke-TerraformDestroyignoring its destroy arguments, the Trivy skip-policy arguments being concatenated into one token, the Azure DevOps org lookup dereferencing the response before its null check, the NSG rule helper ignoring port and prefix parameters, and the Pester runner referencing an undefined configuration variable. - Functions no longer call
exitor useInvoke-Expression; native CLI failures are detected via exit-code checks.
- New
LibreDevOpsHelpers.Graphmodule with reusable helpers for Microsoft Graph and other Azure REST APIs:Invoke-WithRetry: retry wrapper with exponential backoff, jitter, Retry-After support, and a configurable list of retryable status codes so non-transient errors fail fast.Get-GraphToken: per-resource token cache that auto-refreshes near expiry and handles both plaintext and SecureString tokens across Az.Accounts versions.Get-GraphErrorDetail: extracts the real Graph error code and message from the response body.Invoke-GraphRequest: request wrapper that adds the bearer token, retries transient failures, and refreshes the token once on a 401.Clear-GraphTokenCache: clears the cached token for one resource or all.
New-Passwordreferenced a function that did not exist, which made it throw on use. The random sequence generator is now a module-scope function and is called correctly.- The Utils module no longer exports a function name that was never defined.
Publish-ToPSGallery.ps1now accepts-WorkingDirectoryand-ApiKey, resolves the key fromPSGALLERY_TOKENorNUGET_API_KEY, and validates the manifest before publishing.
- Earlier releases. See git history for details.