Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .build/cspell-words.txt
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,11 @@ mepfdns
mepfs
meso
mesos
metacharacters
mfcmapi
Mgmt
misattributed
misrouting
mitigations
msdcs
MSDTC
Expand Down Expand Up @@ -161,6 +164,7 @@ subfolders
syncall
tcpip
TDSDSA
triaging
Truncater
UCMA
unconfigured
Expand All @@ -179,4 +183,5 @@ Webex
Weve
wevtutil
windir
worktree
Xlsb
58 changes: 58 additions & 0 deletions .github/skill-lib/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<!-- cspell:ignore toplevel -->
# skill-lib

Shared filesystem-safety helpers dot-sourced by skills under `.github/skills/`.

This directory sits **beside** `.github/skills/`, not inside it, so the skill
loader does not attempt to discover a `SKILL.md` here. Each helper is a
single-purpose `.ps1` file with a documented contract at the top; skills
dot-source only the helpers they need.

## When to add a helper here

Extract a helper into this directory only when it is:

- **Byte-for-byte reusable** across two or more skills. If two callers need
different behavior, keep the helpers inline in each skill — divergent
copies with the same name are worse than duplication because they silently
break the "same call, same behavior" contract downstream.
- **Purely defensive filesystem plumbing** (path validation, reparse-point
detection, DOS-device probes, handle equality checks). Domain logic,
reporting helpers, and one-of-a-kind orchestration stay in the calling
skill.

## Consuming a helper

Skills dot-source with a `$PSScriptRoot`-relative path that walks up out of
`.github/skills/<skill-name>/` and back down into `.github/skill-lib/`:

```powershell
. $PSScriptRoot\..\..\skill-lib\Test-IsLocalDosDeviceTarget.ps1
. $PSScriptRoot\..\..\skill-lib\Test-PathHasReparsePointRootToLeaf.ps1
```

The `SKILL.md` fenced code block used by the analyze-debug-files skill
does NOT use `$PSScriptRoot` — that variable is unreliable when a
markdown-embedded PowerShell block is executed via `pwsh -Command`, an
extracted temp `.ps1`, or a dot-source from `Invoke-Expression`. That
block anchors to the repo root via `git rev-parse --show-toplevel`,
verifies the resolved repository is `microsoft/CSS-Exchange`, and then
dot-sources the same helper files with `Join-Path $repoRoot ...`. The
skill scripts under `.github/skills/<skill-name>/` use `$PSScriptRoot`
because they are always dot-sourced from a real file where the variable
is well-defined.

## Add-Type namespace convention

Helpers that use `Add-Type` for P/Invoke declare types under the
`SkillLib.*` namespace and guard the declaration with an `-as [type]`
check so re-sourcing is a no-op:

```powershell
if (-not ('SkillLib.DosDeviceHelper' -as [type])) {
Add-Type -Namespace 'SkillLib' -Name 'DosDeviceHelper' -MemberDefinition ...
}
```

A single shared namespace means the P/Invoke types load once per PowerShell
session even when multiple skills consume the same helper.
71 changes: 71 additions & 0 deletions .github/skill-lib/Test-IsLocalDosDeviceTarget.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

<#
.SYNOPSIS
Returns $true when a bare drive letter maps to a real local volume,
$false for SUBST/DefineDosDevice-created drives, raw DOS device aliases,
or drives that fail QueryDosDevice.

.DESCRIPTION
QueryDosDevice check: reject SUBST/DefineDosDevice-created drives
(their target is `\??\<real path>`) and raw DOS device aliases
(`\Device\<name>\<subpath>`). A real local volume maps to a bare
`\Device\<name>` target with no trailing path component.

`[System.IO.DriveInfo]` alone is not enough — a SUBST'd drive reports
DriveType.Fixed but redirects to arbitrary targets (including UNC or
reparse-point paths), so callers must combine DriveInfo with this
check to close the SUBST bypass.

Consumed by:
- .github/skills/analyze-debug-files/Get-DebugFileMetadata.ps1
- .github/skills/analyze-debug-files/SKILL.md (Step 5 code block)
- .github/skills/trace-code-introduction/Trace-CodeIntroduction.ps1

.PARAMETER DriveLetter
A single drive-letter token: bare (`C`) or colon-suffixed (`C:`). Any
other shape — multi-character names (`NUL`, `CON`, `LPT1`), embedded
path separators, empty strings, or non-ASCII characters — is rejected
without calling `QueryDosDevice`. This defends against caller mistakes
that would otherwise let multi-character DOS aliases (which resolve to
a bare `\Device\<name>` target and match the local-volume shape check)
slip through as "local drives."

.OUTPUTS
[bool] — $true when the drive maps to `\Device\<name>` with no
trailing path segment; otherwise $false.

.EXAMPLE
PS> Test-IsLocalDosDeviceTarget -DriveLetter 'C:'
True

.EXAMPLE
PS> subst X: C:\Users
PS> Test-IsLocalDosDeviceTarget -DriveLetter 'X:'
False
#>
function Test-IsLocalDosDeviceTarget {
param([Parameter(Mandatory)][string]$DriveLetter)
# STRICT shape check first — reject anything that isn't a single ASCII
# letter, optionally with a trailing colon. Multi-character DOS device
# names like `NUL`, `CON`, `LPT1`, `COM1`, `PhysicalDrive0` also
# resolve to a bare `\Device\<name>` target and would otherwise match
# the local-volume regex below.
if ($DriveLetter -notmatch '^[A-Za-z]:?$') { return $false }
# QueryDosDevice requires the trailing colon; accept the bare drive
# letter form to match the parameter contract and normalize here so
# callers don't have to remember the format.
$name = if ($DriveLetter.Length -eq 1) { "${DriveLetter}:" } else { $DriveLetter }
if (-not ('SkillLib.DosDeviceHelper' -as [type])) {
Add-Type -Namespace 'SkillLib' -Name 'DosDeviceHelper' -MemberDefinition @'
[System.Runtime.InteropServices.DllImport("kernel32.dll", CharSet=System.Runtime.InteropServices.CharSet.Unicode, SetLastError=true)]
public static extern uint QueryDosDevice(string lpDeviceName, System.Text.StringBuilder lpTargetPath, uint maxChars);
'@ -ErrorAction Stop
}
$sb = New-Object System.Text.StringBuilder 1024
$len = [SkillLib.DosDeviceHelper]::QueryDosDevice($name, $sb, 1024)
if ($len -eq 0) { return $false }
$target = $sb.ToString()
return ($target -match '\A\\Device\\[^\\]+\z')
}
70 changes: 70 additions & 0 deletions .github/skill-lib/Test-PathHasReparsePointRootToLeaf.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

<#
.SYNOPSIS
Returns $true if any component from the volume root down to the target
is a reparse point (junction/symlink); otherwise $false. Segments that
do not yet exist are treated as safe.

.DESCRIPTION
Walks root → leaf. Returns $true as soon as any ancestor is a reparse
point, WITHOUT ever calling filesystem cmdlets on a descendant of a
reparse ancestor. Uses attribute-only reads (no follow) via
`[System.IO.File]::GetAttributes` so a directory symlink pointing at
a UNC share is not opened as part of the check.

Not-yet-existing tail segments return $false — the caller may create
a file into an existing safe directory. Any error inspecting an
ancestor (access denied, broken link, etc.) is treated as UNSAFE and
returns $true rather than assuming absence of a reparse point.

Consumed by:
- .github/skills/analyze-debug-files/Get-DebugFileMetadata.ps1
- .github/skills/analyze-debug-files/SKILL.md (Step 5 code block)
- .github/skills/trace-code-introduction/Trace-CodeIntroduction.ps1

.PARAMETER Path
An absolute filesystem path. Callers should pass a lexically local,
resolved path — validating the path shape is out of scope for this
helper.

.OUTPUTS
[bool] — $true if any ancestor component is a reparse point or a
filesystem error occurs; $false when the entire chain is a plain
directory tree.
#>
function Test-PathHasReparsePointRootToLeaf {
param([Parameter(Mandatory)][string]$Path)
try {
$normalized = [System.IO.Path]::GetFullPath($Path)
} catch {
return $true
}
$parts = New-Object System.Collections.Generic.List[string]
$cur = $normalized
while (-not [string]::IsNullOrEmpty($cur)) {
$parts.Insert(0, $cur)
$parent = Split-Path -Parent $cur
if ([string]::IsNullOrEmpty($parent) -or $parent -eq $cur) { break }
$cur = $parent
}
foreach ($p in $parts) {
try {
$attrs = [System.IO.File]::GetAttributes($p)
if (($attrs -band [System.IO.FileAttributes]::ReparsePoint) -ne 0) {
return $true
}
} catch [System.IO.FileNotFoundException] {
# Not-yet-existing tail segments are OK — the workflow may
# create the report file into an existing directory.
continue
} catch [System.IO.DirectoryNotFoundException] {
continue
} catch {
# Any other error while inspecting an ancestor is a hard fail.
return $true
}
}
return $false
}
Loading