diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..2738c49 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,80 @@ +name: Bug report +description: Report a reproducible runtime, protocol, UI, replay, documentation, security-adjacent, or packaging defect. +title: "[Bug]: " +labels: [bug, triage] +body: + - type: markdown + attributes: + value: | + Thank you. Remove customer, employer, station, device, credential, MAC/IP, capture, screenshot, SCL, and project-sensitive information before submitting public evidence. Security vulnerabilities belong in a private security advisory. + - type: input + id: version + attributes: + label: Process Bus Insight version + description: Use the About dialog version and build commit, or provide the exact source commit. + placeholder: 1.4.0-beta.2; commit abcdef123456 + validations: + required: true + - type: dropdown + id: area + attributes: + label: Area + options: + - SV analyzer / waveform / phasor + - PCAP replay / runtime snapshot + - GOOSE inspector + - PTP / timing context + - SCL validation + - Capture / Npcap / adapter + - About / licensing / legal notices + - Packaging / source offer / SBOM / checksum + - Documentation / website + - Other + validations: + required: true + - type: input + id: environment + attributes: + label: Environment + description: Windows version, Npcap version, adapter type, and authorized capture/replay path. + placeholder: Windows 11 x64; Npcap 1.x; Intel NIC via TAP or sanitized PCAP replay + validations: + required: true + - type: textarea + id: reproduce + attributes: + label: Reproduction steps + placeholder: | + 1. Start capture or replay... + 2. Select stream... + 3. Change publisher setting or replay the sanitized file... + validations: + required: true + - type: textarea + id: expected + attributes: + label: Expected behavior + validations: + required: true + - type: textarea + id: actual + attributes: + label: Actual behavior + validations: + required: true + - type: textarea + id: evidence + attributes: + label: Sanitized evidence + description: Minimal screenshots, logs, SCL excerpt, package manifest, or PCAP. State whether evidence is synthetic, contributor-owned, or documented as authorized and sanitized. + - type: checkboxes + id: checks + attributes: + label: Required checks + options: + - label: I removed customer, employer, station, credential, private address inventory, device, and project-sensitive information. + required: true + - label: I have the legal right to share every attached artifact. + required: true + - label: I searched existing issues first. + required: true diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index a2c845e..3558b91 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,6 +1,6 @@ ## Engineering intent -Describe the FAT/SAT, commissioning, interoperability, troubleshooting, or repository-quality problem addressed by this change. +Describe the FAT/SAT, commissioning, interoperability, troubleshooting, legal, documentation, or repository-quality problem addressed by this change. ## What changed @@ -12,9 +12,10 @@ Describe the FAT/SAT, commissioning, interoperability, troubleshooting, or repos - [ ] `dotnet build .\ProcessBusSuite.sln -c Release --no-restore` - [ ] `dotnet test .\ProcessBusSuite.sln -c Release --no-build` - [ ] `pwsh .\scripts\repository-health.ps1` +- [ ] `python .\scripts\generate-release-pdfs.py --check` - [ ] Runtime smoke test on Windows, when runtime behavior changed - [ ] Documentation/screenshots updated when user-visible behavior or claims changed -- [ ] Portable package verification completed when packaging or legal content changed +- [ ] Portable package, SOURCE.md, SBOM, checksum, and binary wording verification completed when packaging or legal content changed ## Product, claim, and evidence boundary @@ -23,17 +24,17 @@ Describe the FAT/SAT, commissioning, interoperability, troubleshooting, or repos - [ ] Expected configuration, observed traffic, software interpretation, and external-device behavior remain distinct - [ ] No claim implies formal conformance, calibration, deterministic timing, functional safety, cybersecurity approval, universal interoperability, switching authority, or IED acceptance proof - [ ] No customer, employer, station, device, credential, MAC/IP, capture, SCL, or project-sensitive data is included -- [ ] Any fixture or screenshot is synthetic, contributor-owned, or documented as authorized and sanitized +- [ ] Any fixture, screenshot, logo, icon, font, or image is synthetic, contributor-owned, or documented as authorized and sanitized - [ ] No unrelated proprietary code, tests, wording, screenshot, report, asset, or UI design was copied or mechanically translated - [ ] No `bin`, `obj`, `artifacts`, logs, captures, or local settings are committed ## Contribution licensing -- [ ] I have read and affirmatively accept `CONTRIBUTOR-LICENSE-AGREEMENT.md` +- [ ] I have read and affirmatively accept CONTRIBUTOR-LICENSE-AGREEMENT.md (CLA Version 1.0, effective 2026-07-17) - [ ] I have the legal right and any required employer or organizational authorization to submit this contribution - [ ] Every commit includes `Signed-off-by: Name ` under the DCO - [ ] Any third-party material is identified with its license and provenance ## Compatibility / migration notes -None, or describe them here. \ No newline at end of file +None, or describe them here. diff --git a/.github/workflows/candidate-package.yml b/.github/workflows/candidate-package.yml index a6bb8ef..6c70996 100644 --- a/.github/workflows/candidate-package.yml +++ b/.github/workflows/candidate-package.yml @@ -12,11 +12,17 @@ on: - "TRADEMARK.md" - "THIRD_PARTY_NOTICES.md" - "docs/LICENSING.md" + - "docs/ASSET_PROVENANCE.md" + - "docs/QUICK_START.pdf" + - "docs/USER_MANUAL.pdf" - "src/**" - "tests/**" - "scripts/publish-windows-portable.ps1" - "scripts/verify-release-package.ps1" + - "scripts/generate-release-pdfs.py" + - "scripts/generate-sbom.ps1" - "scripts/repository-health.ps1" + - "scripts/validate-public-content.py" - "docs/RELEASE_NOTES_v*.md" - ".github/workflows/candidate-package.yml" - ".github/workflows/release-package.yml" @@ -36,83 +42,102 @@ env: jobs: candidate: name: Build verified Windows x64 candidate - if: github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'stabilization/') || startsWith(github.head_ref, 'architecture/') || startsWith(github.head_ref, 'legal/') - runs-on: windows-latest - timeout-minutes: 35 + if: github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'stabilization/') || startsWith(github.head_ref, 'architecture/') || startsWith(github.head_ref, 'legal/') || startsWith(github.head_ref, 'hardening/') + runs-on: windows-2025 + timeout-minutes: 40 steps: - - name: Checkout - uses: actions/checkout@v4 + - name: Checkout tested ref + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 with: fetch-depth: 0 - name: Setup .NET - uses: actions/setup-dotnet@v5 + uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5 with: - dotnet-version: 8.0.x + dotnet-version: 8.0.4xx - - name: Resolve repository version - id: version + - name: Install document validation dependencies + run: python -m pip install --disable-pip-version-check reportlab==4.4.9 pypdf==6.4.1 + + - name: Resolve build identity + id: identity shell: pwsh run: | [xml]$props = Get-Content .\Directory.Build.props -Raw - $prefixNode = $props.SelectSingleNode('/Project/PropertyGroup/VersionPrefix') + $prefix = $props.SelectSingleNode('/Project/PropertyGroup/VersionPrefix').InnerText.Trim() $suffixNode = $props.SelectSingleNode('/Project/PropertyGroup/VersionSuffix') - if ($null -eq $prefixNode) { throw 'VersionPrefix is missing.' } - $prefix = $prefixNode.InnerText.Trim() $suffix = if ($null -eq $suffixNode) { '' } else { $suffixNode.InnerText.Trim() } $version = if ([string]::IsNullOrWhiteSpace($suffix)) { $prefix } else { "$prefix-$suffix" } - if ([string]::IsNullOrWhiteSpace($version)) { throw 'Unable to resolve repository version.' } - "value=$version" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 - Write-Host "Candidate version: $version" + $builtCommit = (git rev-parse HEAD).Trim() + $sourceHead = if ('${{ github.event_name }}' -eq 'pull_request') { '${{ github.event.pull_request.head.sha }}' } else { $builtCommit } + $testedMerge = $builtCommit + $sourceRef = if ('${{ github.event_name }}' -eq 'pull_request') { '${{ github.head_ref }}' } else { '${{ github.ref_name }}' } + "version=$version" | Out-File $env:GITHUB_OUTPUT -Append -Encoding utf8 + "built_commit=$builtCommit" | Out-File $env:GITHUB_OUTPUT -Append -Encoding utf8 + "source_head=$sourceHead" | Out-File $env:GITHUB_OUTPUT -Append -Encoding utf8 + "tested_merge=$testedMerge" | Out-File $env:GITHUB_OUTPUT -Append -Encoding utf8 + "source_ref=$sourceRef" | Out-File $env:GITHUB_OUTPUT -Append -Encoding utf8 + + - name: Verify generated PDFs and public wording + run: | + python .\scripts\generate-release-pdfs.py --check + python .\scripts\validate-public-content.py - name: Repository health gate shell: pwsh - run: .\scripts\repository-health.ps1 -ExpectedVersion '${{ steps.version.outputs.value }}' - - - name: Restore - run: dotnet restore .\ProcessBusSuite.sln + run: .\scripts\repository-health.ps1 -ExpectedVersion '${{ steps.identity.outputs.version }}' - - name: Build - run: dotnet build .\ProcessBusSuite.sln -c Release --no-restore /p:ContinuousIntegrationBuild=true - - - name: Test - run: dotnet test .\ProcessBusSuite.sln -c Release --no-build --logger "trx;LogFileName=candidate-tests.trx" --results-directory .\TestResults + - name: Restore, build, and test + shell: pwsh + run: | + dotnet restore .\ProcessBusSuite.sln + dotnet build .\ProcessBusSuite.sln -c Release --no-restore /p:ContinuousIntegrationBuild=true + dotnet test .\ProcessBusSuite.sln -c Release --no-build --logger "trx;LogFileName=candidate-tests.trx" --results-directory .\TestResults - name: Publish portable package shell: pwsh - run: .\scripts\publish-windows-portable.ps1 -Version '${{ steps.version.outputs.value }}' -Configuration Release -Runtime win-x64 + env: + SOURCE_HEAD_COMMIT: ${{ steps.identity.outputs.source_head }} + TESTED_MERGE_COMMIT: ${{ steps.identity.outputs.tested_merge }} + SOURCE_REF: ${{ steps.identity.outputs.source_ref }} + run: .\scripts\publish-windows-portable.ps1 -Version '${{ steps.identity.outputs.version }}' -Configuration Release -Runtime win-x64 - name: Verify portable package shell: pwsh run: | - $zip = ".\artifacts\release\ProcessBusInsight-v${{ steps.version.outputs.value }}-win-x64-portable.zip" - .\scripts\verify-release-package.ps1 -PackageZip $zip + $zip = ".\artifacts\release\ProcessBusInsight-v${{ steps.identity.outputs.version }}-win-x64-portable.zip" + .\scripts\verify-release-package.ps1 -PackageZip $zip -ExpectedVersion '${{ steps.identity.outputs.version }}' - name: Create candidate manifest shell: pwsh run: | $manifest = [ordered]@{ product = 'Process Bus Insight' - version = '${{ steps.version.outputs.value }}' + version = '${{ steps.identity.outputs.version }}' candidate = $true - commit = '${{ github.event.pull_request.head.sha || github.sha }}' - sourceRef = '${{ github.head_ref || github.ref_name }}' + builtCommit = '${{ steps.identity.outputs.built_commit }}' + sourceHeadCommit = '${{ steps.identity.outputs.source_head }}' + testedMergeCommit = '${{ steps.identity.outputs.tested_merge }}' + sourceRef = '${{ steps.identity.outputs.source_ref }}' runtime = 'win-x64' framework = 'net8.0-windows' communityLicense = 'GPL-3.0-or-later' historicalBoundary = '85d43a0fe58a5888a9e8008c168ab76d2333ea87' + signatureRequired = $false timestampUtc = (Get-Date).ToUniversalTime().ToString('o') } $manifest | ConvertTo-Json | Set-Content .\artifacts\release\candidate-manifest.json -Encoding utf8 - name: Upload verified candidate - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: - name: ProcessBusInsight-v${{ steps.version.outputs.value }}-win-x64-candidate-${{ github.run_number }} + name: ProcessBusInsight-v${{ steps.identity.outputs.version }}-win-x64-candidate-${{ github.run_number }} path: | - artifacts/release/ProcessBusInsight-v${{ steps.version.outputs.value }}-win-x64-portable.zip + artifacts/release/ProcessBusInsight-v${{ steps.identity.outputs.version }}-win-x64-portable.zip artifacts/release/SHA256SUMS.txt + artifacts/release/SOURCE.md + artifacts/release/ProcessBusInsight-SBOM.cdx.json artifacts/release/candidate-manifest.json TestResults/** if-no-files-found: error diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2af40f1..acc1f13 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,18 +20,29 @@ env: jobs: build-test: - name: Build, test, and repository health - runs-on: windows-latest - timeout-minutes: 25 + name: Build, test, documents, and repository health + runs-on: windows-2025 + timeout-minutes: 30 steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + with: + fetch-depth: 0 - name: Setup .NET - uses: actions/setup-dotnet@v5 + uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5 with: - dotnet-version: 8.0.x + dotnet-version: 8.0.4xx + + - name: Install deterministic document dependencies + run: python -m pip install --disable-pip-version-check reportlab==4.4.9 pypdf==6.4.1 + + - name: Verify generated release PDFs + run: python .\scripts\generate-release-pdfs.py --check + + - name: Validate public licensing and wording + run: python .\scripts\validate-public-content.py - name: Repository health gate shell: pwsh @@ -54,7 +65,7 @@ jobs: - name: Upload test evidence if: always() - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: test-evidence-${{ github.run_id }} path: TestResults/** diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 35f37c5..da2dfab 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -25,20 +25,20 @@ env: jobs: analyze: name: Analyze C# - runs-on: windows-latest + runs-on: windows-2025 timeout-minutes: 35 steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 - name: Setup .NET - uses: actions/setup-dotnet@v5 + uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5 with: - dotnet-version: 8.0.x + dotnet-version: 8.0.4xx - name: Initialize CodeQL - uses: github/codeql-action/init@v3 + uses: github/codeql-action/init@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0 with: languages: csharp build-mode: manual @@ -50,4 +50,4 @@ jobs: run: dotnet build .\ProcessBusSuite.sln -c Release --no-restore /p:ContinuousIntegrationBuild=true - name: Analyze - uses: github/codeql-action/analyze@v3 + uses: github/codeql-action/analyze@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0 diff --git a/.github/workflows/contribution-governance.yml b/.github/workflows/contribution-governance.yml new file mode 100644 index 0000000..1577448 --- /dev/null +++ b/.github/workflows/contribution-governance.yml @@ -0,0 +1,28 @@ +name: Contribution governance + +on: + pull_request_target: + branches: [main] + types: [opened, synchronize, reopened, edited] + +permissions: + contents: read + pull-requests: read + +concurrency: + group: contribution-governance-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + cla-dco: + name: CLA affirmation and DCO sign-off + runs-on: ubuntu-24.04 + steps: + - name: Checkout trusted default-branch policy + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + with: + ref: main + fetch-depth: 0 + + - name: Verify pull-request governance + run: python3 scripts/verify-contribution-governance.py diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 7870573..dde5ebc 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -18,17 +18,17 @@ env: jobs: nuget-audit: name: NuGet vulnerability audit - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 timeout-minutes: 8 steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 - name: Setup .NET - uses: actions/setup-dotnet@v5 + uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5 with: - dotnet-version: 8.0.x + dotnet-version: 8.0.4xx - name: Run high/critical vulnerability audit id: nuget-audit @@ -43,7 +43,7 @@ jobs: - name: Upload audit evidence if: always() - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: nuget-audit-${{ github.run_id }} path: nuget-audit.log @@ -59,7 +59,7 @@ jobs: github-dependency-review: name: GitHub dependency delta review - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 timeout-minutes: 6 steps: diff --git a/.github/workflows/finalize-pdf-determinism.yml b/.github/workflows/finalize-pdf-determinism.yml new file mode 100644 index 0000000..0ebb8e2 --- /dev/null +++ b/.github/workflows/finalize-pdf-determinism.yml @@ -0,0 +1,55 @@ +name: Finalize deterministic release PDFs + +on: + push: + branches: [hardening/release-integrity] + paths: + - ".github/workflows/finalize-pdf-determinism.yml" + workflow_dispatch: + +permissions: + contents: write + +jobs: + finalize: + if: github.actor == 'masarray' + runs-on: ubuntu-24.04 + steps: + - name: Checkout hardening branch + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + with: + ref: hardening/release-integrity + fetch-depth: 0 + + - name: Make ReportLab output invariant + shell: python + run: | + from pathlib import Path + path = Path('scripts/generate-release-pdfs.py') + text = path.read_text(encoding='utf-8') + if 'from reportlab.pdfgen import canvas' not in text: + text = text.replace('from reportlab.pdfbase.pdfmetrics import stringWidth\n', 'from reportlab.pdfbase.pdfmetrics import stringWidth\nfrom reportlab.pdfgen import canvas\n') + marker = '\ndef footer(canvas, doc):\n' + function = "\ndef invariant_canvas(*args, **kwargs):\n kwargs['invariant'] = 1\n return canvas.Canvas(*args, **kwargs)\n" + if 'def invariant_canvas(' not in text: + text = text.replace(marker, function + marker) + text = text.replace('doc.build(story, onFirstPage=footer, onLaterPages=footer)', 'doc.build(story, onFirstPage=footer, onLaterPages=footer, canvasmaker=invariant_canvas)') + path.write_text(text, encoding='utf-8') + + - name: Regenerate and verify PDFs + run: | + python -m pip install --disable-pip-version-check reportlab==4.4.9 pypdf==6.4.1 + python scripts/generate-release-pdfs.py + python scripts/generate-release-pdfs.py --check + python scripts/validate-public-content.py + + - name: Remove one-time workflow and commit + shell: bash + run: | + set -euo pipefail + rm -f .github/workflows/finalize-pdf-determinism.yml + git config user.name "Ari Sulistiono" + git config user.email "admin@arisulistiono.com" + git add -A + git commit -s -m "Make release PDF generation deterministic" + git push origin HEAD:hardening/release-integrity diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 09b6680..870c8ba 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -5,25 +5,49 @@ on: branches: [main] paths: - "docs/**" + - "README.md" + - "LICENSE" + - "COMMERCIAL-LICENSE.md" + - "THIRD_PARTY_NOTICES.md" + - "scripts/generate-release-pdfs.py" + - "scripts/validate-public-content.py" + - ".github/workflows/pages.yml" + pull_request: + branches: [main] + paths: + - "docs/**" + - "README.md" + - "LICENSE" + - "COMMERCIAL-LICENSE.md" + - "THIRD_PARTY_NOTICES.md" + - "scripts/generate-release-pdfs.py" + - "scripts/validate-public-content.py" - ".github/workflows/pages.yml" workflow_dispatch: permissions: contents: read - pages: write - id-token: write concurrency: - group: github-pages - cancel-in-progress: false + group: pages-${{ github.event_name }}-${{ github.ref }} + cancel-in-progress: true jobs: - build: - name: Validate and package site - runs-on: ubuntu-latest + validate: + name: Validate website, licensing, and generated documents + runs-on: ubuntu-24.04 steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + + - name: Install deterministic document dependencies + run: python3 -m pip install --disable-pip-version-check reportlab==4.4.9 pypdf==6.4.1 + + - name: Verify generated PDFs + run: python3 scripts/generate-release-pdfs.py --check + + - name: Validate public wording + run: python3 scripts/validate-public-content.py - name: Verify landing-page sources shell: bash @@ -35,25 +59,64 @@ jobs: test -f docs/site.webmanifest test -f docs/assets/branding/favicon.ico test -f docs/screenshot/analyzer-overview.webp - grep -q 'Process Bus Insight' docs/index.html + grep -q 'GPL-3.0-or-later' docs/index.html + grep -q '1.4.0-beta.2' docs/index.html + ! grep -qi 'Free and open source under Apache-2.0' docs/index.html + ! grep -qi 'source code is licensed under Apache-2.0' docs/index.html - name: Configure Pages - uses: actions/configure-pages@v6 + if: github.event_name != 'pull_request' + uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6 - name: Upload Pages artifact - uses: actions/upload-pages-artifact@v5 + if: github.event_name != 'pull_request' + uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5 with: path: ./docs deploy: - name: Deploy - runs-on: ubuntu-latest - needs: build - if: github.ref == 'refs/heads/main' + name: Deploy product website + if: github.event_name != 'pull_request' + needs: validate + runs-on: ubuntu-24.04 + permissions: + pages: write + id-token: write environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} + outputs: + page_url: ${{ steps.deployment.outputs.page_url }} steps: - name: Deploy Pages id: deployment - uses: actions/deploy-pages@v5 + uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5 + + smoke: + name: Verify deployed GPL website + if: github.event_name != 'pull_request' + needs: deploy + runs-on: ubuntu-24.04 + steps: + - name: Wait for deployment propagation and verify public content + env: + PAGE_URL: ${{ needs.deploy.outputs.page_url }} + shell: bash + run: | + set -euo pipefail + for attempt in $(seq 1 18); do + if curl --fail --silent --show-error --location --max-time 20 "$PAGE_URL" -o deployed.html; then + if grep -q 'GPL-3.0-or-later' deployed.html \ + && grep -q '1.4.0-beta.2' deployed.html \ + && ! grep -qi 'Free and open source under Apache-2.0' deployed.html \ + && ! grep -qi 'source code is licensed under Apache-2.0' deployed.html; then + echo "Deployed website validation passed: $PAGE_URL" + exit 0 + fi + fi + echo "Deployment not current yet (attempt $attempt/18); retrying..." + sleep 10 + done + echo "Deployed website did not reach the expected GPL/version state: $PAGE_URL" >&2 + cat deployed.html 2>/dev/null || true + exit 1 diff --git a/.github/workflows/release-package.yml b/.github/workflows/release-package.yml index fbf40fe..379fc7b 100644 --- a/.github/workflows/release-package.yml +++ b/.github/workflows/release-package.yml @@ -8,7 +8,7 @@ on: required: true default: 1.4.0-beta.2 publish_release: - description: Create or update a GitHub Release + description: Create a new GitHub Release required: true default: false type: boolean @@ -32,6 +32,8 @@ on: permissions: contents: write + id-token: write + attestations: write concurrency: group: release-${{ github.ref }} @@ -43,23 +45,26 @@ env: jobs: package: - name: Validate and package Windows x64 - runs-on: windows-latest - timeout-minutes: 35 + name: Validate, sign, attest, and package Windows x64 + runs-on: windows-2025 + timeout-minutes: 45 steps: - - name: Checkout - uses: actions/checkout@v4 + - name: Checkout exact release ref + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 with: fetch-depth: 0 - name: Setup .NET - uses: actions/setup-dotnet@v5 + uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5 with: - dotnet-version: 8.0.x + dotnet-version: 8.0.4xx - - name: Resolve release version - id: version + - name: Install document validation dependencies + run: python -m pip install --disable-pip-version-check reportlab==4.4.9 pypdf==6.4.1 + + - name: Resolve and validate release identity + id: identity shell: pwsh run: | $version = '${{ github.event.inputs.version }}' @@ -67,86 +72,128 @@ jobs: $version = $version.Trim() if ($version.StartsWith('v')) { $version = $version.Substring(1) } if ([string]::IsNullOrWhiteSpace($version)) { throw 'Unable to resolve release version.' } - "value=$version" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 - Write-Host "Release version: $version" + $builtCommit = (git rev-parse HEAD).Trim() + $mainCommit = (git rev-parse origin/main).Trim() + git merge-base --is-ancestor $builtCommit $mainCommit + if ($LASTEXITCODE -ne 0) { throw "Release commit $builtCommit is not on main." } + if ('${{ github.event_name }}' -eq 'push' -and '${{ github.ref_name }}' -ne "v$version") { + throw "Tag/version mismatch: tag=${{ github.ref_name }}, version=$version" + } + "version=$version" | Out-File $env:GITHUB_OUTPUT -Append -Encoding utf8 + "built_commit=$builtCommit" | Out-File $env:GITHUB_OUTPUT -Append -Encoding utf8 - - name: Repository health gate + - name: Require signing secret for a public release + if: ${{ github.event_name == 'push' || github.event.inputs.publish_release == 'true' }} shell: pwsh - run: .\scripts\repository-health.ps1 -ExpectedVersion '${{ steps.version.outputs.value }}' + env: + SIGNING_CERT: ${{ secrets.WINDOWS_SIGNING_CERTIFICATE_BASE64 }} + SIGNING_PASSWORD: ${{ secrets.WINDOWS_SIGNING_CERTIFICATE_PASSWORD }} + run: | + if ([string]::IsNullOrWhiteSpace($env:SIGNING_CERT) -or [string]::IsNullOrWhiteSpace($env:SIGNING_PASSWORD)) { + throw 'Public release publishing is blocked until WINDOWS_SIGNING_CERTIFICATE_BASE64 and WINDOWS_SIGNING_CERTIFICATE_PASSWORD are configured.' + } - - name: Restore - run: dotnet restore .\ProcessBusSuite.sln + - name: Verify generated PDFs and public wording + run: | + python .\scripts\generate-release-pdfs.py --check + python .\scripts\validate-public-content.py - - name: Build - run: dotnet build .\ProcessBusSuite.sln -c Release --no-restore /p:ContinuousIntegrationBuild=true + - name: Repository health gate + shell: pwsh + run: .\scripts\repository-health.ps1 -ExpectedVersion '${{ steps.identity.outputs.version }}' - - name: Test - run: dotnet test .\ProcessBusSuite.sln -c Release --no-build --logger "trx;LogFileName=release-tests.trx" --results-directory .\TestResults + - name: Restore, build, and test + shell: pwsh + run: | + dotnet restore .\ProcessBusSuite.sln + dotnet build .\ProcessBusSuite.sln -c Release --no-restore /p:ContinuousIntegrationBuild=true + dotnet test .\ProcessBusSuite.sln -c Release --no-build --logger "trx;LogFileName=release-tests.trx" --results-directory .\TestResults - name: Publish portable package shell: pwsh - run: .\scripts\publish-windows-portable.ps1 -Version '${{ steps.version.outputs.value }}' -Configuration Release -Runtime win-x64 - - - name: Verify portable package + env: + SOURCE_HEAD_COMMIT: ${{ steps.identity.outputs.built_commit }} + TESTED_MERGE_COMMIT: ${{ steps.identity.outputs.built_commit }} + SOURCE_REF: ${{ github.ref_name }} + WINDOWS_SIGNING_CERTIFICATE_BASE64: ${{ secrets.WINDOWS_SIGNING_CERTIFICATE_BASE64 }} + WINDOWS_SIGNING_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_SIGNING_CERTIFICATE_PASSWORD }} + run: .\scripts\publish-windows-portable.ps1 -Version '${{ steps.identity.outputs.version }}' -Configuration Release -Runtime win-x64 + + - name: Verify package and signature boundary shell: pwsh run: | - $zip = ".\artifacts\release\ProcessBusInsight-v${{ steps.version.outputs.value }}-win-x64-portable.zip" - .\scripts\verify-release-package.ps1 -PackageZip $zip + $zip = ".\artifacts\release\ProcessBusInsight-v${{ steps.identity.outputs.version }}-win-x64-portable.zip" + $requireSignature = '${{ github.event_name }}' -eq 'push' -or '${{ github.event.inputs.publish_release }}' -eq 'true' + if ($requireSignature) { + .\scripts\verify-release-package.ps1 -PackageZip $zip -ExpectedVersion '${{ steps.identity.outputs.version }}' -RequireAuthenticode + } else { + .\scripts\verify-release-package.ps1 -PackageZip $zip -ExpectedVersion '${{ steps.identity.outputs.version }}' + } - name: Create release manifest shell: pwsh run: | + $zip = "artifacts/release/ProcessBusInsight-v${{ steps.identity.outputs.version }}-win-x64-portable.zip" + $sbom = 'artifacts/release/ProcessBusInsight-SBOM.cdx.json' + $source = 'artifacts/release/SOURCE.md' $manifest = [ordered]@{ product = 'Process Bus Insight' - version = '${{ steps.version.outputs.value }}' - commit = '${{ github.sha }}' + version = '${{ steps.identity.outputs.version }}' + builtCommit = '${{ steps.identity.outputs.built_commit }}' + sourceHeadCommit = '${{ steps.identity.outputs.built_commit }}' + testedMergeCommit = '${{ steps.identity.outputs.built_commit }}' + sourceRef = '${{ github.ref_name }}' runtime = 'win-x64' framework = 'net8.0-windows' communityLicense = 'GPL-3.0-or-later' historicalBoundary = '85d43a0fe58a5888a9e8008c168ab76d2333ea87' + sourceArchive = "https://github.com/masarray/DigSubAnalyzer/archive/${{ steps.identity.outputs.built_commit }}.zip" + packageSha256 = (Get-FileHash $zip -Algorithm SHA256).Hash.ToLowerInvariant() + sbomSha256 = (Get-FileHash $sbom -Algorithm SHA256).Hash.ToLowerInvariant() + sourceOfferSha256 = (Get-FileHash $source -Algorithm SHA256).Hash.ToLowerInvariant() timestampUtc = (Get-Date).ToUniversalTime().ToString('o') } $manifest | ConvertTo-Json | Set-Content .\artifacts\release\release-manifest.json -Encoding utf8 + - name: Attest release package provenance + id: attest-package + uses: actions/attest@a1948c3f048ba23858d222213b7c278aabede763 # v4 + with: + subject-path: artifacts/release/ProcessBusInsight-v${{ steps.identity.outputs.version }}-win-x64-portable.zip + + - name: Attest SBOM provenance + uses: actions/attest@a1948c3f048ba23858d222213b7c278aabede763 # v4 + with: + subject-path: artifacts/release/ProcessBusInsight-SBOM.cdx.json + - name: Upload workflow artifact - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: - name: ProcessBusInsight-v${{ steps.version.outputs.value }}-win-x64-portable + name: ProcessBusInsight-v${{ steps.identity.outputs.version }}-win-x64-portable path: | - artifacts/release/ProcessBusInsight-v${{ steps.version.outputs.value }}-win-x64-portable.zip + artifacts/release/ProcessBusInsight-v${{ steps.identity.outputs.version }}-win-x64-portable.zip artifacts/release/SHA256SUMS.txt + artifacts/release/SOURCE.md + artifacts/release/ProcessBusInsight-SBOM.cdx.json artifacts/release/release-manifest.json TestResults/** if-no-files-found: error retention-days: 30 - - name: Publish GitHub Release + - name: Publish new GitHub Release if: ${{ github.event_name == 'push' || github.event.inputs.publish_release == 'true' }} - shell: pwsh - env: - GH_TOKEN: ${{ github.token }} - run: | - $version = '${{ steps.version.outputs.value }}' - $tag = "v$version" - $zip = "artifacts/release/ProcessBusInsight-v$version-win-x64-portable.zip" - $sha = "artifacts/release/SHA256SUMS.txt" - $manifest = "artifacts/release/release-manifest.json" - $notesFile = '${{ github.event.inputs.release_notes_file }}' - if ([string]::IsNullOrWhiteSpace($notesFile)) { $notesFile = 'docs/RELEASE_NOTES_v1.4.0-beta.2.md' } - if (-not (Test-Path $notesFile)) { throw "Release notes file not found: $notesFile" } - - $releaseArgs = @('release', 'create', $tag, $zip, $sha, $manifest, '--title', "Process Bus Insight $tag", '--notes-file', $notesFile, '--target', '${{ github.sha }}') - $isDraft = '${{ github.event.inputs.draft }}' - $isPrerelease = '${{ github.event.inputs.prerelease }}' - if ('${{ github.event_name }}' -eq 'push') { - $isPrerelease = if ($version -match '-') { 'true' } else { 'false' } - } - if ($isDraft -eq 'true') { $releaseArgs += '--draft' } - if ($isPrerelease -eq 'true') { $releaseArgs += '--prerelease' } - - gh release view $tag *> $null - if ($LASTEXITCODE -eq 0) { - gh release upload $tag $zip $sha $manifest --clobber - } else { - gh @releaseArgs - } + uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3.0.2 + with: + tag_name: v${{ steps.identity.outputs.version }} + target_commitish: ${{ steps.identity.outputs.built_commit }} + name: Process Bus Insight v${{ steps.identity.outputs.version }} + body_path: ${{ github.event.inputs.release_notes_file || 'docs/RELEASE_NOTES_v1.4.0-beta.2.md' }} + draft: ${{ github.event.inputs.draft || false }} + prerelease: ${{ github.event_name == 'push' && contains(steps.identity.outputs.version, '-') || github.event.inputs.prerelease }} + fail_on_unmatched_files: true + files: | + artifacts/release/ProcessBusInsight-v${{ steps.identity.outputs.version }}-win-x64-portable.zip + artifacts/release/SHA256SUMS.txt + artifacts/release/SOURCE.md + artifacts/release/ProcessBusInsight-SBOM.cdx.json + artifacts/release/release-manifest.json diff --git a/.github/workflows/squash-release-integrity-branch.yml b/.github/workflows/squash-release-integrity-branch.yml new file mode 100644 index 0000000..2b7e41c --- /dev/null +++ b/.github/workflows/squash-release-integrity-branch.yml @@ -0,0 +1,42 @@ +name: Squash release integrity branch + +on: + push: + branches: [hardening/release-integrity] + paths: + - ".github/workflows/squash-release-integrity-branch.yml" + workflow_dispatch: + +permissions: + contents: write + +jobs: + squash: + if: github.actor == 'masarray' + runs-on: ubuntu-24.04 + steps: + - name: Checkout hardening branch + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + with: + ref: hardening/release-integrity + fetch-depth: 0 + + - name: Validate repository before squashing + run: | + python -m pip install --disable-pip-version-check reportlab==4.4.9 pypdf==6.4.1 + python scripts/generate-release-pdfs.py --check + python scripts/validate-public-content.py + pwsh -NoLogo -NoProfile -File scripts/repository-health.ps1 + + - name: Replace branch history with one signed-off commit + shell: bash + run: | + set -euo pipefail + git fetch origin main + rm -f .github/workflows/squash-release-integrity-branch.yml + git reset --soft origin/main + git add -A + git config user.name "Ari Sulistiono" + git config user.email "admin@arisulistiono.com" + git commit -s -m "Harden release integrity, licensing, and provenance" + git push --force-with-lease origin HEAD:hardening/release-integrity diff --git a/COMMERCIAL-LICENSE.md b/COMMERCIAL-LICENSE.md index a730130..0f04244 100644 --- a/COMMERCIAL-LICENSE.md +++ b/COMMERCIAL-LICENSE.md @@ -1,26 +1,28 @@ # Commercial Licensing -Process Bus Insight / DigSubAnalyzer is available as a community edition under `GPL-3.0-or-later`. +**Licensor:** Ari Sulistiono, the project owner for rights he controls in Process Bus Insight / DigSubAnalyzer. -Organizations that cannot use or redistribute the software under the GNU GPL may contact the project owner to discuss a separate commercial agreement for rights controlled by that owner, including: +Process Bus Insight / DigSubAnalyzer is available as a community edition under `GPL-3.0-or-later` for post-transition revisions and release packages. + +Organizations that cannot use or redistribute the software under the GNU GPL may contact the Licensor to discuss a separate commercial agreement for rights controlled by the Licensor, including: - proprietary or closed-source integration; - OEM or white-label distribution; -- redistribution without the GPL source-distribution model; -- private product branches and organization-specific features; +- redistribution outside the GPL source-distribution model; +- private product branches and organization-specific features; and - contractual maintenance, priority support, training, warranty, or engineering services. ## Important legal boundary This document is an invitation to negotiate. It is **not itself a commercial license**, does not create a contract, and **grants no additional rights** beyond the licenses that already apply. -Commercial terms become effective only through a separately negotiated and executed agreement. Such an agreement can cover only copyrights and other rights controlled by the granting party. It does not replace or remove: +Commercial terms become effective only through a separately negotiated and executed agreement signed by the relevant parties. Such an agreement can cover only copyrights, trademark permissions, and other rights controlled by the granting party. It does not replace or remove: - third-party license obligations; - contributor-owned rights not covered by an applicable agreement; - historical open-source grants; - standards-access restrictions; -- trademark rules; or -- operational authorization, validation, safety, or regulatory requirements. +- operational authorization, validation, safety, cybersecurity, or regulatory requirements; or +- employment, invention-assignment, confidentiality, customer, educational, sponsorship, or other private obligations. -For a confidential commercial discussion, contact the project owner through the `masarray` GitHub profile. \ No newline at end of file +For a confidential commercial discussion, contact Ari Sulistiono through the `masarray` GitHub profile or the contact address displayed by the application. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8e69987..5f7f65c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,14 +4,14 @@ Thank you for helping improve a receive-only IEC 61850 Process Bus engineering t ## Licensing and acceptance -The current community edition is `GPL-3.0-or-later` and the project maintains a separate commercial licensing path. +Post-transition community revisions are `GPL-3.0-or-later`, and the project maintains a separate commercial licensing path for rights controlled by Ari Sulistiono. Before merge, contributors must: -1. read and affirmatively accept `CONTRIBUTOR-LICENSE-AGREEMENT.md`; +1. read and affirmatively accept **CLA Version 1.0, effective 2026-07-17**, in `CONTRIBUTOR-LICENSE-AGREEMENT.md`; 2. add a Developer Certificate of Origin sign-off to every commit; -3. have the legal right and any required employer or organizational authorization to submit the contribution; and -4. identify any third-party material together with its provenance and license. +3. have the legal right and any required employer, customer, educational, sponsor, or organizational authorization to submit the contribution; and +4. identify third-party material, assets, and fixtures together with provenance and license evidence. Sign commits with: @@ -19,14 +19,14 @@ Sign commits with: Signed-off-by: Full Name ``` -The CLA grants rights needed for GPL-compatible publication and a separate commercial path. It does not transfer ownership of the contributor's work. +The CLA grants rights needed for GPL-compatible publication and a separate commercial path. It does not transfer ownership of the contributor's work. CI records the checked, versioned CLA statement and verifies DCO sign-offs. ## Start here 1. Read `AGENTS.md` for permanent engineering invariants. 2. Search existing issues and pull requests. 3. Use a focused branch and keep changes reviewable. -4. Remove customer, site, device, capture, SCL, MAC/IP, and project-sensitive information from all evidence. +4. Remove customer, employer, site, device, capture, SCL, MAC/IP, credential, and project-sensitive information from all evidence. ## Development setup @@ -36,22 +36,24 @@ cd DigSubAnalyzer dotnet restore .\ProcessBusSuite.sln dotnet build .\ProcessBusSuite.sln -c Release dotnet test .\ProcessBusSuite.sln -c Release +python .\scripts\generate-release-pdfs.py --check +python .\scripts\validate-public-content.py .\scripts\repository-health.ps1 ``` ## Good contribution areas -- SV, GOOSE, PTP, Ethernet, BER, and SCL parser hardening +- SV, GOOSE, PTP, Ethernet, BER, SCL, and supported-PCAP parser hardening - Per-stream runtime isolation and coherent snapshot behavior - Golden-frame, replay, rollover, malformed-input, and multi-stream tests - UI clarity that reduces visual noise without hiding evidence -- Release automation, documentation, and field-validation scenarios +- Release signing, source/SBOM provenance, documentation, and field-validation scenarios ## Independent-development and data boundary External software may be used only as a lawfully licensed black-box interoperability or packet-observation endpoint. Do not copy or mechanically translate unrelated source, API composition, tests, documentation wording, screenshots, icons, reports, UI layouts, binaries, or extracted resources unless the project has documented incorporation and relicensing rights. -Use synthetic or contributor-owned fixtures whenever practical. Real SCL, PCAP, screenshots, logs, and diagnostics require documented sharing rights and sanitization. +Use synthetic or contributor-owned fixtures whenever practical. Real SCL, PCAP, screenshots, logs, and diagnostics require documented sharing rights and sanitization. New visual assets must update `docs/ASSET_PROVENANCE.md`. Do not submit: @@ -63,6 +65,6 @@ Do not submit: ## Pull-request expectations -A pull request should explain the engineering problem, source of truth, validation method, compatibility impact, operational and data-handling impact, public wording changes, and remaining uncertainty. Runtime changes should include automated regression coverage where practical and a Windows smoke test. +A pull request should explain the engineering problem, source of truth, validation method, compatibility impact, operational and data-handling impact, public wording changes, asset provenance, and remaining uncertainty. Runtime changes should include automated regression coverage where practical and a Windows smoke test. -The pull-request template is the minimum checklist, not a substitute for engineering evidence. \ No newline at end of file +The pull-request template is the minimum checklist, not a substitute for engineering evidence. diff --git a/CONTRIBUTOR-LICENSE-AGREEMENT.md b/CONTRIBUTOR-LICENSE-AGREEMENT.md index eb4f6c1..842e534 100644 --- a/CONTRIBUTOR-LICENSE-AGREEMENT.md +++ b/CONTRIBUTOR-LICENSE-AGREEMENT.md @@ -1,6 +1,10 @@ # Contributor License Agreement -By submitting a contribution to Process Bus Insight / DigSubAnalyzer, you agree to the terms below. “Contribution” means code, documentation, tests, designs, data, or other material intentionally submitted for inclusion in the project. +**CLA Version:** 1.0 +**Effective date:** 2026-07-17 +**Project owner / Licensor:** Ari Sulistiono + +By submitting a contribution to Process Bus Insight / DigSubAnalyzer and affirmatively accepting this CLA version in the pull-request record, you agree to the terms below. “Contribution” means code, documentation, tests, designs, data, or other material intentionally submitted for inclusion in the project. ## 1. Ownership @@ -8,13 +12,13 @@ You retain ownership of your Contribution. ## 2. Copyright license -You grant the project owner and recipients of the project a perpetual, worldwide, non-exclusive, royalty-free, irrevocable copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, sublicense, and distribute your Contribution as part of the project. +You grant Ari Sulistiono, the project, and recipients of the project a perpetual, worldwide, non-exclusive, royalty-free, irrevocable copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, sublicense, and distribute your Contribution as part of the project. This grant includes permission to distribute the Contribution: - under `GPL-3.0-or-later` and compatible later community licensing; -- under a separately negotiated commercial, OEM, white-label, proprietary-integration, support, or services agreement; and -- as part of historical, archival, source, binary, documentation, test, and release materials. +- under a separately negotiated commercial, OEM, white-label, proprietary-integration, support, warranty, training, or services agreement; and +- as part of historical, archival, source, binary, documentation, test, website, and release materials. ## 3. Patent license @@ -28,12 +32,18 @@ You represent that: - the Contribution is your original work or you have clearly identified and documented all third-party material; - you have obtained any required employer, client, educational institution, sponsor, or organizational permission; - the Contribution does not knowingly include confidential, restricted, customer, station, credential, or unlawfully obtained material; and -- any SCL, PCAP, screenshot, report, test fixture, or diagnostic evidence is synthetic, contributor-owned, or documented as authorized and sanitized. +- any SCL, PCAP, screenshot, report, test fixture, diagnostic evidence, logo, icon, font, image, or documentation source is synthetic, contributor-owned, or documented as authorized and sanitized. -## 5. No obligation +## 5. No obligation or warranty The project is not required to accept, use, maintain, support, or distribute any Contribution. Unless separately agreed in writing, Contributions are provided without warranty. -## Acceptance +## 6. Acceptance record + +A pull request must contain the checked statement: + +```text +I have read and affirmatively accept CONTRIBUTOR-LICENSE-AGREEMENT.md (CLA Version 1.0, effective 2026-07-17) +``` -A pull request must affirm acceptance of this agreement. Every commit must also include a Developer Certificate of Origin sign-off as described in `DCO.txt`. \ No newline at end of file +The pull-request number, GitHub account, timestamp, checked statement, and this versioned document form the public acceptance record. Every commit must also include a Developer Certificate of Origin sign-off as described in `DCO.txt`. diff --git a/NOTICE b/NOTICE index 9206742..190f1ed 100644 --- a/NOTICE +++ b/NOTICE @@ -1,7 +1,7 @@ Process Bus Insight / DigSubAnalyzer Copyright 2026 Ari Sulistiono and Process Bus Insight Contributors -Current community revisions and current public release packages are licensed under GPL-3.0-or-later. +Revisions after the historical boundary and release packages built from those revisions are licensed under GPL-3.0-or-later. The final active Apache-2.0 revision is commit: 85d43a0fe58a5888a9e8008c168ab76d2333ea87 @@ -9,8 +9,8 @@ The final active Apache-2.0 revision is commit: That historical revision is preserved on: archive/apache-2.0-final -Historical Apache-2.0 grants remain effective for the revisions to which they applied. They are not a current alternative license for later revisions or current binaries. +Historical Apache-2.0 grants remain effective for the revisions and artifacts to which they applied. They are not a current alternative license for later revisions or post-transition binaries. A separate commercial licensing path is described in COMMERCIAL-LICENSE.md. That notice is not itself a license and grants no additional rights. -Npcap is a separately installed runtime prerequisite for raw packet capture on Windows and is not vendored or silently bundled. Third-party components retain their applicable licenses and notices. \ No newline at end of file +Npcap is a separately installed runtime prerequisite for raw packet capture on Windows and is not vendored or silently bundled. Third-party components retain their applicable licenses and notices. diff --git a/SECURITY.md b/SECURITY.md index 8c519a5..aaccf27 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -2,7 +2,7 @@ ## Supported versions -The latest public-beta line and the default `main` branch receive security fixes. Older beta packages may be asked to upgrade before investigation. +The latest post-transition public-beta line and the default `main` branch receive security fixes. Historical beta packages may be asked to upgrade before investigation. ## Reporting a vulnerability @@ -10,25 +10,31 @@ Use a private GitHub security advisory: `https://github.com/masarray/DigSubAnalyzer/security/advisories/new` -Do not publish exploit details, malicious captures, customer SCL files, or sensitive screenshots in a public issue. +Do not publish exploit details, malicious captures, customer SCL files, credentials, or sensitive screenshots in a public issue. -Include the affected version/commit, Windows and Npcap environment, minimal reproduction steps, impact, and sanitized evidence. An initial acknowledgement is targeted within seven days; remediation timing depends on severity and reproducibility. +Include the affected version and build commit from About, Windows and Npcap environment, minimal reproduction steps, impact, and sanitized evidence. An initial acknowledgement is targeted within seven days; remediation timing depends on severity, reproducibility, and maintainer capacity. ## In scope -- Unsafe parsing of crafted Ethernet, SV, GOOSE, PTP, BER, MMS, or SCL input -- Crashes, hangs, excessive allocation, or resource exhaustion from malformed traffic +- Unsafe parsing of crafted Ethernet, Sampled Values, GOOSE, PTP, BER, SCL, or supported PCAP input +- Crashes, hangs, excessive allocation, or resource exhaustion from malformed or adversarial traffic - Cross-stream state contamination that can misrepresent engineering evidence -- Accidental capture, log, configuration, or project-data disclosure -- Release-package integrity and workflow-permission issues +- Accidental capture, log, configuration, screenshot, source-offer, or project-data disclosure +- Release-package integrity, source/SBOM mismatch, signature, checksum, workflow-permission, or provenance issues +- Public wording that materially misstates the active license or package build identity ## Out of scope - Certification of timing measurement accuracy - Unsupported or compromised Npcap/driver installations - Vulnerabilities in third-party software not distributed by this repository -- Social engineering, denial-of-service against public infrastructure, or tests involving customer networks without authorization +- Social engineering, denial-of-service against public infrastructure, or tests involving customer or employer networks without authorization +- IEC 61850 control or MMS-client behavior, which is outside this receive-only product build ## Data handling -Process Bus Insight is intended for local engineering use. Sanitize captures, SCL files, screenshots, MAC/IP addresses, device names, and project references before sharing them. +Process Bus Insight is intended for local engineering use. Sanitize captures, SCL files, screenshots, MAC/IP addresses, device names, credentials, and project references before sharing them. Prefer synthetic fixtures and use an authorized TAP, mirror port, or isolated engineering network. + +## Release trust + +A public post-transition release should provide a checksum, exact `SOURCE.md`, CycloneDX SBOM, release manifest, build attestation, and a valid Authenticode signature. Do not treat an unsigned local or pull-request candidate as an official signed release. diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index d98e31b..0444aa3 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -1,19 +1,32 @@ # Third-Party Notices -Process Bus Insight / DigSubAnalyzer project material covered by the current community license is distributed under `GPL-3.0-or-later`. Third-party components retain their own licenses and are not relicensed merely because they are used, restored, bundled, or documented by this project. +Process Bus Insight / DigSubAnalyzer project material covered by the post-transition community license is distributed under `GPL-3.0-or-later`. Third-party components retain their own licenses and are not relicensed merely because they are used, restored, bundled, or documented by this project. ## Microsoft .NET and WPF -Process Bus Insight is built with Microsoft .NET and WPF for Windows desktop use. Self-contained packages created with `dotnet publish` may contain applicable .NET runtime and WPF files generated by the .NET SDK. Their Microsoft license terms and notices remain applicable. +Process Bus Insight is built with Microsoft .NET and Windows Presentation Foundation. Self-contained packages created with `dotnet publish` include applicable .NET runtime and WPF files generated by the .NET SDK. Their Microsoft license terms, third-party notices, and component licenses remain applicable. The release CycloneDX SBOM records the runtime versions resolved by the build environment. ## Npcap -Npcap is required on the target Windows machine for live raw Ethernet capture. It is not vendored in this repository and is not silently bundled by the portable package. Users install it separately and are responsible for the applicable license and deployment terms. +Npcap is required on the target Windows machine for live raw Ethernet capture. It is not vendored in this repository and is not silently bundled by the portable package. Users install it separately and are responsible for the applicable Npcap license, OEM/redist terms, deployment policy, and driver security posture. -## Managed dependencies +## Website fonts and symbols -The exact direct and transitive NuGet dependency graph is defined by the project files and resolved during restore. Dependencies, test tools, GitHub Actions, build services, fonts, and other external resources retain their applicable licenses and notices. +The public landing page loads: + +- **Inter**, distributed under the SIL Open Font License 1.1; and +- **Material Symbols**, distributed under Apache-2.0. + +They are loaded from Google Fonts and retain their own licenses. Offline application packages do not silently bundle the web-font files. + +## Development and test dependencies + +The test project uses packages such as Microsoft.NET.Test.Sdk, xUnit, the Visual Studio xUnit runner, and coverlet. These are development/test dependencies and are not intended to be shipped as application runtime components. Exact resolved versions are controlled by project metadata and CI evidence. + +## Managed dependency and SBOM boundary + +The exact source dependency graph is defined by project files and resolved during restore. Each post-transition release produces `ProcessBusInsight-SBOM.cdx.json` and includes `sbom.cdx.json` inside the portable ZIP. The SBOM is a dependency and runtime inventory; it does not replace the underlying license texts or notices. ## Commercial licensing boundary -A separate Process Bus Insight commercial agreement can cover only rights controlled by the relevant copyright holder. It does not replace, remove, or expand third-party license rights or obligations. \ No newline at end of file +A separate Process Bus Insight commercial agreement can cover only rights controlled by the relevant copyright holder. It does not replace, remove, or expand third-party license rights or obligations. diff --git a/docs/ASSET_PROVENANCE.md b/docs/ASSET_PROVENANCE.md new file mode 100644 index 0000000..f52465d --- /dev/null +++ b/docs/ASSET_PROVENANCE.md @@ -0,0 +1,27 @@ +# Asset Provenance Register + +This register documents public and packaged non-code assets in Process Bus Insight / DigSubAnalyzer. It is a repository record, not a substitute for underlying ownership or license evidence. + +| Asset or group | Repository use | Recorded status | Redistribution boundary | +| --- | --- | --- | --- | +| `src/ProcessBus.App.Wpf/IEC61850.ico` and project icon PNG files | Application identity and window icons | Project branding asset maintained by Ari Sulistiono | Software copyright license does not grant unrestricted trademark or branding rights; see `TRADEMARK.md`. | +| `src/ProcessBus.App.Wpf/Views/Assets/Profile/ari-profile.jpg` | Maintainer profile in About | Personal profile image supplied for this project by Ari Sulistiono | Not granted for unrelated identity, endorsement, biometric, or third-party branding use. | +| `docs/screenshot/*.webp` | Public product screenshots and social preview | Screenshots generated from the project application; public versions must contain no customer, station, credential, or restricted project data | May accompany truthful project documentation; modified products must avoid branding confusion. | +| `docs/assets/branding/*` | Website favicon and product branding | Project branding assets | Governed by `TRADEMARK.md`. | +| Inter web font | Website typography loaded from Google Fonts | Third-party font, SIL Open Font License 1.1 | Retains its own license; not relicensed by this project. | +| Material Symbols | Website icons loaded from Google Fonts | Third-party icon font, Apache-2.0 | Retains its own license and attribution requirements. | +| Generated Quick Start and User Manual PDFs | Offline release documentation | Deterministically generated by `scripts/generate-release-pdfs.py` from project-controlled text and styles | Distributed with the post-transition package under GPL-3.0-or-later, subject to embedded third-party font licensing. | +| SCL, PCAP, logs, reports, and diagnostic fixtures | Tests and engineering evidence | Must be synthetic, contributor-owned, or documented as authorized and sanitized | Do not commit or redistribute customer, employer, station, credential, or restricted network material without documented authority. | + +## Required evidence for new assets + +A pull request adding or replacing an asset must state: + +- creator or source; +- date obtained or created; +- applicable license or written permission; +- whether it contains personal, customer, station, device, or project information; +- sanitization steps; and +- whether commercial/OEM redistribution rights are available. + +Unclear-provenance assets must not be included in a commercial deliverable until their rights are resolved. diff --git a/docs/LICENSING.md b/docs/LICENSING.md index d099917..0851c88 100644 --- a/docs/LICENSING.md +++ b/docs/LICENSING.md @@ -2,13 +2,13 @@ ## Current community edition -Current revisions on `main` and current public release packages are licensed **only** under: +Revisions after the historical boundary on `main`, and release packages built from those revisions, are licensed **only** under: ```text GPL-3.0-or-later ``` -The GNU General Public License gives users permission to run, study, modify, and redistribute covered software subject to its terms, including source-availability obligations when conveying object code. +The GNU General Public License gives users permission to run, study, modify, and redistribute covered software subject to its terms, including corresponding-source obligations when conveying object code. ## Historical Apache-2.0 boundary @@ -24,28 +24,30 @@ It is preserved on: archive/apache-2.0-final ``` -Revisions at or before that boundary retain the Apache-2.0 rights already granted. Those historical rights are not revoked. Later revisions and current binaries are not offered as an Apache-or-GPL choice. +Revisions at or before that boundary, and historical binaries built from those revisions, retain the rights already granted under Apache-2.0. Those historical rights are not revoked. Later revisions and post-transition binaries are not offered as an Apache-or-GPL choice. + +The project should also maintain a protected annotated tag named `license-boundary/apache-2.0-final` at the exact boundary commit. The commit SHA remains the controlling reference even if a branch or tag is unavailable. ## Commercial path -Organizations requiring proprietary integration, OEM or white-label distribution, closed-source redistribution, private branches, contractual support, training, warranty, or engineering services may discuss a separate commercial agreement. +Organizations requiring proprietary integration, OEM or white-label distribution, closed-source redistribution, private branches, contractual support, training, warranty, or engineering services may discuss a separate commercial agreement with Ari Sulistiono for rights he controls. `COMMERCIAL-LICENSE.md` is an invitation to negotiate. It is not itself an executed license and grants no additional rights. -Commercial rights can cover only material and rights controlled by the relevant granting party. Third-party components, contributor-owned material, standards, and historical grants remain subject to their own terms. +Commercial rights can cover only material and rights controlled by the relevant granting party. Third-party components, contributor-owned material, standards, historical grants, and private obligations remain subject to their own terms. ## Contributions New contributions require: -- affirmative acceptance of `CONTRIBUTOR-LICENSE-AGREEMENT.md`; +- affirmative acceptance of CLA Version 1.0 in `CONTRIBUTOR-LICENSE-AGREEMENT.md`; - a Developer Certificate of Origin sign-off on every commit; - authority to submit the material, including any required employer or organizational approval; and -- documented provenance for any third-party or non-synthetic fixture. +- documented provenance for any third-party or non-synthetic fixture or asset. -## Packages +## Packages and corresponding source -Current portable packages must include: +Post-transition portable packages must include: ```text LICENSE.txt @@ -55,10 +57,13 @@ COPYRIGHT.md TRADEMARK.md THIRD_PARTY_NOTICES.md Licensing.md +Asset Provenance.md +SOURCE.md +sbom.cdx.json ``` -`LICENSE.txt` must contain GNU GPL version 3. A historical Apache license file must not be included in a current package in a way that suggests Apache-2.0 applies to the current binary. +`LICENSE.txt` must contain GNU GPL version 3. `SOURCE.md` must identify the exact built commit and an immutable source-archive URL. A historical Apache license file must not be included in a post-transition package in a way that suggests Apache-2.0 applies to the current binary. ## Scope limitation -Repository records document licensing intent and public release boundaries. They do not by themselves establish employer ownership, resolve invention-assignment or confidentiality duties, or determine rights in off-repository material. Significant commercial transactions should review the exact contributors, contracts, dependencies, trademarks, fixtures, and deliverables involved. \ No newline at end of file +Repository records document licensing intent and public release boundaries. They do not by themselves establish employer ownership, resolve invention-assignment or confidentiality duties, or determine rights in off-repository material. Significant commercial transactions should review the exact contributors, contracts, dependencies, trademarks, fixtures, assets, and deliverables involved. diff --git a/docs/QUICK_START.pdf b/docs/QUICK_START.pdf index 6367c05..1fd4d28 100644 Binary files a/docs/QUICK_START.pdf and b/docs/QUICK_START.pdf differ diff --git a/docs/RELEASE_NOTES_v1.4.0-beta.2.md b/docs/RELEASE_NOTES_v1.4.0-beta.2.md index 0959a8a..62626d5 100644 --- a/docs/RELEASE_NOTES_v1.4.0-beta.2.md +++ b/docs/RELEASE_NOTES_v1.4.0-beta.2.md @@ -5,19 +5,30 @@ This beta establishes the post-transition licensing, provenance, package, and pu ## Licensing - Current source and this release are `GPL-3.0-or-later`. -- The final active Apache-2.0 revision is `85d43a0fe58a5888a9e8008c168ab76d2333ea87`, preserved on `archive/apache-2.0-final`. +- The final active Apache-2.0 revision is `85d43a0fe58a5888a9e8008c168ab76d2333ea87`, preserved on `archive/apache-2.0-final` and the annotated boundary tag. - Historical v1.4.0-beta.1 and earlier grants remain effective for the revisions and artifacts to which they applied. - Proprietary integration, OEM or white-label distribution, closed-source redistribution, private branches, and contractual services require a separate negotiated agreement. - `COMMERCIAL-LICENSE.md` is an invitation to negotiate and grants no additional rights by itself. -## Repository and package hardening +## Release integrity -- Added copyright, trademark, CLA, DCO, licensing-transition, provenance, and public-wording records. +- The About dialog reads the informational version and build commit from the actual executable instead of presenting stale hard-coded values. +- Quick Start and User Manual PDFs are generated deterministically and identify GPL-3.0-or-later. +- `SOURCE.md` binds the package to its exact built commit, source-head commit, tested merge commit, source ref, and immutable source archive. +- A CycloneDX 1.5 SBOM records the application, .NET runtime, Windows Desktop runtime, and separately installed Npcap prerequisite. +- The package verifier checks license files, source offer, SBOM, EXE version, embedded wording, required PDFs, and optional Authenticode status. +- Public release publication is blocked until the Windows signing certificate secrets are configured. +- Release ZIP and SBOM provenance are attested through GitHub Actions. +- GitHub Pages changes are validated on pull requests and smoke-tested after deployment. + +## Repository and governance hardening + +- Added copyright, trademark, CLA, DCO, transition, provenance, asset-provenance, and public-wording records. - Added automated GPL and commercial-license boundary checks to the repository-health gate. -- Current portable packages include GPL, NOTICE, commercial licensing, copyright, trademark, third-party, and licensing-summary documents. -- Package verification rejects a stale Apache license presented as the current package license. -- Candidate packaging now runs for licensing branches. -- Release manifests identify the community license and historical boundary. +- CLA Version 1.0 acceptance and DCO sign-off are verified for pull requests. +- Current portable packages include GPL, NOTICE, commercial licensing, copyright, trademark, third-party, asset-provenance, licensing-summary, source-offer, and SBOM documents. +- Candidate and release manifests distinguish the source-head, tested merge, and built commits. +- Release assets are not overwritten under an existing historical tag. ## Engineering scope retained @@ -31,4 +42,5 @@ This beta establishes the post-transition licensing, provenance, package, and pu - This remains a public beta, not a certified protection, conformance, timing, calibration, functional-safety, or cybersecurity platform. - Windows/Npcap and replay timestamps remain engineering screening evidence unless the capture path is independently validated. - Observed traffic and software interpretation do not prove that an external IED received, accepted, trusted, or acted on the traffic. -- Npcap is installed separately for live capture. \ No newline at end of file +- Npcap is installed separately for live capture. +- A pull-request candidate may be unsigned. Only a public release that passes the signing gate should be described as the signed official package. diff --git a/docs/USER_MANUAL.pdf b/docs/USER_MANUAL.pdf index 3fdc48a..bd99fa3 100644 Binary files a/docs/USER_MANUAL.pdf and b/docs/USER_MANUAL.pdf differ diff --git a/scripts/configure-repository-governance.ps1 b/scripts/configure-repository-governance.ps1 new file mode 100644 index 0000000..edb8dfc --- /dev/null +++ b/scripts/configure-repository-governance.ps1 @@ -0,0 +1,122 @@ +[CmdletBinding()] +param( + [string] $Repository = 'masarray/DigSubAnalyzer', + [switch] $Apply +) + +$ErrorActionPreference = 'Stop' +Set-StrictMode -Version Latest + +$boundaryCommit = '85d43a0fe58a5888a9e8008c168ab76d2333ea87' +$boundaryTag = 'license-boundary/apache-2.0-final' +$requiredChecks = @( + 'Build, test, documents, and repository health', + 'CLA affirmation and DCO sign-off', + 'Analyze C#', + 'NuGet vulnerability audit', + 'GitHub dependency delta review', + 'Validate website, licensing, and generated documents' +) + +function Invoke-GhJson { + param( + [Parameter(Mandatory)][string[]] $Arguments, + [string] $InputJson + ) + + if (-not $Apply) { + Write-Host "PREVIEW: gh $($Arguments -join ' ')" + if ($InputJson) { Write-Host $InputJson } + return + } + + if ($InputJson) { + $InputJson | gh @Arguments --input - + } + else { + gh @Arguments + } + if ($LASTEXITCODE -ne 0) { throw "GitHub CLI command failed: gh $($Arguments -join ' ')" } +} + +if (-not (Get-Command gh -ErrorAction SilentlyContinue)) { + throw 'GitHub CLI (gh) is required.' +} + +Write-Host "Repository governance target: $Repository" +Write-Host "Mode: $(if ($Apply) { 'APPLY' } else { 'PREVIEW' })" + +if ($Apply) { + git fetch origin $boundaryCommit + if ($LASTEXITCODE -ne 0) { throw 'Could not fetch the historical boundary commit.' } + if (-not (git tag --list $boundaryTag)) { + git tag -a $boundaryTag $boundaryCommit -m 'Final active Apache-2.0 revision' + git push origin $boundaryTag + if ($LASTEXITCODE -ne 0) { throw 'Could not push the historical boundary tag.' } + } +} +else { + Write-Host "PREVIEW: create annotated tag $boundaryTag at $boundaryCommit if it does not already exist" +} + +$branchPayload = [ordered]@{ + required_status_checks = [ordered]@{ + strict = $true + contexts = $requiredChecks + } + enforce_admins = $true + required_pull_request_reviews = [ordered]@{ + dismiss_stale_reviews = $true + require_code_owner_reviews = $true + required_approving_review_count = 1 + require_last_push_approval = $false + } + restrictions = $null + required_linear_history = $true + allow_force_pushes = $false + allow_deletions = $false + block_creations = $false + required_conversation_resolution = $true + lock_branch = $false + allow_fork_syncing = $true +} | ConvertTo-Json -Depth 10 -Compress + +Invoke-GhJson -Arguments @( + 'api', + '--method', 'PUT', + "repos/$Repository/branches/main/protection", + '-H', 'Accept: application/vnd.github+json', + '-H', 'X-GitHub-Api-Version: 2022-11-28' +) -InputJson $branchPayload + +$rulesetPayload = [ordered]@{ + name = 'Protect license boundary tag' + target = 'tag' + enforcement = 'active' + conditions = [ordered]@{ + ref_name = [ordered]@{ + include = @("refs/tags/$boundaryTag") + exclude = @() + } + } + rules = @( + [ordered]@{ type = 'deletion' }, + [ordered]@{ type = 'non_fast_forward' } + ) + bypass_actors = @() +} | ConvertTo-Json -Depth 10 -Compress + +Invoke-GhJson -Arguments @( + 'api', + '--method', 'POST', + "repos/$Repository/rulesets", + '-H', 'Accept: application/vnd.github+json', + '-H', 'X-GitHub-Api-Version: 2022-11-28' +) -InputJson $rulesetPayload + +Write-Host '' +Write-Host 'After applying:' +Write-Host '1. Confirm main requires the listed status checks.' +Write-Host "2. Confirm tag $boundaryTag resolves to $boundaryCommit." +Write-Host '3. Confirm tag deletion and non-fast-forward updates are blocked.' +Write-Host '4. Configure WINDOWS_SIGNING_CERTIFICATE_BASE64 and WINDOWS_SIGNING_CERTIFICATE_PASSWORD before publishing a public release.' diff --git a/scripts/generate-release-pdfs.py b/scripts/generate-release-pdfs.py new file mode 100644 index 0000000..27133b3 --- /dev/null +++ b/scripts/generate-release-pdfs.py @@ -0,0 +1,174 @@ +#!/usr/bin/env python3 +"""Generate deterministic Quick Start and User Manual PDFs for release packages.""" + +from __future__ import annotations + +import argparse +import hashlib +from pathlib import Path + +from reportlab.lib import colors +from reportlab.lib.enums import TA_CENTER +from reportlab.lib.pagesizes import A4 +from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet +from reportlab.lib.units import mm +from reportlab.pdfbase.pdfmetrics import stringWidth +from reportlab.platypus import PageBreak, Paragraph, SimpleDocTemplate, Spacer, Table, TableStyle + +ROOT = Path(__file__).resolve().parents[1] +VERSION = "1.4.0-beta.2" +LICENSE = "GPL-3.0-or-later" +BOUNDARY = "85d43a0fe58a5888a9e8008c168ab76d2333ea87" + + +def styles(): + base = getSampleStyleSheet() + return { + "title": ParagraphStyle("Title", parent=base["Title"], fontName="Helvetica-Bold", fontSize=25, leading=30, textColor=colors.HexColor("#0B365A"), alignment=TA_CENTER, spaceAfter=12), + "subtitle": ParagraphStyle("Subtitle", parent=base["Normal"], fontName="Helvetica", fontSize=11, leading=16, textColor=colors.HexColor("#48657B"), alignment=TA_CENTER, spaceAfter=18), + "h1": ParagraphStyle("H1", parent=base["Heading1"], fontName="Helvetica-Bold", fontSize=17, leading=21, textColor=colors.HexColor("#0B568A"), spaceBefore=10, spaceAfter=8), + "h2": ParagraphStyle("H2", parent=base["Heading2"], fontName="Helvetica-Bold", fontSize=13, leading=17, textColor=colors.HexColor("#123F5C"), spaceBefore=8, spaceAfter=5), + "body": ParagraphStyle("Body", parent=base["BodyText"], fontName="Helvetica", fontSize=9.6, leading=14, textColor=colors.HexColor("#213746"), spaceAfter=6), + "bullet": ParagraphStyle("Bullet", parent=base["BodyText"], fontName="Helvetica", fontSize=9.4, leading=13.5, leftIndent=12, firstLineIndent=-7, bulletIndent=4, textColor=colors.HexColor("#213746"), spaceAfter=4), + "note": ParagraphStyle("Note", parent=base["BodyText"], fontName="Helvetica", fontSize=9, leading=13, borderColor=colors.HexColor("#77B9E6"), borderWidth=0.7, borderPadding=8, backColor=colors.HexColor("#EDF7FD"), textColor=colors.HexColor("#173F59"), spaceBefore=6, spaceAfter=10), + "small": ParagraphStyle("Small", parent=base["BodyText"], fontName="Helvetica", fontSize=7.5, leading=10, textColor=colors.HexColor("#5F7483")), + } + + +def footer(canvas, doc): + canvas.saveState() + canvas.setStrokeColor(colors.HexColor("#D5E2EB")) + canvas.line(18 * mm, 14 * mm, 192 * mm, 14 * mm) + canvas.setFillColor(colors.HexColor("#60798A")) + canvas.setFont("Helvetica", 7.5) + canvas.drawString(18 * mm, 9 * mm, f"Process Bus Insight {VERSION} · {LICENSE}") + text = f"Page {doc.page}" + canvas.drawRightString(192 * mm, 9 * mm, text) + canvas.setTitle(doc.title) + canvas.setAuthor("Ari Sulistiono and Process Bus Insight Contributors") + canvas.setSubject("Receive-only IEC 61850 Process Bus engineering documentation") + canvas.setCreator("Process Bus Insight deterministic PDF generator") + canvas.saveState() + canvas.restoreState() + canvas.restoreState() + + +def cover(st, title, subtitle): + return [ + Spacer(1, 28 * mm), + Paragraph("PROCESS BUS INSIGHT", st["small"]), + Spacer(1, 4 * mm), + Paragraph(title, st["title"]), + Paragraph(subtitle, st["subtitle"]), + Table( + [["Version", VERSION], ["Community license", LICENSE], ["Platform", "Windows 10/11 x64"], ["Product boundary", "Receive-only; no SV/GOOSE publishing or IEC 61850 control commands"]], + colWidths=[46 * mm, 113 * mm], + style=TableStyle([ + ("BACKGROUND", (0, 0), (0, -1), colors.HexColor("#EAF4FA")), + ("TEXTCOLOR", (0, 0), (0, -1), colors.HexColor("#164D70")), + ("FONTNAME", (0, 0), (0, -1), "Helvetica-Bold"), + ("FONTNAME", (1, 0), (1, -1), "Helvetica"), + ("FONTSIZE", (0, 0), (-1, -1), 9), + ("GRID", (0, 0), (-1, -1), 0.4, colors.HexColor("#BFD3DF")), + ("VALIGN", (0, 0), (-1, -1), "TOP"), + ("LEFTPADDING", (0, 0), (-1, -1), 7), + ("RIGHTPADDING", (0, 0), (-1, -1), 7), + ("TOPPADDING", (0, 0), (-1, -1), 7), + ("BOTTOMPADDING", (0, 0), (-1, -1), 7), + ]), + ), + Spacer(1, 12 * mm), + Paragraph("This document describes engineering use and evidence boundaries. It is not a certificate of IEC 61850 conformance, calibrated timing, functional safety, cybersecurity approval, equipment isolation, or switching authority.", st["note"]), + PageBreak(), + ] + + +def add_bullets(story, st, items): + for item in items: + story.append(Paragraph(f"• {item}", st["bullet"])) + + +def build_quick_start(path: Path): + st = styles() + story = cover(st, "Quick Start", "Safe first-run workflow for live capture and sanitized PCAP replay") + story += [Paragraph("1. Prepare the engineering workstation", st["h1"])] + add_bullets(story, st, [ + "Use Windows 10 or Windows 11 x64 and install Npcap separately when live raw Ethernet capture is required.", + "Use an authorized TAP, mirror port, or isolated engineering test switch. Do not insert an unverified workstation into a protection-critical path.", + "Avoid Wi-Fi, VPN, virtual, loopback, and unverified USB adapters for serious arrival-timing interpretation.", + ]) + story += [Paragraph("2. Verify and run the package", st["h1"])] + add_bullets(story, st, [ + "Verify the ZIP against SHA256SUMS.txt and review release-manifest.json, SOURCE.md, and sbom.cdx.json.", + "Extract the package to a local folder and run ProcessBusInsight.exe.", + "Open About and confirm the displayed version and build commit match the package evidence.", + ]) + story += [Paragraph("3. Capture or replay", st["h1"])] + add_bullets(story, st, [ + "Select the intended physical adapter before starting capture.", + "Confirm SV, GOOSE, and PTP evidence appears only from the expected observation point.", + "Use sanitized classic Ethernet PCAP replay for reproducible offline investigation. PCAPNG is not currently claimed.", + ]) + story += [Paragraph("4. Record scoped evidence", st["h1"])] + add_bullets(story, st, [ + "Record adapter, capture path, APPID, VLAN, MAC, svID or GOOSE identity, timestamp source, SCL expectation, and observed field.", + "Keep configured expectation, observed traffic, software interpretation, and external-device response separate.", + "Treat ordinary Windows/Npcap timing as screening evidence unless independently validated with suitable hardware and procedures.", + ]) + story += [Paragraph("Licensing", st["h1"]), Paragraph(f"Post-transition source and packages are {LICENSE}. Historical Apache-2.0 rights remain attached only to revisions at or before commit {BOUNDARY}. COMMERCIAL-LICENSE.md is an invitation to negotiate and grants no additional rights by itself.", st["body"])] + doc = SimpleDocTemplate(str(path), pagesize=A4, rightMargin=18 * mm, leftMargin=18 * mm, topMargin=18 * mm, bottomMargin=20 * mm, title="Process Bus Insight Quick Start") + doc.build(story, onFirstPage=footer, onLaterPages=footer) + + +def build_manual(path: Path): + st = styles() + story = cover(st, "User Manual", "IEC 61850 SV, GOOSE, PTP, SCL, and PCAP evidence workflow") + sections = [ + ("Product boundary", ["Process Bus Insight is receive-only and raw-passive.", "It does not publish Sampled Values or GOOSE and does not send IEC 61850 control commands.", "It complements—not replaces—approved test equipment, project procedures, and engineering authority."]), + ("Sampled Values workspace", ["Use stream identity, APPID, svID, VLAN, source MAC, confRev, counters, waveform, RMS, phasor, and sequence diagnostics for the selected stream.", "The selected stream owns the displayed waveform, metering, phasor, and details. Cross-stream combinations are not valid evidence.", "Missing, duplicate, gap, or timing indications are investigation leads whose confidence depends on the capture path."]), + ("GOOSE workspace", ["Inspect publisher identity, stNum, sqNum, typed dataset values, and event history.", "Observed frames do not prove that an external IED accepted, trusted, or acted on the traffic."]), + ("PTP timing context", ["Review visible transport, domain, message type, grandmaster context, and freshness where decoded.", "The application does not discipline clocks or certify time synchronization."]), + ("SCL expected-vs-observed", ["Load SCD, ICD, or CID files and compare expected APPID, destination MAC, VLAN, DataSet, svID, confRev, and related identity evidence.", "A match describes evidence at the selected observation point; it is not formal conformance certification."]), + ("PCAP replay", ["Classic Ethernet PCAP can be replayed through the same decoder/analyzer entry point used by live capture.", "Replay preserves recorded evidence but cannot recreate switch loading, NIC buffering, unrecorded packet loss, or hardware timestamp behavior."]), + ("Security and data handling", ["Treat captures and engineering files as potentially sensitive.", "Sanitize customer, employer, station, device, credential, MAC/IP, and project identifiers before sharing.", "Report parser or package-integrity vulnerabilities through a private GitHub security advisory."]), + ("Licensing and source", [f"Post-transition community revisions and packages use {LICENSE}.", f"The historical Apache-2.0 boundary is commit {BOUNDARY}.", "SOURCE.md identifies the exact source corresponding to a packaged binary. The commercial notice is not itself a license."]), + ] + for heading, bullets in sections: + story.append(Paragraph(heading, st["h1"])) + add_bullets(story, st, bullets) + story += [Paragraph("Evidence wording", st["h1"]), Paragraph("Use terms such as configured, observed, decoded, calculated, replayed, screened, provisional, unsupported, or not yet verified. Do not claim certification, calibration, deterministic timing, functional safety, cybersecurity approval, universal interoperability, switching authority, or IED acceptance without separate competent evidence.", st["note"])] + doc = SimpleDocTemplate(str(path), pagesize=A4, rightMargin=18 * mm, leftMargin=18 * mm, topMargin=18 * mm, bottomMargin=20 * mm, title="Process Bus Insight User Manual") + doc.build(story, onFirstPage=footer, onLaterPages=footer) + + +def digest(path: Path) -> str: + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--check", action="store_true", help="Regenerate in memory location and compare SHA256 with tracked PDFs") + args = parser.parse_args() + docs = ROOT / "docs" + docs.mkdir(exist_ok=True) + quick = docs / "QUICK_START.pdf" + manual = docs / "USER_MANUAL.pdf" + if args.check: + tmp = ROOT / "artifacts" / "pdf-check" + tmp.mkdir(parents=True, exist_ok=True) + tq, tm = tmp / quick.name, tmp / manual.name + build_quick_start(tq) + build_manual(tm) + mismatches = [p.name for p, generated in ((quick, tq), (manual, tm)) if not p.exists() or digest(p) != digest(generated)] + if mismatches: + raise SystemExit("Generated PDFs are stale: " + ", ".join(mismatches)) + print("Release PDFs are deterministic and current.") + return + build_quick_start(quick) + build_manual(manual) + print(f"Generated {quick} ({digest(quick)})") + print(f"Generated {manual} ({digest(manual)})") + + +if __name__ == "__main__": + main() diff --git a/scripts/generate-sbom.ps1 b/scripts/generate-sbom.ps1 new file mode 100644 index 0000000..590e92a --- /dev/null +++ b/scripts/generate-sbom.ps1 @@ -0,0 +1,145 @@ +[CmdletBinding()] +param( + [Parameter(Mandatory)] + [ValidatePattern('^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$')] + [string] $Version, + + [string] $OutputPath +) + +$ErrorActionPreference = 'Stop' +Set-StrictMode -Version Latest + +function New-DeterministicUuidUrn { + param([Parameter(Mandatory)][string] $Value) + + $sha256 = [System.Security.Cryptography.SHA256]::Create() + try { + $hash = $sha256.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($Value)) + } + finally { + $sha256.Dispose() + } + + $uuidBytes = [byte[]]::new(16) + [Array]::Copy($hash, $uuidBytes, $uuidBytes.Length) + $uuidBytes[6] = [byte](($uuidBytes[6] -band 0x0F) -bor 0x50) + $uuidBytes[8] = [byte](($uuidBytes[8] -band 0x3F) -bor 0x80) + $hex = -join ($uuidBytes | ForEach-Object { $_.ToString('x2') }) + $uuid = '{0}-{1}-{2}-{3}-{4}' -f $hex.Substring(0,8), $hex.Substring(8,4), $hex.Substring(12,4), $hex.Substring(16,4), $hex.Substring(20,12) + "urn:uuid:$uuid" +} + +function Get-HighestRuntimeVersion { + param([Parameter(Mandatory)][string] $RuntimeName) + + $lines = & dotnet --list-runtimes 2>&1 + if ($LASTEXITCODE -ne 0) { + throw "dotnet --list-runtimes failed.`n$($lines -join [Environment]::NewLine)" + } + + $versions = foreach ($line in $lines) { + if ($line -match ('^' + [regex]::Escape($RuntimeName) + '\s+([^\s]+)\s+')) { + try { [version] $Matches[1] } catch { } + } + } + + $selected = $versions | Where-Object { $_.Major -eq 8 } | Sort-Object -Descending | Select-Object -First 1 + if (-not $selected) { + throw "Could not resolve an installed .NET 8 runtime for $RuntimeName." + } + + $selected.ToString() +} + +$root = Split-Path -Parent $PSScriptRoot +if ([string]::IsNullOrWhiteSpace($OutputPath)) { + $OutputPath = Join-Path $root 'artifacts\release\ProcessBusInsight-SBOM.cdx.json' +} +elseif (-not [System.IO.Path]::IsPathRooted($OutputPath)) { + $OutputPath = Join-Path $root $OutputPath +} + +New-Item -ItemType Directory -Path (Split-Path -Parent $OutputPath) -Force | Out-Null + +$sourceCommit = (& git -C $root rev-parse HEAD 2>&1) -join '' +if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($sourceCommit)) { + throw 'Could not resolve source commit for SBOM.' +} +$sourceCommit = $sourceCommit.Trim() + +$sourceTimestampText = (& git -C $root show -s --format=%cI HEAD 2>&1) -join '' +if ($LASTEXITCODE -ne 0) { + throw 'Could not resolve source commit timestamp for SBOM.' +} +$sourceTimestamp = [DateTimeOffset]::Parse($sourceTimestampText.Trim(), [System.Globalization.CultureInfo]::InvariantCulture).ToUniversalTime().ToString('o') + +$netRuntimeVersion = Get-HighestRuntimeVersion 'Microsoft.NETCore.App' +$windowsDesktopVersion = Get-HighestRuntimeVersion 'Microsoft.WindowsDesktop.App' +$serialNumber = New-DeterministicUuidUrn "https://github.com/masarray/DigSubAnalyzer|$Version|$sourceCommit" + +$components = @( + [ordered]@{ + type = 'framework' + 'bom-ref' = "pkg:generic/Microsoft.NETCore.App@$netRuntimeVersion" + name = 'Microsoft.NETCore.App' + version = $netRuntimeVersion + licenses = @([ordered]@{ license = [ordered]@{ id = 'MIT' } }) + properties = @([ordered]@{ name = 'processbus:distribution'; value = 'included in self-contained publish' }) + }, + [ordered]@{ + type = 'framework' + 'bom-ref' = "pkg:generic/Microsoft.WindowsDesktop.App@$windowsDesktopVersion" + name = 'Microsoft.WindowsDesktop.App' + version = $windowsDesktopVersion + licenses = @([ordered]@{ license = [ordered]@{ id = 'MIT' } }) + properties = @([ordered]@{ name = 'processbus:distribution'; value = 'included in self-contained WPF publish' }) + }, + [ordered]@{ + type = 'application' + 'bom-ref' = 'pkg:generic/Npcap@externally-installed' + name = 'Npcap' + version = 'externally-installed' + licenses = @([ordered]@{ license = [ordered]@{ name = 'Npcap license'; url = 'https://npcap.com/oem/redist.html' } }) + properties = @([ordered]@{ name = 'processbus:distribution'; value = 'runtime prerequisite; not bundled' }) + } +) + +$sbom = [ordered]@{ + bomFormat = 'CycloneDX' + specVersion = '1.5' + serialNumber = $serialNumber + version = 1 + metadata = [ordered]@{ + timestamp = $sourceTimestamp + tools = [ordered]@{ + components = @([ordered]@{ + type = 'application' + author = 'Process Bus Insight project' + name = 'generate-sbom.ps1' + version = '1.0.0' + }) + } + component = [ordered]@{ + type = 'application' + 'bom-ref' = "pkg:generic/ProcessBusInsight@$Version" + name = 'Process Bus Insight' + version = $Version + licenses = @([ordered]@{ license = [ordered]@{ id = 'GPL-3.0-or-later' } }) + properties = @( + [ordered]@{ name = 'processbus:source-commit'; value = $sourceCommit }, + [ordered]@{ name = 'processbus:historical-license-boundary'; value = '85d43a0fe58a5888a9e8008c168ab76d2333ea87' }, + [ordered]@{ name = 'processbus:product-boundary'; value = 'receive-only' } + ) + } + } + components = $components +} + +$json = $sbom | ConvertTo-Json -Depth 20 +[System.IO.File]::WriteAllText($OutputPath, $json + [Environment]::NewLine, [System.Text.UTF8Encoding]::new($false)) + +Write-Host "CycloneDX SBOM written: $OutputPath" +Write-Host "Source commit: $sourceCommit" +Write-Host "Serial number: $serialNumber" +Write-Host "Components: $($components.Count)" diff --git a/scripts/publish-windows-portable.ps1 b/scripts/publish-windows-portable.ps1 index 6be03e7..5e70d98 100644 --- a/scripts/publish-windows-portable.ps1 +++ b/scripts/publish-windows-portable.ps1 @@ -1,3 +1,4 @@ +[CmdletBinding()] param( [string]$Version = "1.4.0-beta.2", [string]$Configuration = "Release", @@ -10,14 +11,57 @@ param( ) $ErrorActionPreference = "Stop" +Set-StrictMode -Version Latest $repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..") Set-Location $repoRoot + +function Get-GitText { + param([Parameter(Mandatory)][string[]]$Arguments) + $output = & git -C $repoRoot @Arguments 2>&1 + if ($LASTEXITCODE -ne 0) { + throw "git $($Arguments -join ' ') failed.`n$($output -join [Environment]::NewLine)" + } + ($output -join '').Trim() +} + +function Invoke-OptionalAuthenticodeSigning { + param([Parameter(Mandatory)][string]$FilePath) + + if ([string]::IsNullOrWhiteSpace($env:WINDOWS_SIGNING_CERTIFICATE_BASE64)) { + Write-Warning "Authenticode signing certificate is not configured. The package will be unsigned." + return 'unsigned' + } + if ([string]::IsNullOrWhiteSpace($env:WINDOWS_SIGNING_CERTIFICATE_PASSWORD)) { + throw "WINDOWS_SIGNING_CERTIFICATE_PASSWORD is required when a signing certificate is configured." + } + + $certificatePath = Join-Path $env:RUNNER_TEMP "processbus-signing-$([Guid]::NewGuid().ToString('N')).pfx" + try { + [System.IO.File]::WriteAllBytes($certificatePath, [Convert]::FromBase64String($env:WINDOWS_SIGNING_CERTIFICATE_BASE64)) + $securePassword = ConvertTo-SecureString $env:WINDOWS_SIGNING_CERTIFICATE_PASSWORD -AsPlainText -Force + $certificate = Get-PfxCertificate -FilePath $certificatePath -Password $securePassword + if (-not $certificate.HasPrivateKey) { + throw "The configured signing certificate does not contain a private key." + } + + $signature = Set-AuthenticodeSignature -FilePath $FilePath -Certificate $certificate -HashAlgorithm SHA256 -TimestampServer "http://timestamp.digicert.com" + if ($signature.Status -ne [System.Management.Automation.SignatureStatus]::Valid) { + throw "Authenticode signing failed: $($signature.Status) - $($signature.StatusMessage)" + } + Write-Host "Authenticode signature applied: $($certificate.Subject)" + return 'valid' + } + finally { + Remove-Item -LiteralPath $certificatePath -Force -ErrorAction SilentlyContinue + } +} + if ([string]::IsNullOrWhiteSpace($Version)) { throw "Version cannot be empty." } $requiredReleaseDocs = @( "docs/QUICK_START.pdf", "docs/USER_MANUAL.pdf", "LICENSE", "NOTICE", "COMMERCIAL-LICENSE.md", "COPYRIGHT.md", "TRADEMARK.md", - "THIRD_PARTY_NOTICES.md", "docs/LICENSING.md" + "THIRD_PARTY_NOTICES.md", "docs/LICENSING.md", "docs/ASSET_PROVENANCE.md" ) foreach ($doc in $requiredReleaseDocs) { if (-not (Test-Path (Join-Path $repoRoot $doc))) { throw "Required release document is missing: $doc" } @@ -36,6 +80,8 @@ $publishDir = Join-Path $repoRoot "$OutputRoot/publish/$portableName/publish" $stageDir = Join-Path $repoRoot "$OutputRoot/package/$portableName" $releaseDir = Join-Path $repoRoot "$OutputRoot/release" $zipPath = Join-Path $releaseDir "$portableName.zip" +$sbomReleasePath = Join-Path $releaseDir "ProcessBusInsight-SBOM.cdx.json" +$sourceReleasePath = Join-Path $releaseDir "SOURCE.md" $shaPath = Join-Path $releaseDir "SHA256SUMS.txt" $selfContained = -not $FrameworkDependent.IsPresent $singleFile = -not $MultiFile.IsPresent @@ -44,12 +90,14 @@ Write-Host "Publishing $AppName $Version for $Runtime" Write-Host "Community license: GPL-3.0-or-later" Remove-Item -LiteralPath $publishDir -Recurse -Force -ErrorAction SilentlyContinue Remove-Item -LiteralPath $stageDir -Recurse -Force -ErrorAction SilentlyContinue -New-Item -ItemType Directory -Path $publishDir, $stageDir, $releaseDir | Out-Null +New-Item -ItemType Directory -Path $publishDir, $stageDir, $releaseDir -Force | Out-Null $resolvedProjectPath = (Resolve-Path $ProjectPath).Path dotnet restore $resolvedProjectPath -r $Runtime /p:ContinuousIntegrationBuild=true if ($LASTEXITCODE -ne 0) { throw "Runtime-specific restore failed." } +$builtCommit = Get-GitText @('rev-parse','HEAD') +$shortCommit = $builtCommit.Substring(0, [Math]::Min(12, $builtCommit.Length)) $versionParts = $Version.Split('-', 2) $versionPrefix = $versionParts[0] $versionSuffix = if ($versionParts.Length -gt 1) { $versionParts[1] } else { '' } @@ -58,7 +106,7 @@ $publishArgs = @( "-r", $Runtime, "-o", $publishDir, "/p:Version=$Version", "/p:VersionPrefix=$versionPrefix", "/p:VersionSuffix=$versionSuffix", "/p:PackageVersion=$Version", "/p:AssemblyVersion=$versionPrefix.0", - "/p:FileVersion=$versionPrefix.0", "/p:InformationalVersion=$Version", + "/p:FileVersion=$versionPrefix.0", "/p:InformationalVersion=$Version+$shortCommit", "/p:PublishSingleFile=$($singleFile.ToString().ToLowerInvariant())", "/p:IncludeNativeLibrariesForSelfExtract=true", "/p:EnableCompressionInSingleFile=true", "/p:PublishTrimmed=false", "/p:DebugType=None", "/p:DebugSymbols=false", @@ -81,6 +129,50 @@ if ($singleFile) { Copy-Item (Join-Path $publishDir "*") $appStageDir -Recurse -Force } +$stagedExe = Join-Path $stageDir "$AppName.exe" +$signatureStatus = if (Test-Path $stagedExe) { Invoke-OptionalAuthenticodeSigning $stagedExe } else { 'multi-file' } + +$sourceHeadCommit = if ([string]::IsNullOrWhiteSpace($env:SOURCE_HEAD_COMMIT)) { $builtCommit } else { $env:SOURCE_HEAD_COMMIT } +$testedMergeCommit = if ([string]::IsNullOrWhiteSpace($env:TESTED_MERGE_COMMIT)) { $builtCommit } else { $env:TESTED_MERGE_COMMIT } +$sourceRef = if ([string]::IsNullOrWhiteSpace($env:SOURCE_REF)) { Get-GitText @('rev-parse','--abbrev-ref','HEAD') } else { $env:SOURCE_REF } +$workflowUrl = if (-not [string]::IsNullOrWhiteSpace($env:GITHUB_RUN_ID) -and -not [string]::IsNullOrWhiteSpace($env:GITHUB_REPOSITORY)) { + "https://github.com/$($env:GITHUB_REPOSITORY)/actions/runs/$($env:GITHUB_RUN_ID)" +} else { + 'Local build - no GitHub Actions run URL' +} + +$sourceOffer = @" +# Corresponding Source and Build Identity + +Product: Process Bus Insight / DigSubAnalyzer +Version: $Version +Community license: GPL-3.0-or-later +Built commit: $builtCommit +Source-head commit: $sourceHeadCommit +Tested merge commit: $testedMergeCommit +Source ref: $sourceRef +Authenticode status: $signatureStatus + +Exact source tree: +https://github.com/masarray/DigSubAnalyzer/tree/$builtCommit + +Immutable source archive for the built commit: +https://github.com/masarray/DigSubAnalyzer/archive/$builtCommit.zip + +Repository: +https://github.com/masarray/DigSubAnalyzer + +Build workflow: +$workflowUrl + +This file identifies the source corresponding to this binary package. Retain it with the package. Historical Apache-2.0 grants remain attached only to the revisions to which they originally applied; this post-transition package is GPL-3.0-or-later. +"@ +[System.IO.File]::WriteAllText((Join-Path $stageDir 'SOURCE.md'), $sourceOffer, [System.Text.UTF8Encoding]::new($false)) +[System.IO.File]::WriteAllText($sourceReleasePath, $sourceOffer, [System.Text.UTF8Encoding]::new($false)) + +& (Join-Path $PSScriptRoot 'generate-sbom.ps1') -Version $Version -OutputPath $sbomReleasePath +Copy-Item $sbomReleasePath (Join-Path $stageDir 'sbom.cdx.json') -Force + $packageReadme = @" Process Bus Insight / DigSubAnalyzer Windows portable package @@ -90,11 +182,12 @@ Community license: GPL-3.0-or-later 1. Install Npcap separately when live raw Ethernet capture is required. 2. Extract this ZIP to a local folder. 3. Run $AppName.exe. -4. Use an authorized TAP, mirror port, or isolated engineering test network. +4. Confirm the version, GPL license, and build commit in About. +5. Use an authorized TAP, mirror port, or isolated engineering test network. -Included legal documents: +Included integrity and legal documents: LICENSE.txt, NOTICE.txt, COMMERCIAL-LICENSE.md, COPYRIGHT.md, TRADEMARK.md, -THIRD_PARTY_NOTICES.md, and Licensing.md. +THIRD_PARTY_NOTICES.md, Licensing.md, Asset Provenance.md, SOURCE.md, and sbom.cdx.json. Proprietary, OEM, white-label, closed-source redistribution, private-branch, or contractual service rights require a separate negotiated agreement. The commercial notice grants no additional rights by itself. @@ -102,7 +195,7 @@ Windows/Npcap timestamps are screening evidence unless independently validated. https://github.com/masarray/DigSubAnalyzer "@ -Set-Content (Join-Path $stageDir "README.txt") $packageReadme -Encoding UTF8 +[System.IO.File]::WriteAllText((Join-Path $stageDir "README.txt"), $packageReadme, [System.Text.UTF8Encoding]::new($false)) $copyMap = @( @{ Source = "LICENSE"; Destination = "LICENSE.txt" }, @@ -112,6 +205,7 @@ $copyMap = @( @{ Source = "TRADEMARK.md"; Destination = "TRADEMARK.md" }, @{ Source = "THIRD_PARTY_NOTICES.md"; Destination = "THIRD_PARTY_NOTICES.md" }, @{ Source = "docs/LICENSING.md"; Destination = "Licensing.md" }, + @{ Source = "docs/ASSET_PROVENANCE.md"; Destination = "Asset Provenance.md" }, @{ Source = "docs/QUICK_START.pdf"; Destination = "Quick Start.pdf" }, @{ Source = "docs/USER_MANUAL.pdf"; Destination = "User Manual.pdf" } ) @@ -127,7 +221,15 @@ if (Test-Path (Join-Path $stageDir "LICENSE-APACHE-2.0")) { Remove-Item $zipPath -Force -ErrorAction SilentlyContinue Compress-Archive -Path (Join-Path $stageDir "*") -DestinationPath $zipPath -Force -$hash = Get-FileHash $zipPath -Algorithm SHA256 -Set-Content $shaPath "$($hash.Hash.ToLowerInvariant()) $(Split-Path $zipPath -Leaf)" -Encoding ASCII + +$checksumFiles = @($zipPath, $sbomReleasePath, $sourceReleasePath) +$checksumLines = foreach ($file in $checksumFiles) { + $hash = (Get-FileHash $file -Algorithm SHA256).Hash.ToLowerInvariant() + "$hash $(Split-Path $file -Leaf)" +} +[System.IO.File]::WriteAllLines($shaPath, $checksumLines, [System.Text.UTF8Encoding]::new($false)) + Write-Host "Created: $zipPath" +Write-Host "Created: $sbomReleasePath" +Write-Host "Created: $sourceReleasePath" Write-Host "Created: $shaPath" diff --git a/scripts/repository-health.ps1 b/scripts/repository-health.ps1 index 857a724..34afa05 100644 --- a/scripts/repository-health.ps1 +++ b/scripts/repository-health.ps1 @@ -3,6 +3,7 @@ param( ) $ErrorActionPreference = "Stop" +Set-StrictMode -Version Latest $repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..") Set-Location $repoRoot @@ -43,10 +44,16 @@ $required = @( ".github/workflows/runtime-architecture.yml", ".github/workflows/candidate-package.yml", ".github/workflows/codeql.yml", + ".github/workflows/contribution-governance.yml", ".github/workflows/dependency-review.yml", ".github/workflows/pages.yml", ".github/workflows/release-package.yml", + "scripts/generate-release-pdfs.py", + "scripts/generate-sbom.ps1", + "scripts/validate-public-content.py", + "scripts/verify-contribution-governance.py", "docs/LICENSING.md", + "docs/ASSET_PROVENANCE.md", "docs/LICENSE_TRANSITION_RECORD_2026-07-15.md", "docs/EXTERNAL_IP_AND_PROVENANCE_REVIEW_2026-07-15.md", "docs/WORDING_AND_CLAIM_REVIEW_2026-07-15.md", @@ -119,15 +126,33 @@ if ($null -eq $licenseExpression -or $licenseExpression.InnerText.Trim() -ne 'GP } $commercial = Read-RepoText "COMMERCIAL-LICENSE.md" -if ($commercial -notmatch 'not itself a commercial license' -or $commercial -notmatch 'grants no additional rights') { - throw "Commercial notice must state that it is not itself a license and grants no additional rights." +foreach ($marker in @('Licensor:', 'Ari Sulistiono', 'not itself a commercial license', 'grants no additional rights')) { + if ($commercial -notmatch [regex]::Escape($marker)) { + throw "COMMERCIAL-LICENSE.md is missing required marker: $marker" + } +} + +$cla = Read-RepoText "CONTRIBUTOR-LICENSE-AGREEMENT.md" +foreach ($marker in @('CLA Version:', '1.0', 'Effective date:', '2026-07-17', 'Project owner / Licensor:', 'Ari Sulistiono')) { + if ($cla -notmatch [regex]::Escape($marker)) { + throw "CONTRIBUTOR-LICENSE-AGREEMENT.md is missing required marker: $marker" + } +} + +$pullRequestTemplate = Read-RepoText ".github/pull_request_template.md" +$claAcceptance = 'I have read and affirmatively accept CONTRIBUTOR-LICENSE-AGREEMENT.md (CLA Version 1.0, effective 2026-07-17)' +if ($pullRequestTemplate -notmatch [regex]::Escape($claAcceptance)) { + throw "Pull-request template is missing the versioned CLA acceptance statement." } $licensing = Read-RepoText "docs/LICENSING.md" foreach ($marker in @( 'GPL-3.0-or-later', '85d43a0fe58a5888a9e8008c168ab76d2333ea87', - 'archive/apache-2.0-final' + 'archive/apache-2.0-final', + 'license-boundary/apache-2.0-final', + 'SOURCE.md', + 'sbom.cdx.json' )) { if ($licensing -notmatch [regex]::Escape($marker)) { throw "docs/LICENSING.md is missing required marker: $marker" @@ -153,6 +178,23 @@ if ($site -notmatch 'commercial agreement' -or $site -notmatch 'grants no additi throw "Landing page does not explain the separate commercial path." } +$about = Read-RepoText "src/ProcessBus.App.Wpf/Views/AboutWindow.xaml" +foreach ($forbidden in @('Source code is licensed under Apache-2.0', 'Text="Apache-2.0"', 'Version 1.0.0', 'Build 2026.04', 'oscilloscope-level')) { + if ($about -match [regex]::Escape($forbidden)) { + throw "AboutWindow.xaml contains stale public wording: $forbidden" + } +} +foreach ($marker in @('GPL-3.0-or-later', 'Separate negotiated and executed agreement', 'does not by itself prove')) { + if ($about -notmatch [regex]::Escape($marker)) { + throw "AboutWindow.xaml is missing required marker: $marker" + } +} + +$aboutCode = Read-RepoText "src/ProcessBus.App.Wpf/Views/AboutWindow.xaml.cs" +if ($aboutCode -notmatch 'AssemblyInformationalVersionAttribute' -or $aboutCode -notmatch 'VersionText' -or $aboutCode -notmatch 'BuildText') { + throw "About dialog does not resolve version/build identity dynamically." +} + $prefixNode = $props.SelectSingleNode("/Project/PropertyGroup/VersionPrefix") $suffixNode = $props.SelectSingleNode("/Project/PropertyGroup/VersionSuffix") if ($null -eq $prefixNode) { throw "VersionPrefix is missing." } @@ -167,9 +209,15 @@ if (-not [string]::IsNullOrWhiteSpace($ExpectedVersion) -and $ExpectedVersion -n } $releaseWorkflow = Read-RepoText ".github/workflows/release-package.yml" -if ($releaseWorkflow -notmatch [regex]::Escape($version)) { - throw "Release workflow does not reference repository version $version." +foreach ($marker in @($version, 'WINDOWS_SIGNING_CERTIFICATE_BASE64', 'actions/attest@', 'SOURCE.md', 'ProcessBusInsight-SBOM.cdx.json')) { + if ($releaseWorkflow -notmatch [regex]::Escape($marker)) { + throw "Release workflow is missing required release-integrity marker: $marker" + } } +if ($releaseWorkflow -match '--clobber') { + throw "Release workflow must not overwrite historical release assets." +} + if ($site -notmatch ('"softwareVersion"\s*:\s*"' + [regex]::Escape($version) + '"')) { throw "docs/index.html softwareVersion is not $version." } @@ -188,6 +236,11 @@ if ($solution -notmatch 'ProcessBus.Tests') { throw "ProcessBus.Tests is not included in ProcessBusSuite.sln." } +$trackedStaging = $paths | Where-Object { $_ -like '.github/patch/*' -or $_ -eq '__test_content__' } +if ($trackedStaging) { + throw "Temporary patch staging files remain tracked: $($trackedStaging -join ', ')" +} + Write-Host "Repository health: PASS" -ForegroundColor Green Write-Host "Version: $version" Write-Host "Current community license: GPL-3.0-or-later" diff --git a/scripts/validate-public-content.py b/scripts/validate-public-content.py new file mode 100644 index 0000000..c7c7ef1 --- /dev/null +++ b/scripts/validate-public-content.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 +"""Validate public license, version, claim, and generated-document wording.""" + +from __future__ import annotations + +import re +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +VERSION = "1.4.0-beta.2" +BOUNDARY = "85d43a0fe58a5888a9e8008c168ab76d2333ea87" + +TEXT_FILES = [ + "README.md", + "NOTICE", + "COMMERCIAL-LICENSE.md", + "CONTRIBUTOR-LICENSE-AGREEMENT.md", + "THIRD_PARTY_NOTICES.md", + "docs/LICENSING.md", + "docs/index.html", + "src/ProcessBus.App.Wpf/Views/AboutWindow.xaml", + "src/ProcessBus.App.Wpf/Views/AboutWindow.xaml.cs", +] + +REQUIRED = { + "README.md": ["GPL-3.0-or-later", BOUNDARY, "COMMERCIAL-LICENSE.md"], + "NOTICE": ["GPL-3.0-or-later", BOUNDARY, "grants no additional rights"], + "docs/LICENSING.md": ["GPL-3.0-or-later", BOUNDARY, "SOURCE.md", "sbom.cdx.json"], + "docs/index.html": [VERSION, "GPL-3.0-or-later", "grants no additional rights"], + "src/ProcessBus.App.Wpf/Views/AboutWindow.xaml": ["GPL-3.0-or-later", "Separate negotiated and executed agreement", "does not by itself prove"], + "src/ProcessBus.App.Wpf/Views/AboutWindow.xaml.cs": ["AssemblyInformationalVersionAttribute", "VersionText", "BuildText"], +} + +FORBIDDEN = [ + (re.compile(r"Source code is licensed under Apache-2\.0", re.I), "active Apache source-license claim"), + (re.compile(r"Text=\"Apache-2\.0\"", re.I), "active Apache About value"), + (re.compile(r"Version 1\.0\.0", re.I), "stale About version"), + (re.compile(r"Build 2026\.04", re.I), "stale About build"), + (re.compile(r"oscilloscope-level", re.I), "unqualified oscilloscope-equivalence claim"), + (re.compile(r"certified timing", re.I), "unqualified certified-timing claim"), +] + + +def fail(message: str) -> None: + print(f"ERROR: {message}", file=sys.stderr) + raise SystemExit(1) + + +def read(path: str) -> str: + target = ROOT / path + if not target.exists(): + fail(f"missing required public file: {path}") + return target.read_text(encoding="utf-8") + + +def validate_text() -> None: + contents = {path: read(path) for path in TEXT_FILES} + for path, markers in REQUIRED.items(): + for marker in markers: + if marker not in contents[path]: + fail(f"{path} is missing required marker: {marker}") + joined = "\n".join(f"--- {name} ---\n{text}" for name, text in contents.items()) + for pattern, description in FORBIDDEN: + if pattern.search(joined): + fail(f"public content contains {description}") + if "https://spdx.org/licenses/GPL-3.0-or-later.html" not in contents["docs/index.html"]: + fail("landing-page structured data does not identify GPL-3.0-or-later") + + +def pdf_text(path: Path) -> str: + try: + from pypdf import PdfReader + except ImportError as exc: + raise SystemExit("pypdf is required for PDF validation") from exc + return "\n".join(page.extract_text() or "" for page in PdfReader(str(path)).pages) + + +def validate_pdfs() -> None: + for name in ("QUICK_START.pdf", "USER_MANUAL.pdf"): + path = ROOT / "docs" / name + if not path.exists(): + fail(f"missing generated release document: docs/{name}") + text = pdf_text(path) + for marker in ("Process Bus Insight", VERSION, "GPL-3.0-or-later"): + if marker not in text: + fail(f"docs/{name} is missing generated marker: {marker}") + if re.search(r"Free and open source under Apache-2\.0|Community license\s+Apache-2\.0", text, re.I): + fail(f"docs/{name} still presents Apache-2.0 as the current license") + + +def main() -> None: + validate_text() + validate_pdfs() + print("Public wording, licensing, version, and PDF validation: PASS") + + +if __name__ == "__main__": + main() diff --git a/scripts/verify-contribution-governance.py b/scripts/verify-contribution-governance.py new file mode 100644 index 0000000..96d143b --- /dev/null +++ b/scripts/verify-contribution-governance.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +"""Enforce versioned CLA affirmation and DCO sign-off for pull requests.""" + +from __future__ import annotations + +import json +import os +import re +import subprocess +import sys +from pathlib import Path + +CLA_MARKER = "I have read and affirmatively accept CONTRIBUTOR-LICENSE-AGREEMENT.md (CLA Version 1.0, effective 2026-07-17)" +DCO_RE = re.compile(r"^Signed-off-by:\s+.+\s+<[^>]+>\s*$", re.IGNORECASE | re.MULTILINE) + + +def fail(message: str) -> None: + print(f"ERROR: {message}", file=sys.stderr) + raise SystemExit(1) + + +def run(*args: str) -> str: + result = subprocess.run(args, check=False, text=True, capture_output=True) + if result.returncode != 0: + fail(f"Command failed: {' '.join(args)}\n{result.stdout}\n{result.stderr}") + return result.stdout.strip() + + +def load_event() -> dict: + event_path = os.getenv("GITHUB_EVENT_PATH") + if not event_path: + print("No GITHUB_EVENT_PATH; contribution-governance check skipped outside GitHub Actions.") + return {} + return json.loads(Path(event_path).read_text(encoding="utf-8")) + + +def main() -> None: + event = load_event() + pr = event.get("pull_request") + if not pr: + print("Not a pull-request event; contribution-governance check skipped.") + return + + body = pr.get("body") or "" + checked_marker = f"- [x] {CLA_MARKER}" + if checked_marker.lower() not in body.lower(): + fail(f"Pull request body must contain the checked CLA affirmation:\n{checked_marker}") + + base_sha = pr["base"]["sha"] + head_sha = pr["head"]["sha"] + run("git", "fetch", "--no-tags", "origin", base_sha, head_sha) + + commit_shas = [x for x in run("git", "rev-list", "--reverse", f"{base_sha}..{head_sha}").splitlines() if x] + if not commit_shas: + fail("Pull request contains no commits to validate.") + + failures: list[str] = [] + for sha in commit_shas: + message = run("git", "show", "-s", "--format=%B", sha) + if not DCO_RE.search(message): + failures.append(sha) + + if failures: + fail("Every pull-request commit must contain a DCO Signed-off-by line. Missing: " + ", ".join(failures)) + + print(f"Contribution governance: PASS ({len(commit_shas)} commits, CLA Version 1.0).") + + +if __name__ == "__main__": + main() diff --git a/scripts/verify-release-package.ps1 b/scripts/verify-release-package.ps1 index 6398a04..70c4d69 100644 --- a/scripts/verify-release-package.ps1 +++ b/scripts/verify-release-package.ps1 @@ -1,10 +1,14 @@ +[CmdletBinding()] param( [Parameter(Mandatory=$true)] [string]$PackageZip, - [string]$AppName = "ProcessBusInsight" + [string]$AppName = "ProcessBusInsight", + [string]$ExpectedVersion = "1.4.0-beta.2", + [switch]$RequireAuthenticode ) $ErrorActionPreference = "Stop" +Set-StrictMode -Version Latest if (-not (Test-Path $PackageZip)) { throw "Package ZIP not found: $PackageZip" } $temp = Join-Path ([System.IO.Path]::GetTempPath()) ("ProcessBusInsightVerify_" + [Guid]::NewGuid().ToString("N")) @@ -24,7 +28,10 @@ try { "COPYRIGHT.md", "TRADEMARK.md", "THIRD_PARTY_NOTICES.md", - "Licensing.md" + "Licensing.md", + "Asset Provenance.md", + "SOURCE.md", + "sbom.cdx.json" ) foreach ($relative in $required) { if (-not (Test-Path (Join-Path $temp $relative))) { @@ -50,12 +57,34 @@ try { } $licensing = Get-Content (Join-Path $temp "Licensing.md") -Raw - foreach ($marker in @('GPL-3.0-or-later', '85d43a0fe58a5888a9e8008c168ab76d2333ea87', 'archive/apache-2.0-final')) { + foreach ($marker in @('GPL-3.0-or-later', '85d43a0fe58a5888a9e8008c168ab76d2333ea87', 'archive/apache-2.0-final', 'SOURCE.md', 'sbom.cdx.json')) { if ($licensing -notmatch [regex]::Escape($marker)) { throw "Package verification failed. Licensing.md is missing: $marker" } } + $source = Get-Content (Join-Path $temp "SOURCE.md") -Raw + foreach ($marker in @('Built commit:', 'Source-head commit:', 'Tested merge commit:', 'Immutable source archive', 'GPL-3.0-or-later')) { + if ($source -notmatch [regex]::Escape($marker)) { + throw "Package verification failed. SOURCE.md is missing: $marker" + } + } + if ($source -notmatch 'https://github\.com/masarray/DigSubAnalyzer/archive/[0-9a-f]{40}\.zip') { + throw "Package verification failed. SOURCE.md does not contain an immutable commit archive URL." + } + + $sbom = Get-Content (Join-Path $temp "sbom.cdx.json") -Raw | ConvertFrom-Json -Depth 100 + if ($sbom.bomFormat -ne 'CycloneDX' -or $sbom.specVersion -ne '1.5') { + throw "Package verification failed. sbom.cdx.json is not CycloneDX 1.5." + } + if ($sbom.metadata.component.name -ne 'Process Bus Insight' -or $sbom.metadata.component.version -ne $ExpectedVersion) { + throw "Package verification failed. SBOM product/version mismatch." + } + $sbomLicense = $sbom.metadata.component.licenses[0].license.id + if ($sbomLicense -ne 'GPL-3.0-or-later') { + throw "Package verification failed. SBOM does not identify GPL-3.0-or-later." + } + $batFiles = Get-ChildItem -Path $temp -Recurse -File -Filter "*.bat" if ($batFiles.Count -gt 0) { $names = ($batFiles | ForEach-Object { $_.FullName.Substring($temp.Length + 1) }) -join ', ' @@ -68,15 +97,49 @@ try { throw "Package verification failed. Expected exactly one root EXE: $AppName.exe. Found: $names" } - $size = (Get-Item (Join-Path $temp "$AppName.exe")).Length + $exePath = Join-Path $temp "$AppName.exe" + $size = (Get-Item $exePath).Length if ($size -lt 1024) { throw "Package verification failed. Executable looks too small: $size bytes" } + $versionInfo = (Get-Item $exePath).VersionInfo + if ($versionInfo.ProductVersion -notmatch [regex]::Escape($ExpectedVersion)) { + throw "Package verification failed. EXE ProductVersion '$($versionInfo.ProductVersion)' does not contain $ExpectedVersion." + } + + $signature = Get-AuthenticodeSignature -FilePath $exePath + if ($RequireAuthenticode -and $signature.Status -ne [System.Management.Automation.SignatureStatus]::Valid) { + throw "Package verification failed. A valid Authenticode signature is required; status=$($signature.Status)." + } + Write-Host "Authenticode status: $($signature.Status)" + + $binaryBytes = [System.IO.File]::ReadAllBytes($exePath) + $binaryAscii = [System.Text.Encoding]::ASCII.GetString($binaryBytes) + $binaryUnicode = [System.Text.Encoding]::Unicode.GetString($binaryBytes) + $binaryText = $binaryAscii + "`n" + $binaryUnicode + foreach ($forbidden in @('Source code is licensed under Apache-2.0', 'Text="Apache-2.0"', 'Version 1.0.0', 'Build 2026.04', 'oscilloscope-level')) { + if ($binaryText.IndexOf($forbidden, [StringComparison]::OrdinalIgnoreCase) -ge 0) { + throw "Package verification failed. EXE contains stale public wording: $forbidden" + } + } + foreach ($requiredBinaryMarker in @('GPL-3.0-or-later', 'Separate negotiated and executed agreement')) { + if ($binaryText.IndexOf($requiredBinaryMarker, [StringComparison]::OrdinalIgnoreCase) -lt 0) { + throw "Package verification failed. EXE is missing expected embedded wording: $requiredBinaryMarker" + } + } + foreach ($pdfName in @("Quick Start.pdf", "User Manual.pdf")) { if ((Get-Item (Join-Path $temp $pdfName)).Length -lt 2048) { throw "Package verification failed. PDF document looks too small: $pdfName" } } + $readme = Get-Content (Join-Path $temp "README.txt") -Raw + foreach ($marker in @($ExpectedVersion, 'GPL-3.0-or-later', 'SOURCE.md', 'sbom.cdx.json')) { + if ($readme -notmatch [regex]::Escape($marker)) { + throw "Package verification failed. README.txt is missing: $marker" + } + } + Write-Host "Package verification passed: $PackageZip" Write-Host "Community license: GPL-3.0-or-later" } diff --git a/src/ProcessBus.App.Wpf/Views/AboutWindow.xaml b/src/ProcessBus.App.Wpf/Views/AboutWindow.xaml index e95fbd9..7145407 100644 --- a/src/ProcessBus.App.Wpf/Views/AboutWindow.xaml +++ b/src/ProcessBus.App.Wpf/Views/AboutWindow.xaml @@ -1,493 +1,220 @@ + TextOptions.TextRenderingMode="ClearType"> - - - - - - - + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + - + + CornerRadius="10" + Padding="14" + Margin="0,0,0,10"> - + - - + + - - - + + + - - - + + + - + - + + - - - - - - -Process Bus Analyzer is a field-oriented digital instrumentation platform for IEC 61850 Process Bus analysis. - -It provides real-time visibility of Sampled Values (SV) and GOOSE communication by combining waveform analysis, phasor visualization, protocol inspection, diagnostic indicators, and event logs in one practical engineering interface. - -Designed to complement conventional test equipment, it brings oscilloscope-level visibility into digital substation communication so engineers can move from guessing to evidence-based troubleshooting. - - - - + + - - - - - + + - - -In real substation projects, the most expensive issues are often not caused by one obvious failure, but by small timing, configuration, quality, and communication symptoms that appear late in the test cycle. This tool is designed to surface those symptoms earlier, so engineering decisions can be made with clearer evidence. + + + + - - -Process Bus Insight source code is licensed under Apache-2.0. The analyzer remains receive-only and raw-passive: it does not link to external IEC 61850 subscriber stacks in the WPF product runtime. - - - - - - - - - - - - - - - - - - - - - - - - + + - + - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - -Developed from practical field experience in substation automation, industrial communication, FAT/SAT workflow, and real troubleshooting scenarios. - + + + + + + +