Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#Requires -Version 7.0
Set-StrictMode -Version Latest

# Capability flag at DISCOVERY time (-Skip is evaluated then).
$HasPSSA = [bool](Get-Module -ListAvailable -Name PSScriptAnalyzer)

BeforeAll {
# Exercise the rule through the real hook in a CHILD process. This tests the
# public boundary (what CI runs) and avoids in-process PSScriptAnalyzer state
# leaking between test files in one Pester session.
$script:Hook = Join-Path $PSScriptRoot '..' 'scripts' 'Test-PowerShellScript.ps1' |
Resolve-Path | Select-Object -ExpandProperty Path

function Invoke-Hook {
param([string]$Content, [string]$Extension = '.ps1')
$file = Join-Path ([IO.Path]::GetTempPath()) ('mrsm-{0}{1}' -f [guid]::NewGuid(), $Extension)
Set-Content -LiteralPath $file -Value $Content
try {
$output = & pwsh -NoProfile -File $script:Hook $file 2>&1 | Out-String
[pscustomobject]@{ Exit = $LASTEXITCODE; Output = $output }
}
finally { Remove-Item -LiteralPath $file -ErrorAction SilentlyContinue }
}
}

Describe 'Measure-RequireStrictMode (via the hook)' -Tag 'Fast' -Skip:(-not $HasPSSA) {
It 'fails a script that omits Set-StrictMode, naming the rule' {
$r = Invoke-Hook -Content 'Write-Output 1'
$r.Exit | Should -Be 1
$r.Output | Should -Match 'Measure-RequireStrictMode'
}

It 'passes a script that calls Set-StrictMode' {
(Invoke-Hook -Content "Set-StrictMode -Version Latest`nWrite-Output 1").Exit | Should -Be 0
}

It 'exempts data files (*.psd1)' {
(Invoke-Hook -Content '@{ Answer = 42 }' -Extension '.psd1').Exit | Should -Be 0
}
}
58 changes: 58 additions & 0 deletions .config/PSScriptAnalyzerRules/Measure-RequireStrictMode.psm1
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#Requires -Version 7.0
<#
Custom PSScriptAnalyzer rule, loaded via CustomRulePath in
../PSScriptAnalyzerSettings.psd1. See .config/scripts/README.md.
#>

Set-StrictMode -Version Latest

function Measure-RequireStrictMode {
<#
.SYNOPSIS
Flags a script that never calls Set-StrictMode.
.DESCRIPTION
`Set-StrictMode -Version Latest` turns unassigned variables, missing
properties and malformed calls into errors instead of silent bugs — the
fail-fast baseline this repo expects. The rule fires once per file when
no Set-StrictMode invocation is present anywhere in it.
.PARAMETER ScriptBlockAst
Script block AST supplied by PSScriptAnalyzer for each block in the file.
.OUTPUTS
[Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord[]]
#>
[CmdletBinding()]
[OutputType([Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord[]])]
param(
[Parameter(Mandatory)]
[System.Management.Automation.Language.ScriptBlockAst]$ScriptBlockAst
)

process {
# Evaluate only the file's root block. Nested blocks (functions, script
# blocks) inherit strict mode, so checking them would raise a duplicate
# finding per block.
if ($null -ne $ScriptBlockAst.Parent) { return }

# Data files (*.psd1) are declarative manifests, not executable scripts;
# Set-StrictMode does not apply to them.
if ($ScriptBlockAst.Extent.File -like '*.psd1') { return }

$hasStrictMode = $ScriptBlockAst.FindAll({
param($node)
$node -is [System.Management.Automation.Language.CommandAst] -and
$node.GetCommandName() -eq 'Set-StrictMode'
}, $true)

if ($hasStrictMode) { return }

[Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord]::new(
'Script does not call Set-StrictMode. Add `Set-StrictMode -Version Latest` near the top to fail fast on unassigned variables and typos.',
$ScriptBlockAst.Extent,
'Measure-RequireStrictMode',
[Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticSeverity]::Warning,
$ScriptBlockAst.Extent.File
)
}
}

Export-ModuleMember -Function Measure-RequireStrictMode
48 changes: 48 additions & 0 deletions .config/overlays/semgrep-pro/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Semgrep Pro overlay (documentation only — no license, not activated)

This folder is a **decision record**, not working config. It exists so the
tradeoff is written down once instead of rediscovered per repo.

The free-tier semgrep setup is the [`semgrep` overlay](../semgrep/README.md) —
also opt-in, also needing no account, license, or per-repo quota. Activating
that overlay is unaffected by anything below; this overlay only concerns
*Pro*-tier features layered on top of it.

## What Pro adds, and why it isn't on

Semgrep's **free/OSS CLI accepts a `languages: [powershell]` rule but silently
skips it** — 0 findings, no error, just a warning that it "requires Pro". Pro
also adds cross-file analysis, AI-assisted triage/remediation, and 60 AI
credits. Verified by testing against the running CLI (2026-07, semgrep 1.170.0).

The blocker is not cost tuning, it's a **hard account-wide cap**:

> Scan up to 10 repositories · Maximum 10 contributors

This template is cloned into new repos on purpose. Baking a Pro dependency into
the template would mean every repo created from it silently competes for one of
only 10 slots on the account — a scarce resource, not a per-repo toggle. That is
why this stays an inert, deliberately-activated overlay: the decision to spend a
slot belongs to a human, once, per repo that actually needs PowerShell
enforcement (or the other Pro features) badly enough to justify it.

## If you decide to activate it (on a specific repo)

1. Check the current slot count against the 10-repo cap before linking another
one — <https://semgrep.dev/orgs/-/settings> (or your org's usage page).
2. Link the repo: <https://semgrep.dev/login>, then `semgrep login` locally / in
CI.
3. Rules with `languages: [powershell]` (or other Pro-only targets) then run
with `semgrep scan --pro` — the free-tier flag drops the Pro skip.
4. No PowerShell Pro rules exist in this repo yet — none are written or tested,
since there is currently no account to verify them against. Write and verify
them the same way the [bash rule](../../semgrep/rules/bash-no-blanket-strict-mode.yaml)
was: a violating + a clean sample file, run through `semgrep scan`, before
adding the rule to `.config/semgrep/rules/`.

## Re-evaluate if

- The account already holds a Pro subscription for other reasons (then the
10-repo cap is a real, trackable constraint to budget against, not a blocker).
- PowerShell policy enforcement becomes valuable enough on a *specific* repo to
spend one of the 10 slots on it deliberately.
44 changes: 44 additions & 0 deletions .config/overlays/semgrep/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Semgrep overlay (opt-in activation)

Semgrep is **not wired into pre-commit by default**. The rule files are real
and tracked at [`.config/semgrep/`](../../semgrep/README.md) — this overlay is
only the *activation* (the pre-commit hook fragment). Nothing runs until you
paste it in.

## Why activation is opt-in, not core

It was briefly wired in as an active hook + scheduled CI workflow and then
reverted. Two real frictions drove that:

- **The managed pre-commit hook does not work on Windows** — pre-commit clones
semgrep's own git repo, which contains case-colliding paths that fail
checkout on NTFS. The `repo: local` PyPI-wheel workaround below fixes it but
adds install weight and a `require_serial: true` workaround for a
`~/.semgrep/settings.yml` race between parallel instances.
- **Its language coverage has real gaps for this template's stack**: SQL has
no support at any tier; **PowerShell parses but Pro-gates rule execution**
(free CLI silently reports 0 findings — see
[`../semgrep-pro/README.md`](../semgrep-pro/README.md)), and Pro is capped at
10 repos account-wide — unsuitable for a template cloned into many repos.

None of that makes semgrep *bad* — the shipped rules are verified and work —
but it does not clear the bar for base-template tooling every clone inherits by
default. Tracking issue for a replacement evaluation:
<https://github.com/DenWin/Template/issues/3>.

## Activate

1. **Install semgrep** — `pip install semgrep` (or `pipx install semgrep`).
2. **Wire the hook** — paste the block from
[`precommit-hook.yaml`](precommit-hook.yaml) into the `repos:` list of
[`/.pre-commit-config.yaml`](../../../.pre-commit-config.yaml). It installs
from PyPI as a `repo: local` hook (not the managed `repo:` clone, which
fails on Windows — see above).
3. **Verify** — `semgrep scan --config .config/semgrep/rules --metrics=off .`
should run clean, or report only intended findings.

## Why an overlay, not a placeholder

A half-configured tool that errors on every run trains people to ignore it.
Inert-but-complete means it never breaks a repo that ignores it, yet activation
is a paste-and-pin. Same rationale as the [Vale overlay](../vale/README.md).
23 changes: 23 additions & 0 deletions .config/overlays/semgrep/precommit-hook.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Semgrep hook FRAGMENT — inert. To activate, paste this block into the `repos:`
# list of /.pre-commit-config.yaml, then bump the pin manually (semgrep is NOT
# managed by `pre-commit autoupdate` here — see below).
#
# `repo: local` + PyPI wheel, NOT the managed `repo: https://github.com/semgrep/semgrep`
# form: that clones semgrep's own git repo, which contains case-colliding paths
# that fail checkout on Windows/NTFS. `require_serial: true` avoids a
# `~/.semgrep/settings.yml` race between parallel instances on Windows.
- repo: local
hooks:
- id: semgrep
name: semgrep (project policy rules)
language: python
additional_dependencies: ["semgrep==1.170.0"] # verify / bump manually
require_serial: true
entry: semgrep
args:
- --config=.config/semgrep/rules
- --error
- --metrics=off
- --quiet
- --skip-unknown-extensions
- --disable-version-check
45 changes: 0 additions & 45 deletions .config/scripts/Enable-MergeQueue.Tests.ps1

This file was deleted.

109 changes: 0 additions & 109 deletions .config/scripts/Enable-MergeQueue.ps1

This file was deleted.

2 changes: 2 additions & 0 deletions .config/scripts/Initialize-DevEnvironment.Tests.ps1
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
#Requires -Version 7.0
Set-StrictMode -Version Latest

BeforeAll {
# Dot-source the SUT: defines its functions; the run guard skips execution.
. (Join-Path $PSScriptRoot 'Initialize-DevEnvironment.ps1')
Expand Down
Loading