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
7 changes: 6 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
**/.DS_Store
**/.DS_Store

# Runtime output folders
Logs/
Results/
Reports/
34 changes: 34 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,40 @@
The format is based on and uses the types of changes according to [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [2.1.0] - 2026-07-10

### Added

- Read-only **trust matrix** reporting:
- `Get-SPSTrustStatus` (public) — collects the current cross-farm trust state
(ROOT, STS, Published, Topology permission, SA permission, Proxy) by calling only
the read-only `Get-SPS*` functions. Never changes anything; `Content` services are
reported as N/A for STS/Published/SA-permission/Proxy; getter failures are captured
as `Error` with a per-row note instead of aborting the audit.
- `Export-SPSTrustReport` (public) — renders a status object (`-Status`) or a results
JSON file (`-InputFile`) into a self-contained, offline HTML report with summary
cards and an interactive (search + click-to-sort) trust matrix.
- `Backup-SPSJsonFile` (public) — archives the previous results JSON before overwrite.
- Private HTML report helpers: `ConvertTo-SPSHtmlEncoded`, `Get-SPSReportHtmlHead`,
`Get-SPSReportCardHtml`, `Get-SPSReportHtmlScript`.
- `SPSTrust.ps1`:
- `-ReportOnly` switch — read-only audit that skips all four configuration stages and
only collects state and writes the report.
- `-HistoryRetentionDays` parameter (default 30) — rotation of archived result
snapshots in `Results\history\`.
- A post-run reporting stage (runs after both a normal/clean run and a `-ReportOnly`
audit): writes `Results\<Application>-<Environment>.json` and
`Reports\<Application>-<Environment>.html`, archiving the previous snapshot and
pruning history first.
- Wiki: new `Reports.md` (trust matrix, output layout, regeneration) linked from the
sidebar; Home and Usage updated for the new parameters and outputs.
- Tests for the collector, the renderer, `Backup-SPSJsonFile`, and the script report
wiring.

### Changed

- `.gitignore` now excludes the runtime `Logs/`, `Results/` and `Reports/` folders.

## [2.0.0] - 2026-07-10

### Added
Expand Down
43 changes: 20 additions & 23 deletions RELEASE-NOTES.md
Original file line number Diff line number Diff line change
@@ -1,36 +1,33 @@
# SPSTrust - Release Notes

## [2.0.0] - 2026-07-10
## [2.1.0] - 2026-07-10

> [!IMPORTANT]
> This is a major release with breaking changes. The package layout moved from
> `scripts/` to `src/`, and the configuration format changed from JSON to a PowerShell
> data file (`.psd1`). Convert your JSON configuration to the equivalent `.psd1` hashtable
> (see the [Configuration](https://github.com/luigilink/SPSTrust/wiki/Configuration) wiki
> page) before upgrading.
This release adds a read-only **trust matrix** report, produced both at the end of a
normal run and in a new dedicated audit mode. It is fully backward compatible with 2.0.0.

### Added

- New reusable `SPSTrust.Common` module (Public/Private layout, manifest-driven version).
- `-LogRetentionDays` parameter with automatic transcript log rotation.
- Example `.psd1` configuration, Pester test suite, PSScriptAnalyzer settings, and a
Pester CI workflow.
- Wiki sidebar and Release Process page.
- **`-ReportOnly`** — read-only audit mode: skips all configuration stages and only
collects the current trust state and writes the report (changes nothing).
- **Trust matrix report** — every run now writes a JSON snapshot
(`Results\<Application>-<Environment>.json`) and a self-contained, offline HTML report
(`Reports\<Application>-<Environment>.html`) showing, per publishing-farm /
consuming-farm / service, the state of each trust dimension (ROOT, STS, Published,
Topology permission, SA permission, Proxy) as Present / Absent / N/A / Error.
- New public functions in `SPSTrust.Common`: `Get-SPSTrustStatus` (read-only collector),
`Export-SPSTrustReport` (HTML renderer, also usable standalone via `-InputFile`) and
`Backup-SPSJsonFile`.
- **`-HistoryRetentionDays`** (default 30) — rotation of archived result snapshots in
`Results\history\`.
- Wiki: new **Reports & Audit** page.

### Changed

- **BREAKING**: `scripts/` → `src/`; release archives extract to `SPSTrust.ps1` + `Modules/`.
- **BREAKING**: configuration is now a `.psd1` data file loaded with `Import-PowerShellDataFile`.
- Script version sourced from the module manifest.
- Workflows bumped (`checkout@v7`, `action-gh-release@v3`); README trimmed to defer to the wiki.
- `.gitignore` excludes the runtime `Logs/`, `Results/` and `Reports/` folders.

### Fixed
### Compatibility

- Wiki `Usage` examples (wrong script name and a missing mandatory `-FarmAccount`).

### Removed

- `scripts/` folder, the monolithic `sps.util.psm1` / `util.psm1` modules, and the unused
`Clear-SPSLog` helper.
- No breaking changes. Existing `-ConfigFile` / `-FarmAccount` / `-CleanServices` usage is
unchanged; the reporting stage is additive and read-only.

A full list of changes in each version can be found in the [change log](CHANGELOG.md).
50 changes: 50 additions & 0 deletions src/Modules/SPSTrust.Common/Private/ConvertTo-SPSHtmlEncoded.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
function ConvertTo-SPSHtmlEncoded {
<#
.SYNOPSIS
HTML-encodes a string for safe insertion into generated HTML reports.

.DESCRIPTION
Replaces the five characters that are significant in HTML markup
(& < > " ') with their entity equivalents. Used by
Export-SPSTrustReport to neutralize values (database names,
site/web IDs, logins, messages) before baking them into the report.

Returns an empty string for null or empty input.

.PARAMETER Value
The string to encode.

.EXAMPLE
ConvertTo-SPSHtmlEncoded -Value 'SERVICES <prod> & "co"'
# SERVICES &lt;prod&gt; &amp; &quot;co&quot;
#>
[CmdletBinding()]
[OutputType([System.String])]
param
(
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[AllowEmptyString()]
[AllowNull()]
[System.String]
$Value
)

process {
if ([string]::IsNullOrEmpty($Value)) {
return ''
}

$sb = [System.Text.StringBuilder]::new()
foreach ($char in $Value.ToCharArray()) {
switch ($char) {
'&' { [void]$sb.Append('&amp;') }
'<' { [void]$sb.Append('&lt;') }
'>' { [void]$sb.Append('&gt;') }
'"' { [void]$sb.Append('&quot;') }
"'" { [void]$sb.Append('&#39;') }
default { [void]$sb.Append($char) }
}
}
return $sb.ToString()
}
}
45 changes: 45 additions & 0 deletions src/Modules/SPSTrust.Common/Private/Get-SPSReportCardHtml.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
function Get-SPSReportCardHtml {
<#
.SYNOPSIS
Builds the HTML for one summary "card" (a big number plus a label).

.PARAMETER Value
The value shown as the big number.

.PARAMETER Label
The label shown under the value.

.PARAMETER Sub
Optional sub-label shown under the label.

.PARAMETER Tone
Optional visual tone: '', 'accent', 'clean' (green) or 'alert' (red).
#>
[CmdletBinding()]
[OutputType([System.String])]
param
(
[Parameter(Mandatory = $true)]
$Value,

[Parameter(Mandatory = $true)]
[System.String]
$Label,

[Parameter()]
[System.String]
$Sub = '',

[Parameter()]
[ValidateSet('', 'accent', 'clean', 'alert')]
[System.String]
$Tone = ''
)

$encValue = ConvertTo-SPSHtmlEncoded -Value ("$Value")
$encLabel = ConvertTo-SPSHtmlEncoded -Value $Label
$encSub = ConvertTo-SPSHtmlEncoded -Value $Sub
$toneClass = if ([string]::IsNullOrEmpty($Tone)) { '' } else { " $Tone" }
$subHtml = if ([string]::IsNullOrEmpty($encSub)) { '' } else { "<div class=`"card-sub`">$encSub</div>" }
return "<div class=`"card$toneClass`"><div class=`"card-value`">$encValue</div><div class=`"card-label`">$encLabel</div>$subHtml</div>"
}
58 changes: 58 additions & 0 deletions src/Modules/SPSTrust.Common/Private/Get-SPSReportHtmlHead.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
function Get-SPSReportHtmlHead {
<#
.SYNOPSIS
Returns the document head (with the embedded stylesheet) and the opening body tag.

.DESCRIPTION
Emits a self-contained <head> block (no CDN, works offline on a SharePoint
server) with the embedded SPSTrust report stylesheet, followed by the opening
<body> tag. The stylesheet includes the status "pill" styles used to render the
trust matrix (present / absent / n-a).

.PARAMETER Title
Page title used in the <title> element.
#>
[CmdletBinding()]
[OutputType([System.String])]
param
(
[Parameter(Mandatory = $true)]
[System.String]
$Title
)

$css = @'
:root{--brand:#1f6fb2;--brand-dark:#155a91;--ink:#222;--muted:#666;--line:#e3e3e3;--zebra:#f7f9fb;--ok:#2e9b57;--warn:#c19c00;--alert:#c0392b}
*{box-sizing:border-box}
body{font-family:'Aptos','Segoe UI',-apple-system,BlinkMacSystemFont,sans-serif;color:var(--ink);margin:0;padding:24px;background:#fff}
h1{color:var(--brand);font-size:22px;margin:0 0 4px}
h2{color:var(--brand);font-size:16px;margin:24px 0 8px;border-bottom:2px solid var(--brand);padding-bottom:4px}
h3{color:var(--brand-dark);font-size:13px;margin:0 0 6px}
.meta{color:var(--muted);font-size:12px;margin-bottom:16px}
.summary{background:#eef5fb;border:1px solid #cfe0ef;border-left:4px solid var(--brand);border-radius:6px;padding:16px;margin-bottom:8px}
.cards{display:flex;flex-wrap:wrap;gap:12px}
.card{background:#fff;border:1px solid var(--line);border-radius:6px;padding:12px 16px;min-width:120px}
.card-value{font-size:24px;font-weight:700;color:var(--brand)}
.card-label{font-size:12px;color:var(--muted)}
.card-sub{font-size:11px;color:var(--muted);margin-top:2px}
.card.accent{background:#eef5fb;border-color:#cfe0ef}
.card.clean .card-value{color:var(--ok)}
.card.alert .card-value{color:var(--alert)}
table{border-collapse:collapse;width:100%;font-size:12px}
th,td{text-align:left;padding:6px 8px;border-bottom:1px solid var(--line);vertical-align:top;word-break:break-word}
th{background:var(--brand);color:#fff;cursor:pointer;user-select:none;position:sticky;top:0}
td.num,th.num{text-align:right}
tbody tr:nth-child(even){background:var(--zebra)}
.controls{display:flex;justify-content:space-between;align-items:center;margin:12px 0;flex-wrap:wrap;gap:8px}
.search{padding:6px 10px;border:1px solid var(--line);border-radius:4px;font-size:13px;width:280px;max-width:100%}
.info{color:var(--muted);font-size:12px}
.pill{display:inline-block;min-width:64px;text-align:center;padding:2px 8px;border-radius:10px;font-size:11px;font-weight:600;line-height:1.4}
.pill-present{background:#e6f4ec;color:var(--ok);border:1px solid #bfe2cd}
.pill-absent{background:#fbeceb;color:var(--alert);border:1px solid #f0c9c5}
.pill-na{background:#f0f0f0;color:var(--muted);border:1px solid #e0e0e0}
.empty{color:var(--ok);font-size:13px;margin:8px 0 20px;font-weight:600}
.footer{color:var(--muted);font-size:11px;margin-top:24px;border-top:1px solid var(--line);padding-top:8px}
'@

return "<!DOCTYPE html><html lang=`"en`"><head><meta charset=`"utf-8`"><meta name=`"viewport`" content=`"width=device-width, initial-scale=1`"><title>$Title</title><style>$css</style></head><body>"
}
71 changes: 71 additions & 0 deletions src/Modules/SPSTrust.Common/Private/Get-SPSReportHtmlScript.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
function Get-SPSReportHtmlScript {
<#
.SYNOPSIS
Returns the vanilla-JavaScript block that makes the trust matrix table interactive.

.DESCRIPTION
Operates on the static <table id="trust-matrix"> rendered server-side by
Export-SPSTrustReport (so the status "pills" are real HTML). It wires:

- a search box (id="matrix-search") that shows/hides rows by matching text in
any cell (case-insensitive), and
- clickable column headers that sort the visible rows. Sorting uses each cell's
textContent, which for status columns is the pill label (Present/Absent/N/A).

The row count indicator (id="matrix-info") is updated on every filter. No
external dependency (works offline on a SharePoint server).
#>
[CmdletBinding()]
[OutputType([System.String])]
param ()

$js = @'
(function(){
var table = document.getElementById('trust-matrix');
if (!table) { return; }
var tbody = table.tBodies[0];
var search = document.getElementById('matrix-search');
var info = document.getElementById('matrix-info');
var allRows = Array.prototype.slice.call(tbody.rows);
var sortCol = -1, sortDir = 1;

function cellText(row, i){
var c = row.cells[i];
return c ? (c.textContent || '').trim().toLowerCase() : '';
}

function applyFilter(){
var q = (search && search.value || '').trim().toLowerCase();
var shown = 0;
allRows.forEach(function(r){
var match = !q || (r.textContent || '').toLowerCase().indexOf(q) !== -1;
r.style.display = match ? '' : 'none';
if (match) { shown++; }
});
if (info) { info.textContent = shown + ' / ' + allRows.length + ' rows'; }
}

function applySort(col){
if (sortCol === col) { sortDir = -sortDir; } else { sortCol = col; sortDir = 1; }
var rows = allRows.slice();
rows.sort(function(a,b){
var x = cellText(a, col), y = cellText(b, col);
if (x < y) { return -1 * sortDir; }
if (x > y) { return 1 * sortDir; }
return 0;
});
rows.forEach(function(r){ tbody.appendChild(r); });
}

var headers = table.tHead ? table.tHead.rows[0].cells : [];
Array.prototype.forEach.call(headers, function(th, i){
th.addEventListener('click', function(){ applySort(i); });
});

if (search) { search.addEventListener('input', applyFilter); }
applyFilter();
})();
'@

return "<script>$js</script>"
}
Loading
Loading