mirror of
https://github.com/kellymichels/zscripts-token-savers
synced 2026-10-07 07:18:18 +00:00
feat(zchecksums): SHA-256 manifest so a download can be verified before it's run
CHECKSUMS.txt lists a SHA-256 for every top-level .ps1 and .cmd - the files a user actually executes. zchecksums verifies them; zchecksums -Update regenerates after an intentional edit. The manifest is sha256sum format, so 'sha256sum -c CHECKSUMS.txt' works on Linux/macOS/WSL as well as the PowerShell path on Windows. Hashes are identical on every platform because .gitattributes pins .ps1/.cmd to CRLF everywhere - that pin is now load-bearing, so it is commented as such. Beyond changed and missing files it also reports a script that is on disk but NOT in the manifest, so something added outside a commit still gets noticed. Exits non-zero on any of the three. Honest about its limits, in the header and the README: the manifest lives in the same repo as the code, so it is an integrity check rather than a signature. It catches a truncated clone, a forgotten local edit, or an unlisted file - not a compromised repo. CHECKSUMS.txt is pinned to LF: sha256sum treats a trailing CR as part of the filename and would report every entry as missing on Linux. tests/Checksums.Tests.ps1 keeps it from rotting - a stale manifest is worse than none, since it either cries wolf until people ignore it or quietly stops covering a new script. The tests assert the format, LF endings, sort order, full coverage of on-disk scripts, current hashes, and that zchecksums itself exits 1 on a tampered file (proved by appending a byte and restoring it).
This commit is contained in:
parent
c20e82e209
commit
9d13b8ce72
8
.gitattributes
vendored
8
.gitattributes
vendored
@ -1,3 +1,9 @@
|
|||||||
# PowerShell/cmd are happiest with CRLF on Windows.
|
# PowerShell/cmd are happiest with CRLF on Windows. The pin matters beyond
|
||||||
|
# convenience: it makes these files byte-identical on every platform, which is
|
||||||
|
# what lets CHECKSUMS.txt hold one hash per file rather than one per OS.
|
||||||
*.ps1 text eol=crlf
|
*.ps1 text eol=crlf
|
||||||
*.cmd text eol=crlf
|
*.cmd text eol=crlf
|
||||||
|
|
||||||
|
# The checksum manifest must stay LF: `sha256sum -c` treats a trailing CR as
|
||||||
|
# part of the filename and reports every entry as missing.
|
||||||
|
CHECKSUMS.txt text eol=lf
|
||||||
|
|||||||
10
CHANGELOG.md
10
CHANGELOG.md
@ -22,6 +22,16 @@ Notable changes to the Evomedia.net Token Savers.
|
|||||||
credentials and RSA signing keys.
|
credentials and RSA signing keys.
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
- **`zchecksums` + `CHECKSUMS.txt`** — a SHA-256 manifest covering every `.ps1`
|
||||||
|
and `.cmd`, so a download can be verified before anything is run. `zchecksums`
|
||||||
|
checks them; `zchecksums -Update` regenerates after an intentional edit. The
|
||||||
|
manifest is `sha256sum` format, so `sha256sum -c CHECKSUMS.txt` works on
|
||||||
|
Linux/macOS/WSL too, and the hashes match on every platform because
|
||||||
|
`.gitattributes` pins these files to CRLF everywhere. Flags changed files,
|
||||||
|
missing files, **and scripts present on disk but absent from the manifest**.
|
||||||
|
It's an integrity check, not a signature — the manifest sits in the same repo
|
||||||
|
as the code, so it catches corruption and accidental drift, not a compromised
|
||||||
|
repo. A Pester test fails if the manifest ever goes stale.
|
||||||
- **Test suite (Pester)** — the toolkit now has automated coverage of its own
|
- **Test suite (Pester)** — the toolkit now has automated coverage of its own
|
||||||
pure logic: `Get-ArchiveExcludes` (including the deploy-vs-backup rule that
|
pure logic: `Get-ArchiveExcludes` (including the deploy-vs-backup rule that
|
||||||
keeps `.env`/`uploads` out of deploys but *in* backups), config and project
|
keeps `.env`/`uploads` out of deploys but *in* backups), config and project
|
||||||
|
|||||||
38
CHECKSUMS.txt
Normal file
38
CHECKSUMS.txt
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
b3452601fafaf1799be9286c41bc9badd95aad7fe45093d3239c968f4ee16e75 setup_backup_schedule.ps1
|
||||||
|
dd39a7be920e5e9b2e99aafcc598491413b89bc902c2efd99ac9464d18f24ee1 token-count.ps1
|
||||||
|
d05ef5941a97911781d638a0a209b10affe777b1021f89071c5414471bf6b097 zbackup.cmd
|
||||||
|
90f05dc2db328d6a8797c6d3dfc0b9cb9e1c068de7180826928bae94deda2e77 zbackup.ps1
|
||||||
|
1fa2a6587f4d3a0b3cc0a2b2c265e1b520c7b7caf21e6cd9e110794e803375fa zbackup_and_sync.ps1
|
||||||
|
dc6f31069607a7f60cd45399a27a88abe01695879f0b2d9272537d63b0a71d99 zbackup_ec2.cmd
|
||||||
|
5a88b72c1e15036346e9fe8e48ba83acb6465acdb35b7d94239f03263e095ccc zbackup_ec2.ps1
|
||||||
|
c6f8da4b90d5531f245b7c55f6b4acb7e69edcc1fa2c966bc52aa8bc4f23417f zchecksums.cmd
|
||||||
|
94a96f879db64ac5f2e9f1e991c9a4a506fade5d538e3c0c7c4588124595659e zchecksums.ps1
|
||||||
|
b2c9a2f0205768e7b301c152b1f4cf042962125d4e982812befc2e536c0f7c6b zdeploy.cmd
|
||||||
|
30bc24780b2c6772f57232eee077b3295892afbb8f8298091b11e815d8a07b23 zdeploy.ps1
|
||||||
|
9b2702ac57115c2cd6df1f0ad33b0f8b4fd73a54dcd99475b9cacb1070a235d5 zec2.cmd
|
||||||
|
69696d75c1430ef0afe773880b962f08316c5ced4a118fe8abd71171c7e9359b zec2.ps1
|
||||||
|
2fbd24b329f90a1b6982e3f4b8fc9068fc99e900fe718eb9ff2af7c6a98dfb4f zec2_rotatekeys.cmd
|
||||||
|
a0016e48c3b4b9b1f7e800315bbef7d2b2ca3267bf51a0ce31e41a28cb74b71b zec2_rotatekeys.ps1
|
||||||
|
60e7ab58ca786b1a075ac268e4c2f8454bfcff47ed077cd0219f74c8d0eeaf78 zec2online.cmd
|
||||||
|
0177d8fbb6fba66a82fdde437ed9e7d43f3ef1e2227a418e9783380c549066cf zec2online.ps1
|
||||||
|
77060d8e30cc43ce9114c58e9f2f2f76d647e3237a3357299ca1820131128636 ZHelpers.ps1
|
||||||
|
79074a4dbd4d9fa0388cdac6ce0543a72d2b09c4779750d453c7566e500868ab zkill.cmd
|
||||||
|
d9ca58bf5baf8be8eb90fb8d414f55cd33deee35ea063dc4df7d9c9d4ed0e72c zkill.ps1
|
||||||
|
d419dd7d407ac19b0ca83a54b622399c2343d63bfe53c1ff05e7b5b15c0c7a0f ZKiller.ps1
|
||||||
|
ba9b2204945b19b912420290361191661a80887ecfd5d2c31d32b0686382e9a0 ZKillOnly.ps1
|
||||||
|
462ccd1d8a8600a76885a8a62a11ba64e3123bc273815ee1c7197d2bca48d7bf zrepair.cmd
|
||||||
|
e16f4971d58ccaa9a9b0de1dedc0eb0acc126d195628831e186f2855e7895d0d zrepair.ps1
|
||||||
|
eaa25fce9a0564df24378bedfed0815eb24c4faf9a16fdd490dafe22bcf81f4b zrestart.cmd
|
||||||
|
c1909e96e158a24f17a52053f3faeacbcbd366011bbcb1bd8a4c83d381f0355b zrestart.ps1
|
||||||
|
ec2161b28bd9ab89fe7212a153fb948a29de6ef987b7d794ee3f8c605fd60cc6 zrestartd.cmd
|
||||||
|
48debde34be2910de9c084b155894e502f0b9447314564a6c12ffd64973c4d91 zsetup.cmd
|
||||||
|
5ec860041f07487b3322d47a874daad93dc67b1dd009204201956f3137c34205 zsetup.ps1
|
||||||
|
b5d61fc4105fefb738653f5dfd3440d6c9126c63925e42a2d0ba2adc26afdbbc zsetup_mail.ps1
|
||||||
|
08b943c5eb9d085274a56179f7bcb56740aefd0cfb166d45cfc65d0e6cf8ec31 zstart.cmd
|
||||||
|
e54c85132f99c6a435091758e9bbda3399a1a6833bf6ab9962362b8de62fb46b zstart.ps1
|
||||||
|
d416fc6b781e3f130de79791c54d10b74402de0eca8582b0efcb91e4252b0d30 zstart_docker.cmd
|
||||||
|
77d498086653961c4b6febffaad889652224d46a47473273aa40acfca4916ba7 zstart_docker.ps1
|
||||||
|
0edc7c975dfbe3cb9cd6c3c63e3e43b3b49e92a89e317283acd31a29d04d3239 zstartd.cmd
|
||||||
|
09aa986839f3a969c06ca1300365b2eb5ae0da49b27275aa1cfb9c2cf00517bf zstop.ps1
|
||||||
|
7102ec4d459c663aeb571fa2527c069ab0d82a038e4d1ed115d107506226aa14 zsync.cmd
|
||||||
|
4e40a178eda57e986bd050f95d98ea15aa1197a9edde8df64a468c4b2948d89f zsync.ps1
|
||||||
27
README.md
27
README.md
@ -131,6 +131,7 @@ The `.cmd` wrappers are the everyday interface. Every command takes one or more
|
|||||||
| `zbackup_ec2 [<key> ...]` | Pull DB dumps + server-side data files down from the server |
|
| `zbackup_ec2 [<key> ...]` | Pull DB dumps + server-side data files down from the server |
|
||||||
| `zsync [<key>]` | Copy new backups offsite (or build + mirror a vite dist) |
|
| `zsync [<key>]` | Copy new backups offsite (or build + mirror a vite dist) |
|
||||||
| `zstart_docker` | Run a local docker compose stack from `scriptsRoot\docker\` |
|
| `zstart_docker` | Run a local docker compose stack from `scriptsRoot\docker\` |
|
||||||
|
| `zchecksums [-Update]` | Verify every script against `CHECKSUMS.txt` (SHA-256) |
|
||||||
|
|
||||||
### Local development
|
### Local development
|
||||||
|
|
||||||
@ -385,6 +386,32 @@ Give the project a `verify` block instead, and `zdeploy` checks the app **from t
|
|||||||
2. That's it: `zstart`, `zkill`, `zrestart`, `zbackup`, `zdeploy`, `zec2`, `zec2online`, `zrepair`, `zstop` all accept the new key immediately.
|
2. That's it: `zstart`, `zkill`, `zrestart`, `zbackup`, `zdeploy`, `zec2`, `zec2online`, `zrepair`, `zstop` all accept the new key immediately.
|
||||||
3. A project whose deploy doesn't fit the python/vite/nextjs/edge/docker patterns needs its own `Invoke-<Kind>Deploy` function in `zdeploy.ps1` — copy an existing handler; they're all variations on zip → upload → compose up → verify.
|
3. A project whose deploy doesn't fit the python/vite/nextjs/edge/docker patterns needs its own `Invoke-<Kind>Deploy` function in `zdeploy.ps1` — copy an existing handler; they're all variations on zip → upload → compose up → verify.
|
||||||
|
|
||||||
|
## Verifying what you downloaded
|
||||||
|
|
||||||
|
`CHECKSUMS.txt` holds a SHA-256 for every `.ps1` and `.cmd` in the repo. Check them before running anything:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
zchecksums
|
||||||
|
```
|
||||||
|
|
||||||
|
Or with the standard tool on Linux/macOS/WSL — the manifest is `sha256sum` format:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sha256sum -c CHECKSUMS.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
The hashes are identical on every platform: `.gitattributes` pins `.ps1`/`.cmd` to CRLF everywhere, so a file is byte-for-byte the same whether you cloned on Windows or Linux.
|
||||||
|
|
||||||
|
`zchecksums` flags three things — a file whose contents changed, a listed file that's gone, and a script on disk that **isn't** in the manifest (so something added quietly still gets noticed). It exits non-zero on any of them.
|
||||||
|
|
||||||
|
If you edit a script yourself, regenerate and commit the manifest with it:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
zchecksums -Update
|
||||||
|
```
|
||||||
|
|
||||||
|
**What this does and doesn't prove.** `CHECKSUMS.txt` lives in the same repo as the scripts, so anyone who could alter a script could alter the manifest too. It's an integrity check, not a signature: it reliably catches a truncated clone, a local edit you forgot about, or a file added outside a commit. It does *not* prove the code came from this project — for that you'd need a signature or a hash published outside this repo.
|
||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
|
|
||||||
The toolkit has its own [Pester](https://pester.dev) suite covering the pure logic — the exclude lists, config lookups, and version-label formatting that the deploy and backup paths depend on:
|
The toolkit has its own [Pester](https://pester.dev) suite covering the pure logic — the exclude lists, config lookups, and version-label formatting that the deploy and backup paths depend on:
|
||||||
|
|||||||
111
tests/Checksums.Tests.ps1
Normal file
111
tests/Checksums.Tests.ps1
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
# Evomedia.net Token Savers - https://github.com/kellymichels/zscripts-token-savers
|
||||||
|
# Created by Kelly Michels - dev@evomedia.net
|
||||||
|
# Licensed under the MIT License. See LICENSE.
|
||||||
|
|
||||||
|
# Checksums.Tests.ps1 - keeps CHECKSUMS.txt honest.
|
||||||
|
#
|
||||||
|
# Invoke-Pester .\tests
|
||||||
|
#
|
||||||
|
# A checksum manifest is worse than useless once it drifts: it either cries wolf
|
||||||
|
# on every run until people stop reading it, or it quietly stops covering a new
|
||||||
|
# script. These tests fail the moment the manifest and the scripts disagree, so
|
||||||
|
# "forgot to run zchecksums -Update" surfaces here rather than in a user's
|
||||||
|
# verification run.
|
||||||
|
|
||||||
|
BeforeAll {
|
||||||
|
$script:RepoRoot = Split-Path -Parent $PSScriptRoot
|
||||||
|
$script:Manifest = Join-Path $script:RepoRoot "CHECKSUMS.txt"
|
||||||
|
|
||||||
|
# Same set zchecksums.ps1 covers: top-level executables.
|
||||||
|
$script:Covered = @(
|
||||||
|
Get-ChildItem -LiteralPath $script:RepoRoot -File |
|
||||||
|
Where-Object { $_.Extension -in @('.ps1', '.cmd') } |
|
||||||
|
Sort-Object Name
|
||||||
|
)
|
||||||
|
|
||||||
|
$script:Listed = [ordered]@{}
|
||||||
|
if (Test-Path -LiteralPath $script:Manifest) {
|
||||||
|
foreach ($line in [IO.File]::ReadAllLines($script:Manifest)) {
|
||||||
|
if ($line -match '^([0-9a-fA-F]{64})\s+(.+)$') {
|
||||||
|
$script:Listed[$Matches[2].Trim()] = $Matches[1].ToLowerInvariant()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Describe "CHECKSUMS.txt" {
|
||||||
|
It "exists" {
|
||||||
|
Test-Path -LiteralPath $script:Manifest | Should -BeTrue
|
||||||
|
}
|
||||||
|
|
||||||
|
# NOTE: no angle brackets in It names - Pester treats them as -ForEach data
|
||||||
|
# placeholders and tries to evaluate the contents as an expression.
|
||||||
|
It "is sha256sum-compatible: 64 hex chars, two spaces, then the filename" {
|
||||||
|
# Anything else and `sha256sum -c CHECKSUMS.txt` warns or bails, which
|
||||||
|
# is half the point of publishing it.
|
||||||
|
foreach ($line in [IO.File]::ReadAllLines($script:Manifest)) {
|
||||||
|
if (-not $line.Trim()) { continue }
|
||||||
|
$line | Should -Match '^[0-9a-f]{64} \S.*$'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
It "uses LF line endings" {
|
||||||
|
# A trailing CR becomes part of the filename for sha256sum, so every
|
||||||
|
# entry would report as missing on Linux/macOS.
|
||||||
|
([IO.File]::ReadAllText($script:Manifest)) | Should -Not -Match "`r"
|
||||||
|
}
|
||||||
|
|
||||||
|
It "is sorted by filename" {
|
||||||
|
$names = @($script:Listed.Keys)
|
||||||
|
($names -join ',') | Should -Be (($names | Sort-Object) -join ',')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Describe "manifest matches what is on disk" {
|
||||||
|
It "lists every top-level .ps1 / .cmd (nothing silently uncovered)" {
|
||||||
|
$missingFromManifest = @($script:Covered.Name | Where-Object { -not $script:Listed.Contains($_) })
|
||||||
|
$missingFromManifest -join ', ' | Should -BeNullOrEmpty -Because "these scripts are not in CHECKSUMS.txt - run 'zchecksums -Update'"
|
||||||
|
}
|
||||||
|
|
||||||
|
It "lists nothing that no longer exists" {
|
||||||
|
$onDisk = @($script:Covered.Name)
|
||||||
|
$stale = @($script:Listed.Keys | Where-Object { $onDisk -notcontains $_ })
|
||||||
|
$stale -join ', ' | Should -BeNullOrEmpty -Because "these entries point at files that are gone - run 'zchecksums -Update'"
|
||||||
|
}
|
||||||
|
|
||||||
|
It "records the current hash of <_>" -ForEach @(
|
||||||
|
(Get-ChildItem -LiteralPath (Split-Path -Parent $PSScriptRoot) -File |
|
||||||
|
Where-Object { $_.Extension -in @('.ps1', '.cmd') } |
|
||||||
|
Sort-Object Name | Select-Object -ExpandProperty Name)
|
||||||
|
) {
|
||||||
|
$name = $_
|
||||||
|
$script:Listed.Contains($name) | Should -BeTrue -Because "$name is missing from CHECKSUMS.txt"
|
||||||
|
$actual = (Get-FileHash -LiteralPath (Join-Path $script:RepoRoot $name) -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||||
|
$actual | Should -Be $script:Listed[$name] -Because "$name changed since CHECKSUMS.txt was written - run 'zchecksums -Update'"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Describe "zchecksums.ps1" {
|
||||||
|
It "verifies clean and exits 0 against the committed manifest" {
|
||||||
|
$out = & powershell -NoProfile -ExecutionPolicy Bypass -File (Join-Path $script:RepoRoot "zchecksums.ps1") -Quiet 2>&1
|
||||||
|
$LASTEXITCODE | Should -Be 0 -Because ($out -join "`n")
|
||||||
|
}
|
||||||
|
|
||||||
|
It "exits non-zero when a covered file has been tampered with" {
|
||||||
|
# Proves the check actually detects a modified script rather than always
|
||||||
|
# passing. Appends a byte to a real script, verifies, then restores it.
|
||||||
|
$victim = Join-Path $script:RepoRoot "zchecksums.cmd"
|
||||||
|
$original = [IO.File]::ReadAllBytes($victim)
|
||||||
|
try {
|
||||||
|
[IO.File]::AppendAllText($victim, "REM tampered`r`n")
|
||||||
|
& powershell -NoProfile -ExecutionPolicy Bypass -File (Join-Path $script:RepoRoot "zchecksums.ps1") -Quiet *> $null
|
||||||
|
$LASTEXITCODE | Should -Be 1
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
[IO.File]::WriteAllBytes($victim, $original)
|
||||||
|
}
|
||||||
|
# Restored, so a normal verify passes again.
|
||||||
|
& powershell -NoProfile -ExecutionPolicy Bypass -File (Join-Path $script:RepoRoot "zchecksums.ps1") -Quiet *> $null
|
||||||
|
$LASTEXITCODE | Should -Be 0
|
||||||
|
}
|
||||||
|
}
|
||||||
6
zchecksums.cmd
Normal file
6
zchecksums.cmd
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
REM Evomedia.net Token Savers — https://github.com/kellymichels/zscripts-token-savers
|
||||||
|
REM Created by Kelly Michels · dev@evomedia.net
|
||||||
|
REM Licensed under the MIT License. See LICENSE.
|
||||||
|
|
||||||
|
@echo off
|
||||||
|
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0zchecksums.ps1" %*
|
||||||
143
zchecksums.ps1
Normal file
143
zchecksums.ps1
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
# Evomedia.net Token Savers — https://github.com/kellymichels/zscripts-token-savers
|
||||||
|
# Created by Kelly Michels · dev@evomedia.net
|
||||||
|
# Licensed under the MIT License. See LICENSE.
|
||||||
|
|
||||||
|
# zchecksums.ps1 - verify (or regenerate) SHA-256 checksums for the scripts.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# zchecksums verify every script against CHECKSUMS.txt
|
||||||
|
# zchecksums -Update regenerate CHECKSUMS.txt after changing a script
|
||||||
|
# zchecksums -Quiet verify, print only the summary line
|
||||||
|
#
|
||||||
|
# Exit codes: 0 = everything matches, 1 = a mismatch, missing or unlisted file.
|
||||||
|
#
|
||||||
|
# WHAT THIS DOES AND DOES NOT PROTECT AGAINST
|
||||||
|
# -------------------------------------------
|
||||||
|
# CHECKSUMS.txt lives in the same repo as the scripts, so anyone able to modify
|
||||||
|
# a script can also modify the manifest. This is an integrity check, not a
|
||||||
|
# signature. It reliably catches:
|
||||||
|
#
|
||||||
|
# * a truncated or corrupted download / clone
|
||||||
|
# * a file edited locally that you forgot about
|
||||||
|
# * a script added or removed without going through a commit
|
||||||
|
#
|
||||||
|
# It does NOT prove the code came from this project - only a signature (GPG,
|
||||||
|
# Sigstore) or a hash published somewhere outside this repo can do that. Compare
|
||||||
|
# against the copy on GitHub if you need that assurance.
|
||||||
|
#
|
||||||
|
# Only *.ps1 and *.cmd are covered: they are what you actually execute, and
|
||||||
|
# .gitattributes pins them to CRLF on every platform, so their hashes are
|
||||||
|
# identical on Windows, Linux and macOS. Files without that pin would hash
|
||||||
|
# differently per platform and are deliberately left out.
|
||||||
|
#
|
||||||
|
# RUN -Update ON A CLEAN CHECKOUT
|
||||||
|
# ------------------------------
|
||||||
|
# Hash whatever is on disk. That is only the same as what a user clones if the
|
||||||
|
# working copy matches git's normalised form. An editor that writes LF leaves a
|
||||||
|
# file git still considers unchanged (it normalises to LF in the index either
|
||||||
|
# way), so the file sits there with LF while every clone gets CRLF - and the
|
||||||
|
# manifest generated from it fails for everyone else. If in doubt:
|
||||||
|
#
|
||||||
|
# git status --short # must be clean
|
||||||
|
# git rm -r --cached . ; git reset --hard # or delete the scripts and
|
||||||
|
# # `git checkout -- .`
|
||||||
|
#
|
||||||
|
# then re-run -Update. The surest check is to clone the repo somewhere else and
|
||||||
|
# run `sha256sum -c CHECKSUMS.txt` there.
|
||||||
|
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[switch]$Update,
|
||||||
|
[switch]$Quiet
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
$ManifestName = "CHECKSUMS.txt"
|
||||||
|
$Manifest = Join-Path $PSScriptRoot $ManifestName
|
||||||
|
|
||||||
|
# The covered set: executable scripts, top level only. Sorted for a stable file.
|
||||||
|
function Get-CoveredFiles {
|
||||||
|
Get-ChildItem -LiteralPath $PSScriptRoot -File |
|
||||||
|
Where-Object { $_.Extension -in @('.ps1', '.cmd') } |
|
||||||
|
Sort-Object Name
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-Sha256([string]$Path) {
|
||||||
|
return (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||||
|
}
|
||||||
|
|
||||||
|
# sha256sum-compatible: "<hash> <name>", LF endings, so `sha256sum -c` works on
|
||||||
|
# Linux/macOS as well as this script on Windows.
|
||||||
|
function Write-Manifest($Files) {
|
||||||
|
$lines = foreach ($f in $Files) { "{0} {1}" -f (Get-Sha256 $f.FullName), $f.Name }
|
||||||
|
$text = ($lines -join "`n") + "`n"
|
||||||
|
[IO.File]::WriteAllText($Manifest, $text, (New-Object Text.UTF8Encoding($false)))
|
||||||
|
}
|
||||||
|
|
||||||
|
function Read-Manifest {
|
||||||
|
if (-not (Test-Path -LiteralPath $Manifest)) { return $null }
|
||||||
|
$map = [ordered]@{}
|
||||||
|
foreach ($line in [IO.File]::ReadAllLines($Manifest)) {
|
||||||
|
$t = $line.Trim()
|
||||||
|
if (-not $t -or $t.StartsWith('#')) { continue }
|
||||||
|
# "<64 hex> <name>" - two spaces is the sha256sum convention, but accept
|
||||||
|
# any run of whitespace so a hand-edited file still parses.
|
||||||
|
if ($t -match '^([0-9a-fA-F]{64})\s+(.+)$') {
|
||||||
|
$map[$Matches[2].Trim()] = $Matches[1].ToLowerInvariant()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $map
|
||||||
|
}
|
||||||
|
|
||||||
|
$files = @(Get-CoveredFiles)
|
||||||
|
|
||||||
|
if ($Update) {
|
||||||
|
Write-Manifest $files
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "=== zchecksums (updated) ===" -ForegroundColor Cyan
|
||||||
|
Write-Host (" Wrote {0} with {1} entries." -f $ManifestName, $files.Count) -ForegroundColor Green
|
||||||
|
Write-Host " Commit it alongside the script change, or verification will fail." -ForegroundColor DarkGray
|
||||||
|
Write-Host ""
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
$expected = Read-Manifest
|
||||||
|
if ($null -eq $expected) {
|
||||||
|
Write-Host "ERROR: $ManifestName not found. Run 'zchecksums -Update' to create it." -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
$ok = 0
|
||||||
|
$changed = @()
|
||||||
|
$missing = @()
|
||||||
|
$unlisted = @()
|
||||||
|
|
||||||
|
foreach ($f in $files) {
|
||||||
|
if (-not $expected.Contains($f.Name)) { $unlisted += $f.Name; continue }
|
||||||
|
if ((Get-Sha256 $f.FullName) -eq $expected[$f.Name]) { $ok++ } else { $changed += $f.Name }
|
||||||
|
}
|
||||||
|
foreach ($name in $expected.Keys) {
|
||||||
|
if (-not (Test-Path -LiteralPath (Join-Path $PSScriptRoot $name))) { $missing += $name }
|
||||||
|
}
|
||||||
|
|
||||||
|
$bad = $changed.Count + $missing.Count + $unlisted.Count
|
||||||
|
|
||||||
|
if (-not $Quiet) {
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "=== zchecksums ===" -ForegroundColor Cyan
|
||||||
|
foreach ($n in $changed) { Write-Host " CHANGED $n" -ForegroundColor Red }
|
||||||
|
foreach ($n in $missing) { Write-Host " MISSING $n (listed but not on disk)" -ForegroundColor Red }
|
||||||
|
foreach ($n in $unlisted) { Write-Host " UNLISTED $n (on disk but not in $ManifestName)" -ForegroundColor Yellow }
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($bad -eq 0) {
|
||||||
|
Write-Host (" OK - {0} file(s) match {1}." -f $ok, $ManifestName) -ForegroundColor Green
|
||||||
|
Write-Host ""
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host (" FAILED - {0} ok, {1} changed, {2} missing, {3} unlisted." -f $ok, $changed.Count, $missing.Count, $unlisted.Count) -ForegroundColor Red
|
||||||
|
Write-Host " If you changed a script on purpose, run: zchecksums -Update" -ForegroundColor DarkGray
|
||||||
|
Write-Host ""
|
||||||
|
exit 1
|
||||||
Loading…
Reference in New Issue
Block a user