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:
15 files changed
+670
-17
No files matched your search
@@ -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
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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	script:execute()'>x</a>", "<a href='file:///C:/synthetic/private'>x</a>",
|
||||
"[section](#heading) \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>", "<script>literal</script>", "\"'<>:=[]()\\/!?%",
|
||||
];
|
||||
|
||||
|
||||
|
||||
/// <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;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user