test(security): gate seeded parser campaigns and record assurance

Refs #30, #31. Add real parser properties and replay evidence; retain unenrolled badge and signing requirements without bypasses.

Co-Authored-By: Codex <noreply@openai.com>
Signed-off-by: James Maes <jmaes@dmdbrands.com>
This commit is contained in:
James MaesandCodex committed 2026-10-11 23:31:46 -05:00
1 parent 82256f362e
commit e6fd2818ff
15 files changed
+670 -17

No files matched your search

+66
View File
@@ -0,0 +1,66 @@
# K22-TEST-25: reproducible security-property campaigns, without browser execution or profile writes.
name: fuzz
on:
pull_request:
push:
branches: [main]
schedule:
- cron: "34 6 * * 1"
workflow_dispatch:
workflow_call:
permissions: {}
concurrency:
group: fuzz-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
fuzzing:
name: fuzzing
runs-on: windows-latest
timeout-minutes: 15
permissions:
contents: read
env:
K22_FUZZ_ITERATIONS: ${{ (github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/')) && '10000' || '500' }}
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Set up .NET
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
global-json-file: global.json
- name: Restore and build locked core targets
shell: pwsh
run: |
dotnet restore tests/MarkdownViewr.Windows.Core.Tests -p:Platform=x64 --locked-mode
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
dotnet build tests/MarkdownViewr.Windows.Core.Tests -c Release -p:Platform=x64 -warnaserror --no-restore
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
- name: Restore and build activation adapter targets
shell: pwsh
run: |
dotnet restore tests/MarkdownViewr.Windows.UiTests -p:Platform=x64 --locked-mode
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
dotnet build tests/MarkdownViewr.Windows.UiTests -c Release -p:Platform=x64 -warnaserror --no-restore
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
- name: Verify seeded parser security properties
shell: pwsh
run: |
Write-Output "Fuzz seeds22023/99173, five targets, $env:K22_FUZZ_ITERATIONS iterations per seed/target."
dotnet test tests/MarkdownViewr.Windows.Core.Tests -c Release -p:Platform=x64 --no-build --no-restore --filter Category=Fuzz --logger 'trx;LogFileName=fuzz.trx' --results-directory artifacts/fuzz
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
dotnet test tests/MarkdownViewr.Windows.UiTests -c Release -p:Platform=x64 --no-build --no-restore --filter Category=Fuzz --logger 'trx;LogFileName=activation-fuzz.trx' --results-directory artifacts/fuzz
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
- name: Retain synthetic campaign failures and results
if: always()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: fuzz-results
path: artifacts/fuzz/*.trx
if-no-files-found: error
retention-days: 30
+7 -1
View File
@@ -87,8 +87,14 @@ jobs:
permissions:
contents: read
fuzzing:
needs: verify
uses: ./.github/workflows/fuzz.yml
permissions:
contents: read
release:
needs: [verify, gates, security, codeql, scripts]
needs: [verify, gates, security, codeql, scripts, fuzzing]
uses: KofTwentyTwo/standards/.github/workflows/release-velopack.yml@fdf5ed029603c938615a32b1beca3628b8787945 # v0.1.2
permissions:
contents: write # create and publish the GitHub release
+20 -2
View File
@@ -34,9 +34,17 @@ dotnet format MarkdownViewr.Windows.slnx --verify-no-changes --severity warn --n
# PowerShell tooling gate (install PSScriptAnalyzer 1.25.0 once)
Import-Module PSScriptAnalyzer -RequiredVersion 1.25.0
Invoke-ScriptAnalyzer -Path build -Recurse -Settings ./PSScriptAnalyzerSettings.psd1 -EnableExit
# Load repository command metadata; keep casing separate from other analyzer rules.
Import-Module ./build/GitHubRepository.psm1 -Force
foreach($script in Get-ChildItem build -File | Where-Object Extension -In '.ps1', '.psm1', '.psd1') {
$findings = @(
Invoke-ScriptAnalyzer -Path $script.FullName -Settings ./PSScriptAnalyzerSettings.psd1 -IncludeRule PSUseCorrectCasing
Invoke-ScriptAnalyzer -Path $script.FullName -Settings ./PSScriptAnalyzerSettings.psd1 -ExcludeRule PSUseCorrectCasing
)
if($findings.Count -ne 0) { $findings | Format-Table; throw 'Script analysis failed.' }
}
Import-Module Pester -RequiredVersion 5.7.1
Invoke-Pester -Path build/Assert-Coverage.Tests.ps1 -CI
Invoke-Pester -Path build/*.Tests.ps1 -CI
```
`dotnet format` (without `--verify-no-changes`) fixes layout, `var` use, usings, and
@@ -85,6 +93,7 @@ pre-commit install --hook-type pre-commit --hook-type commit-msg
| `pr / title` | The PR title is a Conventional Commit header |
| `pr / dco` | Every commit is signed off |
| `pr / dependency-review` | Added dependencies have no known vulnerabilities and an allowed license |
| `fuzzing` | Repeatable parser security properties; longer weekly and release campaigns |
| `security / secrets`, `security / sca`, `security / workflows` | No secrets, no vulnerable or malicious dependencies, safe workflows |
| `codeql / analyze (csharp)`, `codeql / analyze (actions)` | No high-severity static analysis findings |
| `ci / build-test` | Locked restore, zero-warning build, unit tests, 100% coverage gate |
@@ -105,6 +114,15 @@ sequential `Invoke-ScriptAnalyzer` calls; evaluate findings from both.
Run PSScriptAnalyzer with `PSScriptAnalyzerSettings.psd1` and Pester against
`build/*.Tests.ps1`. New owners follow [repository setup](docs/repository-setup.md).
## Parser campaigns
Run the [seeded parser targets](docs/security/fuzzing.md) when changing document,
source, URL, theme-import or activation boundaries. The required `fuzzing` check
runs PR/main budgets; weekly and release gates use longer campaigns. Retain and
minimize failing synthetic inputs into regression cases. Never execute generated
content or use private files/profiles as a corpus. Best Practices enrollment and
actual signing remain separate maintainer requirements.
## Sign-off (Developer Certificate of Origin)
By signing off a commit you certify the
+6 -4
View File
@@ -1,6 +1,6 @@
# MarkdownViewr
> In development: a Windows counterpart to [Darin Kelkhoff's macOS markdownViewr](https://github.com/darinkelkhoff/markdownViewr) ([his website](https://kelkhoff.com/)), built with AppKit. The reader, source/find/navigation/editor features and full printing pass hosted acceptance on `bf4fcdc`, including both installed update channels. Document-window acceptance and the new renderer, typography, shared appearance and theme-management controls remain in progress. Full reference parity and signed production releases are not complete.
> In development: a Windows counterpart to [Darin Kelkhoff's macOS markdownViewr](https://github.com/darinkelkhoff/markdownViewr) ([his website](https://kelkhoff.com/)), built with AppKit. Native/published and installed stable/dev reader acceptance passes on `b542dcd`; the newer `82256f3` is undergoing hosted verification. Full owner design/default-app acceptance, current standards assessment and signed production releases remain incomplete.
A Windows Markdown viewer with live reload, themes and document navigation.
@@ -10,7 +10,7 @@ A Windows Markdown viewer with live reload, themes and document navigation.
| | |
| --- | --- |
| **Tier** | Product ([scope](https://github.com/KofTwentyTwo/standards/blob/main/policies/README.md#scope)) |
| **Standards** | Adopting [KofTwentyTwo standards](https://github.com/KofTwentyTwo/standards) (pre-v1.0); built from the KofTwentyTwo Windows app template |
| **Standards** | Adopting [KofTwentyTwo standards](https://github.com/KofTwentyTwo/standards); built from the KofTwentyTwo Windows app template |
| **Latest release** | [Releases](https://github.com/KofTwentyTwo/MarkdownViewr.Windows/releases/latest) |
Settings → Advanced contains the startup splash toggle, splash duration (500–30000 milliseconds) and startup update checks. Changes save immediately and apply on the next launch. The native toolbar keeps Print visible, aligns controls in a compact row and groups reading commands.
@@ -109,6 +109,10 @@ for the full support policy.
Report vulnerabilities privately; see [SECURITY.md](SECURITY.md). The app's attack
surface is described in its [threat model](docs/security/threat-model.md).
[Parser fuzzing](docs/security/fuzzing.md) records reproducible security properties
and campaign budgets. [Best Practices enrollment](docs/security/best-practices.md)
remains incomplete; no project badge or conformance is claimed.
## Contributing
See [CONTRIBUTING.md](CONTRIBUTING.md).
@@ -119,7 +123,6 @@ See [CONTRIBUTING.md](CONTRIBUTING.md).
This Windows application is based on [Darin Kelkhoff's macOS markdownViewr](https://github.com/darinkelkhoff/markdownViewr). Visit [Darin's website](https://kelkhoff.com/). The original MIT copyright/license and adapted resource notices are included in [THIRD-PARTY-NOTICES.md](THIRD-PARTY-NOTICES.md), distributed with the app.
The current review build uses native Settings pages for long theme/editor forms,
with Back, Save and Cancel. Contents and Source start off in new windows unless
changed under Settings → Advanced; toolbar toggles affect the current reader.
@@ -129,7 +132,6 @@ reloads/stale files and offers Open folder. Disabled editor commands explain the
configuration state. These owner-review changes have passing local build/core/
adapter/style gates; current native/distributed acceptance remains pending.
Stable releases use `markdownviewr-windows`; prereleases use
`markdownviewr-windows-dev` and show **MarkdownViewr (dev)**. Each installed edition
has its own install directory, settings/recent files, activity logs and update
+9 -4
View File
@@ -48,10 +48,15 @@ minor receives security fixes for 6 months.
## Code signing
Windows installers and binaries are not yet Authenticode-signed (KofTwentyTwo standards
exception EX-0002, until the signing identity is validated). Until then, verify downloads
with `SHA256SUMS` and `gh attestation verify` as described in the
[README](README.md#verifying-a-release).
Public Windows releases require Authenticode signing of the final shipped binaries
and installers. Publication fails closed until the approved Azure signing identity
and expected publisher, trust and timestamp are verified. No verified signed
MarkdownViewr release has been published yet; unsigned test fixtures are not releases.
Verify final signatures, `SHA256SUMS` and provenance attestations as described in the
[README](README.md#verifying-a-release) and
[release signing instructions](docs/release-signing.md). No historical exception
is claimed as permission to publish unsigned application releases.
## Published vulnerabilities
+2 -2
View File
@@ -29,11 +29,11 @@ Describe 'Offline setup preview location' `
Describe 'Required checks match standalone workflow jobs' `
{
It 'requires the actual check names from both CI and script workflows' `
It 'requires the actual check names from CI, script and fuzz workflows' `
{
$plan = Get-GitHubRepositoryPlan -Repository 'Example/Notes'
$workflowRoot = Join-Path (Split-Path -Parent $PSScriptRoot) '.github/workflows'
foreach($workflow in @('ci.yml', 'scripts.yml'))
foreach($workflow in @('ci.yml', 'scripts.yml', 'fuzz.yml'))
{
$names = @(Select-String -LiteralPath (Join-Path $workflowRoot $workflow) -Pattern '^ name: ')
$names.Count | Should -BeGreaterThan 0
+1 -1
View File
@@ -22,7 +22,7 @@ function Get-GitHubRepositoryPlan
$contexts = @('pr / title', 'pr / dco', 'pr / dependency-review', 'security / secrets',
'security / sca', 'security / workflows', 'codeql / analyze (csharp)',
'codeql / analyze (actions)', 'codeql / analyze (javascript-typescript)',
'ci / build-test', 'ci / format', 'ci / ui-tests', 'installed-updates', 'reader-regressions', 'powershell', 'javascript')
'ci / build-test', 'ci / format', 'ci / ui-tests', 'installed-updates', 'reader-regressions', 'fuzzing', 'powershell', 'javascript')
if(Test-Path -LiteralPath (Join-Path (Split-Path -Parent $PSScriptRoot) 'templates/KofTwentyTwo.Templates.WindowsApp.csproj'))
{
$contexts += 'template'
+31
View File
@@ -2,6 +2,37 @@
Updated: 2026-10-11
## Current standards delta prepared; preceding checkpoint fully accepted - 2026-10-11
Fresh published-standards checker at1cc9d01 identifies mandatory assurance record,
parser campaigns and public Product enrollment missing in this app (35 PASS,
3 FAIL,3 REVIEW); prior34/1 assessment is stale. Issues30/31 created. Implement
five real seeded property targets (Markdown/HTML/frontmatter/extensions, source/
folds, URLs, theme JSON, generated activation schema), a PR/main/weekly/release
fuzz workflow and planned required context; prepare honest badge assessment with
required scope/null project URL, never an invented badge or exception. Remove
obsolete unsigned-release guidance from SECURITY.md; signing remains fail-closed.
Local core campaigns9/9 cover80000 inputs plus the minimized regression;
activation2/2 covers20000 envelopes; zero failures/skips. Full core677/100%
measured lines, isolated full Release0 warnings/errors, format,23 setup tests and all64 script tests
pass. A campaign oracle failure treated browser-relative backslashes as Windows
file URIs; preserve failed TRXs, use actual browser URL semantics and require the
minimized foreign-image denial regression. No production defect inferred and
no existing assertion/case/deadline removed. New native assembly expectation111
and installed110 per channel; fresh hosted proof not yet available.
Exact82256f3/CI38201379210 is now terminal success:668 core/100%, focused28/28,
native/published109/109, installed stable/dev108/108 each; zero failures/skips.
Actual TRXs, coexisting-channel/registration JSON and native log retained under
current-installed-evidence-38201379210, reader-regressions-38201379210 and
current-native-82256f3.log. All actual lifecycle booleans pass; fixtures are unsigned.
The reviewed next assurance push may proceed without cancelling this evidence;
require fresh fuzzing and all original gates and add/read back the new context. Production source blobs remain unchanged. Shared signing31 approval,
Azure resource authority/final signatures, actual enrollment/inventory, final-head
human acknowledgement and owner design/chooser acceptance still required. No
Azure login retry, local GUI input, shared merge, release tag or publication.
## Application source review finished; next hosted checkpoint - 2026-10-11
All105 application C# files (41 core/64 native), native markup/project configuration
+6
View File
@@ -2,6 +2,12 @@
## Current release checklist
- [x] Exact82256f3 CI38201379210 passes668 core/100% lines, focused28, native/published109 and installed108 per channel, zero skips; actual lifecycle artifacts/logs retained.
- [x] Prepare current-standard assurance record, five seeded parser/property targets and honest unenrolled Best Practices assessment (#30/#31).
- [x] Run local long campaigns:80000 core inputs plus minimized oracle regression,20000 activation envelopes,677 core tests/100% measured lines, zero-warning Release/format and23 setup tests.
- [ ] Exact82256f3 CI38201379210 is terminal green; push the reviewed assurance/campaign delta; verify hosted fuzzing plus all native/published/installed gates and add/read back its required check.
- [ ] Enroll this exact Windows public Product in Best Practices; use its real URL/README badge and maintainer-attested assessment. Do not approve an enrollment exception or fabricate passing criteria.
- [x] Exactb542dcd all-channel CI38197169201 passes native/published109/109, focused28/28 and installed stable/dev108/108 each, zero skips; actual TRXs/lifecycle JSON retained.
- [x] Resolve full-review finding: prefix reserved COM/LPT superscript theme-export names;668 core tests/100% measured lines and format pass.
- [x] Finish application-source review:105 C# files plus markup/project configuration and relevant CI/release/installed paths; blob inventory and precise scope retained. Settings theme/editor edits already use pages.
+20
View File
@@ -0,0 +1,20 @@
{
"schemaVersion": 1,
"badge": {
"applicability": "required",
"reason": "MarkdownViewr.Windows is a public open-source Product. Enrollment and maintainer attestation are required; no real project ID has been established.",
"projectUrl": null,
"evidence": "docs/security/best-practices.md"
},
"fuzzing": {
"applicability": "required",
"reason": "Untrusted Markdown/HTML/frontmatter, source ranges/folds, resource URLs, imported theme JSON and activation JSON are parser boundaries. Seeded mutation targets verify unsafe acceptance as well as exceptions.",
"targets": [
"tests/MarkdownViewr.Windows.Core.Tests/SecurityFuzzTests.cs",
"tests/MarkdownViewr.Windows.UiTests/ActivationFuzzTests.cs"
],
"command": "dotnet test tests/MarkdownViewr.Windows.Core.Tests -c Release -p:Platform=x64 --no-build --no-restore --filter Category=Fuzz",
"workflow": ".github/workflows/fuzz.yml",
"evidence": "docs/security/fuzzing.md"
}
}
+40
View File
@@ -0,0 +1,40 @@
# OpenSSF Best Practices assessment
## Scope and ownership
MarkdownViewr.Windows is a public open-source Product; K22-REPO-12 requires real
enrollment and an evidence-backed assessment. This is the Windows repository,
not Darin Kelkhoff's original macOS project. Maintainer: @KofTwentyTwo (James Maes).
Prepared 2026-10-11 by Codex; maintainer/provider assessment has not been completed.
Reassess by 2027-01-09, and before each MINOR/MAJOR release.
No project URL/ID is established. `assurance.json` correctly records required
scope and a null project URL; the README has no invented Best Practices badge.
Enrollment remains a failing requirement, not an exception or passing badge.
## Evidence prepared for enrollment
| Area | Repository evidence and precise limit |
| --- | --- |
| Identity/license | README, LICENSE (MIT), CONTRIBUTING, public source and retained Darin/vendor notices. Shared inventory entry is proposed in standards PR #36, not merged. |
| Documentation/change control | PRODUCT/ADRs, usage/build/replay commands, linked feature/bug issues and signed/DCO topic commits in PR #3. Final-head maintainer acknowledgement remains required. |
| Security reporting | SECURITY.md supplies private GitHub vulnerability reporting, response targets and supported-version policy. Threat model records browser/filesystem/IPC/editor/update boundaries. Maintainer must attest actual operating practices. |
| Build/test | Locked dependencies, zero-warning build, formatter/analyzers, core coverage, hosted native/published/installed acceptance and the prepared parser fuzzing gate. Unsigned fixture tests do not prove signatures or arbitrary document coverage. |
| Supply chain | Full-SHA action pins, dependency review/audit, secret/workflow/CodeQL scans, linked component credits and bundled license notices. |
| Releases/crypto | Stable/dev update isolation and byte checks pass synthetic installed acceptance. Real Azure signing authority, final shipped signature/timestamp verification and signed 1.0/dev releases remain incomplete. |
| Governance | Single-maintainer policy is explicit. Automated review does not constitute independent human review; every current-head finding/acknowledgement must be genuine. |
## Maintainer actions
1. Sign into [OpenSSF Best Practices](https://www.bestpractices.dev/en) and create
the project for `https://github.com/KofTwentyTwo/MarkdownViewr.Windows`, named
MarkdownViewr for Windows. Preserve any existing project if enrollment already
exists; verify its repository identity instead of creating a duplicate.
2. Supply the actual returned project URL/ID. Set `badge.projectUrl` in
[assurance.json](assurance.json), then link its real badge from README.
3. Review every current criterion against the evidence above and actual operations.
Record incomplete items honestly; registration alone is not a passing assessment.
Update this document with the assessed URL/date and remaining criterion actions.
4. Resolve signing/release and other required criteria before claiming conformance.
An enrollment obstacle needs a reviewed dated standards exception; no exception
is approved by this document or by an agent posting a proposed draft.
+68
View File
@@ -0,0 +1,68 @@
# Parser fuzzing assessment
## Scope and properties
K22-TEST-25 applies to this public document reader. Owner: @KofTwentyTwo.
The native .NET/xUnit mutation harness uses the actual parsers and independent
DOM/path/source properties, with no new dependencies. It is deterministic
property fuzzing, not coverage-guided instrumentation or an exhaustive proof.
The existing core coverage gate remains unchanged.
| Target | Inputs and independent assertions |
| --- | --- |
| Markdown/HTML/frontmatter/extensions | Mutated synthetic markup; parsed output has no executable elements, event handlers or authored host attributes; checkboxes remain disabled. URLs use browser HTTPS-base semantics. |
| Source ranges/folds | Actual parser headings have bounded indices/offsets; literal source/copy preserves original newlines after fold/unfold; generated selection ranges fit the committed display. |
| Resource/heading URLs | Accepted once-decoded paths resolve within a synthetic root; foreign hosts and queried document commands are denied; heading targets remain bounded. Native open-handle containment has separate filesystem tests. |
| Theme JSON | Mutated real built-in schemas; invalid imports reject explicitly; accepted title/CSS cannot escape their HTML containers or introduce executable DOM. |
| Activation JSON | Real generated schema and validator; accepted envelopes contain only known properties and bounded absolute literal paths; unknown command fields, malformed syntax and excessive/invalid paths are rejected. No pipe/window/process is opened. |
The two target files and executable CI command are declared in
[assurance.json](assurance.json). Generated document/theme inputs are at most 4096
characters; activation envelopes have at most 144 small synthetic paths. No
attacker script, real document, credential or profile is used. No input causes
browser execution, network requests or native launches.
## Campaigns and replay
Run from the repository root with the pinned .NET 10 SDK on Windows:
```powershell
dotnet restore MarkdownViewr.Windows.slnx -p:Platform=x64 --locked-mode
$env:K22_FUZZ_ITERATIONS = '500'
dotnet test tests/MarkdownViewr.Windows.Core.Tests -c Release -p:Platform=x64 --filter Category=Fuzz
dotnet test tests/MarkdownViewr.Windows.UiTests -c Release -p:Platform=x64 --filter Category=Fuzz
# Weekly and before every MINOR/MAJOR release, repeat both commands with 10000.
$env:K22_FUZZ_ITERATIONS = '10000'
```
Each target uses seeds 22023 and 99173. PR/main campaigns run 500 iterations per
seed/target (5000 generated property evaluations total); weekly and every release
reuse run 10000 (100000 total). `fuzz.yml` runs on PRs, `main`, weekly and manual
requests; release.yml calls it and requires success before publication. The
`fuzzing` check is declared in the repository protection plan; apply and verify
that live protection after its first hosted run. The job has a 15-minute bound;
iteration configuration outside 1–100000 fails explicitly.
Failures retain seed, target, zero-based iteration and JSON-escaped synthetic
input in TRX/CI logs. To replay the sequence, use the same checkout/runtime/seed
and at least iteration+1 operations (a shorter budget does not reach it). Minimize
the input into a named regression before changing an assertion or production
code; retain original failure evidence. Genuine vulnerabilities use
[SECURITY.md](../../SECURITY.md), not a public issue containing private data.
## Evidence and review
Prepared 2026-10-11 by Codex; maintainer assessment pending; reassess by 2027-01-09,
on parser-boundary changes and before MINOR/MAJOR publication. The long local
campaign exposed an oracle error: System.Uri treats backslash input as a Windows
file URI even with an HTTPS base. AngleSharp's browser URL model preserves the
actual HTTPS semantics; a minimized regression still requires resource denial.
No production vulnerability is established from that oracle failure. Original
failed TRXs are retained; no existing test/assertion/deadline was removed.
Corrected core campaign passes 9/9 at 10000, zero skips (80000 generated inputs plus
the minimized regression). Final activation passes 2/2 at 10000, zero skips (20000
envelopes). Full core 677 tests/100% measured lines, zero-warning Release build,
formatting and 23 repository setup tests and all 64 script tests pass. Hosted execution and required-check
readback remain pending. This record does not certify
Best Practices enrollment, production signatures or general parser completeness.
+9 -3
View File
@@ -342,7 +342,6 @@ Portable/unregistered previews cannot initiate it. Selection remains in Windows,
with no writes to UserChoice/defaults and `.txt` explicitly optional. Eleven
isolated adapter tests now pass; OS chooser behavior still needs acceptance.
The native General default-status read uses fixed `.md` and Windows
AssocQueryStringW/ASSOCSTR_PROGID with a1024-character Unicode buffer. Require
unchanged channel/install registration before displaying Default; missing/error
@@ -351,7 +350,6 @@ path, document content or authored native command is read/logged. Returning from
Windows Settings refreshes status without setting associations. Seven real
read-only/isolated adapter cases pass; actual OS selection still needs acceptance.
## Reference CSS, Settings drafts and document status
The attributed reference stylesheet is an embedded application resource. Rendered
@@ -378,7 +376,6 @@ content/paths. The disabled-editor tooltip reports native configuration state,
and its timer and footer polling stop on window closure. Native status/hover/
Explorer acceptance remains pending; passing pure or adapter tests is insufficient.
## Disconnected activation peers
An accepted native pipe connection remains owned until explicitly disconnected,
@@ -420,3 +417,12 @@ committed snapshot inside the source grant. Closed source windows cannot launch
new linked readers. Existing image, sanitizer, CSP and host-message rules remain.
Core URI and actual filesystem/junction tests pass; the prepared physical-link/
reload-scope desktop regression still requires hosted execution.
## Repeatable parser assurance
The seeded targets in [fuzzing.md](fuzzing.md) exercise Markdown/HTML/frontmatter,
literal source/folds, resource URLs, imported theme JSON and activation JSON through
actual parsers. Independent properties detect unsafe acceptance as well as crashes.
Synthetic inputs never execute scripts, start native processes or touch real profiles.
The record in [assurance.json](assurance.json) does not claim coverage-guided
instrumentation, Best Practices enrollment or verified production signatures.
@@ -0,0 +1,297 @@
/*
* Copyright (c) 2026 James Maes
* SPDX-License-Identifier: MIT
*/
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Nodes;
using AngleSharp.Dom;
using AngleSharp.Html.Dom;
using AngleSharp.Html.Parser;
using MarkdownViewr.Windows.Core.Documents;
using MarkdownViewr.Windows.Core.Themes;
namespace MarkdownViewr.Windows.Core.Tests;
/// <summary>Seeded mutation campaigns assert independent security properties without executing authored content or touching profiles.</summary>
[Trait("Category", "Fuzz")]
public sealed class SecurityFuzzTests
{
private static readonly Url s_browserBase = new("https://viewer.markdownviewr.invalid/documents/fuzz/index.html");
private static readonly string[] s_fragments =
[
"# Heading\n", "\r\n## Child\r\n", "Title\n=====\n", "---\ntitle: '==metadata=='\n---\n",
"**strong** *emphasis* ~~deleted~~ ==marked== ^sup^ ~sub~ ++inserted++\n",
"<script>window.chrome.webview.postMessage('execute')</script>",
"<img src='x.png' onerror='execute()' srcset='https://outside.invalid/x.png 2x'>",
"<iframe srcdoc='<script>execute()</script>'></iframe>", "<svg><a xlink:href='javascript:execute()'>x</a></svg>",
"<h2 id='mv-heading-999' data-mv-body='999' onclick='execute()'>Raw heading</h2>\n",
"<pre><code>==literal== ^literal^ ++literal++</code></pre>\n", "`<script>literal()</script>`\n",
"```html\n<script>literal()</script>\n```\n", "<style>@import 'https://outside.invalid';</style>",
"<form><input type='password' oninput='execute()'><button>submit</button></form>",
"<a href='java&#x09;script:execute()'>x</a>", "<a href='file:///C:/synthetic/private'>x</a>",
"[section](#heading) ![image](images/hello%20world.png)\n", "| A | B |\n|---|---|\n| x | y |\n",
"- [x] Task\n> Quote\n", "😀 café\0\t\r\n", "<!-- <code> -->", "</code></pre></div>",
"</style><script>execute()</script>", "&lt;script&gt;literal&lt;/script&gt;", "\"'<>:=[]()\\/!?%",
];
/// <summary>Malformed HTML, Markdown, frontmatter and extension combinations never recover executable DOM or authored host attributes.</summary>
[Theory]
[InlineData(22023)]
[InlineData(99173)]
public void Markdown_MutatedDocuments_RemainPassive(int seed)
=> Campaign(seed, "markdown", GenerateMarkdown, (input, random) =>
{
var extensions = new MarkdownExtensions(random.Next(2) == 1, random.Next(2) == 1, random.Next(2) == 1, random.Next(2) == 1);
string html = new MarkdownRenderer(extensions).Render(input, (FrontmatterMode)random.Next(3));
using IHtmlDocument document = new HtmlParser().ParseDocument(html);
AssertPassive(document);
Assert.All(document.QuerySelectorAll("*"), element => Assert.DoesNotContain(element.Attributes, attribute => attribute.Name.StartsWith("data-mv", StringComparison.OrdinalIgnoreCase)));
});
/// <summary>Generated headings/folds preserve the exact literal source and keep every selection within its committed snapshot.</summary>
[Theory]
[InlineData(22023)]
[InlineData(99173)]
public void Source_MutatedDocumentsAndFolds_KeepLiteralSnapshot(int seed)
=> Campaign(seed, "source", GenerateMarkdown, (input, random) =>
{
var outline = new DocumentOutline(input, (FrontmatterMode)random.Next(3));
var source = new DocumentSourceText(input, outline);
Assert.Equal(input.ReplaceLineEndings("\r"), source.Text);
Assert.Equal(input, source.GetCopyText(0, source.Text.Length));
Assert.All(outline.SourceHeadings, heading =>
{
Assert.InRange(heading.Start, 0, input.Length);
Assert.InRange(heading.End, heading.Start, input.Length);
Assert.InRange(heading.Index, 0, outline.Headings.Count - 1);
});
source.SetCollapsed(outline.Headings.Where(_ => random.Next(2) == 1).Select(heading => heading.Index));
int start = random.Next(input.Length + 1);
int length = random.Next(input.Length - start + 1);
if(source.GetDisplayRange(start, length) is { } range)
{
Assert.InRange(range.Start, 0, source.Text.Length);
Assert.InRange(range.Length, 0, source.Text.Length - range.Start);
}
source.SetCollapsed([]);
Assert.Equal(input.ReplaceLineEndings("\r"), source.Text);
Assert.Equal(input, source.GetCopyText(0, source.Text.Length));
using IHtmlDocument document = new HtmlParser().ParseDocument(outline.Html);
AssertPassive(document);
Assert.Equal(outline.Headings.Count, document.QuerySelectorAll("h1,h2,h3,h4,h5,h6").Select(element => element.Id).Distinct(StringComparer.Ordinal).Count());
});
/// <summary>Once-decoded accepted requests stay under a synthetic Windows folder; foreign namespaces and queries cannot become document commands.</summary>
[Theory]
[InlineData(22023)]
[InlineData(99173)]
public void Resources_MutatedRequests_KeepNamespaceAndPathBounds(int seed)
=> Campaign(seed, "resources", random =>
{
string[] segments = ["images", "nested", "hello%20world", "..", "%2e%2e", "%252e%252e", "%2f", "%5c", "C%3a", "file%3astream", "COM¹", "NUL", "end.", "end%20", "café", "%00", "<script>"];
string[] endings = [".md", ".txt", ".PNG", ".svg", ".exe", ".html", ".md%3aexe", ".mkd"];
return string.Join('/', Enumerable.Range(0, random.Next(1, 6)).Select(_ => segments[random.Next(segments.Length)])) + endings[random.Next(endings.Length)];
}, (input, random) =>
{
var policy = new DocumentResourcePolicy();
Uri request = new(policy.DocumentUri, input);
AssertAcceptedPath(policy.GetDocumentRelativePath(request));
AssertAcceptedPath(policy.GetImageRelativePath(request));
var outside = new UriBuilder(request) { Host = "outside.invalid" };
Assert.Null(policy.GetDocumentRelativePath(outside.Uri));
Assert.Null(policy.GetImageRelativePath(outside.Uri));
var query = new UriBuilder(request) { Query = "command=execute" };
Assert.Null(policy.GetDocumentRelativePath(query.Uri));
Assert.NotNull(policy.GetDocumentRelativePath(new Uri(policy.DocumentUri, "safe.md")));
int count = random.Next(1, 12);
int index = random.Next(-3, 16);
Uri heading = new(policy.DocumentUri.AbsoluteUri + "#mv-heading-" + index.ToString(CultureInfo.InvariantCulture));
int? result = policy.GetHeadingIndex(heading, count);
if(result is { } accepted)
{
Assert.InRange(accepted, 0, count - 1);
}
Assert.Null(policy.GetHeadingIndex(new UriBuilder(heading) { Host = "outside.invalid" }.Uri, count));
});
/// <summary>Accepted imported JSON and hostile titles/CSS cannot escape the document's title/style containers.</summary>
[Theory]
[InlineData(22023)]
[InlineData(99173)]
public void Themes_MutatedJson_KeepPassivePageBoundaries(int seed)
=> Campaign(seed, "themes", GenerateTheme, (input, random) =>
{
ReaderTheme theme;
try
{
theme = ReaderTheme.ParseCompatible(input);
}
catch(JsonException)
{
return; // Invalid imports are expected to be rejected, not silently sanitized into stored themes.
}
string title = s_fragments[random.Next(s_fragments.Length)];
string css = s_fragments[random.Next(s_fragments.Length)];
string html = DocumentPage.Build("<p>trusted-marker</p>", title, theme, new ReadingAppearance(true, 700, css), 1);
using IHtmlDocument document = new HtmlParser().ParseDocument(html);
Assert.Equal(title.ReplaceLineEndings("\n").Replace('\0', '\uFFFD'), document.QuerySelector("title")!.TextContent);
Assert.Equal("trusted-marker", document.QuerySelector("#content-inner")!.TextContent);
Assert.Single(document.QuerySelectorAll("style"));
Assert.Single(document.QuerySelectorAll("title"));
Assert.Empty(document.QuerySelectorAll("script,iframe,object,embed,form,button,img,svg"));
});
/// <summary>The minimized campaign input is a browser-relative authority, not a standalone Windows file URI; it never gains local image access.</summary>
[Fact]
public void Resources_BackslashAuthority_DeniesForeignImage()
{
const string Markdown = "<img src='\\\\x.png' onerror='execute()'>";
using IHtmlDocument document = new HtmlParser().ParseDocument(new MarkdownRenderer().Render(Markdown));
AssertPassive(document);
string source = document.QuerySelector("img")!.GetAttribute("src")!;
var policy = new DocumentResourcePolicy();
Uri target = new(new Url(new Url(policy.DocumentUri.AbsoluteUri), source).Href);
Assert.Equal("https", target.Scheme);
Assert.Null(policy.GetImageRelativePath(target));
}
/// <summary>Combines valid/malformed corpus pieces, then deletes, inserts or repeats one synthetic character.</summary>
private static string GenerateMarkdown(Random random)
{
string input = string.Concat(Enumerable.Range(0, random.Next(1, 18)).Select(_ => s_fragments[random.Next(s_fragments.Length)]));
const string MutationCharacters = "<>[]`~^=+\r\n'\"\\";
int at = random.Next(input.Length + 1);
return random.Next(3) switch
{
0 when at < input.Length => input.Remove(at, 1),
1 => input.Insert(at, s_fragments[random.Next(s_fragments.Length)]),
_ => input.Insert(at, new string(MutationCharacters[random.Next(MutationCharacters.Length)], random.Next(1, 20))),
};
}
/// <summary>Mutates actual built-in schemas and syntax without substituting a mock parser or supplying private documents.</summary>
private static string GenerateTheme(Random random)
{
JsonNode theme = JsonNode.Parse(ThemeCatalog.BuiltIn[random.Next(ThemeCatalog.BuiltIn.Count)].Json)!;
string payload = s_fragments[random.Next(s_fragments.Length)];
switch(random.Next(7))
{
case 0:
theme["name"] = payload;
break;
case 1:
theme["fonts"]!["body"] = payload;
break;
case 2:
theme["colors"]!["text"] = payload;
break;
case 3:
theme["customCSS"] = payload;
break;
case 4:
theme["schemaVersion"] = random.Next(-2, 6);
break;
case 5:
theme["sizes"]!["baseFontSize"] = random.Next(-16, 160);
break;
default:
theme["metadata"] = payload;
break;
}
string input = theme.ToJsonString();
return random.Next(5) == 0 ? input.Remove(random.Next(input.Length), 1) : input;
}
/// <summary>Checks the real platform path result independently, with no disk access, URL execution or host bridge.</summary>
private static void AssertAcceptedPath(string? path)
{
if(path is null)
{
return;
}
string root = Path.Combine(Path.GetTempPath(), "k22-synthetic-fuzz-root") + Path.DirectorySeparatorChar;
Assert.False(Path.IsPathRooted(path));
Assert.StartsWith(root, Path.GetFullPath(Path.Combine(root, path)), StringComparison.Ordinal);
Assert.DoesNotContain(':', path);
Assert.DoesNotContain('\\', path);
Assert.DoesNotContain(path, char.IsControl);
Assert.All(path.Split('/'), segment => Assert.False(segment.EndsWith('.') || segment.EndsWith(' ')));
}
/// <summary>Uses the parsed DOM's executable capabilities as the oracle, not the renderer's private allowlist.</summary>
private static void AssertPassive(IHtmlDocument document)
{
Assert.Empty(document.QuerySelectorAll("script,svg,math,iframe,object,embed,form,button,style,base,meta[http-equiv],audio,video"));
foreach(IElement element in document.QuerySelectorAll("*"))
{
Assert.DoesNotContain(element.Attributes, attribute => attribute.Name.StartsWith("on", StringComparison.OrdinalIgnoreCase));
Assert.False(element.HasAttribute("srcdoc") || element.HasAttribute("srcset") || element.HasAttribute("style"));
foreach(string name in new[] { "href", "src" })
{
string? value = element.GetAttribute(name);
if(value is not null)
{
var target = new Url(s_browserBase, value);
if(!target.IsInvalid)
{
Assert.Contains(target.Scheme, new[] { "http", "https", "mailto" }, StringComparer.Ordinal);
}
}
}
}
Assert.All(document.QuerySelectorAll("input"), element =>
{
IHtmlInputElement input = Assert.IsAssignableFrom<IHtmlInputElement>(element);
Assert.True(input.IsDisabled);
Assert.Equal("checkbox", input.Type);
});
}
/// <summary>Every failure includes a reproducible seed, exact synthetic input and property exception; campaign limits fail closed.</summary>
private static void Campaign(int seed, string target, Func<Random, string> generate, Action<string, Random> verify)
{
string? configured = Environment.GetEnvironmentVariable("K22_FUZZ_ITERATIONS");
int iterations = 500;
if(configured is not null && (!int.TryParse(configured, NumberStyles.None, CultureInfo.InvariantCulture, out iterations) || iterations is < 1 or > 100000))
{
throw new InvalidOperationException("K22_FUZZ_ITERATIONS must be between1 and100000.");
}
var random = new Random(seed);
for(int iteration = 0; iteration < iterations; iteration++)
{
string input = generate(random);
Assert.InRange(input.Length, 0, 4096);
try
{
verify(input, random);
}
catch(Exception exception)
{
throw new InvalidOperationException(FormattableString.Invariant($"Target={target}; Seed={seed}; Iteration={iteration}; Input={JsonSerializer.Serialize(input)}"), exception);
}
}
}
}
@@ -0,0 +1,88 @@
/*
* Copyright (c) 2026 James Maes
* SPDX-License-Identifier: MIT
*/
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Nodes;
namespace MarkdownViewr.Windows.UiTests;
/// <summary>Fuzzes the real generated activation schema and path validator without creating a window, pipe, process or profile.</summary>
[Trait("Category", "Fuzz")]
public sealed class ActivationFuzzTests
{
/// <summary>Only bounded absolute literal paths and the known schema can survive mutated JSON; no command/property can expand its capability.</summary>
[Theory]
[InlineData(22023)]
[InlineData(99173)]
public void Activation_MutatedJson_RejectsUnboundedOrCommandPayloads(int seed)
{
string? configured = Environment.GetEnvironmentVariable("K22_FUZZ_ITERATIONS");
int iterations = 500;
if(configured is not null && (!int.TryParse(configured, NumberStyles.None, CultureInfo.InvariantCulture, out iterations) || iterations is < 1 or > 100000))
{
throw new InvalidOperationException("K22_FUZZ_ITERATIONS must be between1 and100000.");
}
var random = new Random(seed);
for(int iteration = 0; iteration < iterations; iteration++)
{
string input = Generate(random);
try
{
ActivationRequest? request;
try
{
request = JsonSerializer.Deserialize(input, ActivationJsonContext.Default.ActivationRequest);
if(request is null)
{
continue;
}
request.Validate();
}
catch(Exception exception) when(exception is JsonException or InvalidDataException)
{
continue;
}
using var document = JsonDocument.Parse(input);
Assert.All(document.RootElement.EnumerateObject(), property => Assert.Contains(property.Name, new[] { "Paths", "HasInvalidPath" }, StringComparer.Ordinal));
Assert.InRange(request.Paths.Length, 0, 128);
Assert.All(request.Paths, path =>
{
Assert.InRange(path.Length, 1, 32767);
Assert.True(Path.IsPathFullyQualified(path));
Assert.DoesNotContain(path, char.IsControl);
});
Assert.Equal(request.Paths, JsonSerializer.Deserialize(JsonSerializer.Serialize(request, ActivationJsonContext.Default.ActivationRequest), ActivationJsonContext.Default.ActivationRequest)!.Paths);
}
catch(Exception exception)
{
throw new InvalidOperationException(FormattableString.Invariant($"Target=activation; Seed={seed}; Iteration={iteration}; Input={JsonSerializer.Serialize(input)}"), exception);
}
}
}
/// <summary>Generated envelopes include foreign command fields, relative/control-bearing paths, excessive lists and malformed syntax.</summary>
private static string Generate(Random random)
{
string[] corpus = ["C:\\k22-fuzz\\notes & café $HOME.md", "relative.md", "", "C:\\k22-fuzz\\line\nbreak.md", "\\\\synthetic.invalid\\share\\notes.md", "C:\\k22-fuzz\\literal \" quote.md"];
var paths = new JsonArray();
int count = random.Next(0, 145);
bool validPaths = random.Next(3) == 0;
for(int index = 0; index < count; index++)
{
paths.Add(validPaths ? corpus[0] : corpus[random.Next(corpus.Length)]);
}
var value = new JsonObject { ["Paths"] = paths, ["HasInvalidPath"] = random.Next(2) == 1 };
if(random.Next(4) == 0)
{
value["command"] = "execute-synthetic-content";
}
string input = value.ToJsonString();
return random.Next(6) == 0 ? input.Remove(random.Next(input.Length), 1) : input;
}
}