diff --git a/.github/codeql/.gitignore b/.github/codeql/.gitignore new file mode 100644 index 0000000..52f673c --- /dev/null +++ b/.github/codeql/.gitignore @@ -0,0 +1 @@ +out/ diff --git a/.github/codeql/Production.proj b/.github/codeql/Production.proj new file mode 100644 index 0000000..db319be --- /dev/null +++ b/.github/codeql/Production.proj @@ -0,0 +1,28 @@ + + + + $([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..\..')) + + + + + + + + + + + + + + + + + diff --git a/.github/codeql/build.json b/.github/codeql/build.json new file mode 100644 index 0000000..a63eb3b --- /dev/null +++ b/.github/codeql/build.json @@ -0,0 +1,5 @@ +{ + "productionSourceRoot": "package\\src", + "requiresSqlite": true, + "requiresPowerShell": false +} diff --git a/.github/scripts/Build-CodeQLProduction.ps1 b/.github/scripts/Build-CodeQLProduction.ps1 new file mode 100644 index 0000000..a7f9936 --- /dev/null +++ b/.github/scripts/Build-CodeQLProduction.ps1 @@ -0,0 +1,90 @@ +#Requires -Version 7.4 +[CmdletBinding()] +param([string]$OutputDirectory) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' +$root = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..\..')) +$configuration = Get-Content (Join-Path $root '.github\codeql\build.json') -Raw | ConvertFrom-Json +$sourceRoot = [IO.Path]::GetFullPath((Join-Path $root $configuration.productionSourceRoot)) +if ($sourceRoot -notin @((Join-Path $root 'package\src'), (Join-Path $root 'src'))) { + throw 'Only the production src directory is supported.' +} +if (-not $OutputDirectory) { $OutputDirectory = Join-Path $root '.github\codeql\out' } +$OutputDirectory = [IO.Path]::GetFullPath($OutputDirectory) +if ($OutputDirectory.StartsWith($root + '\', [StringComparison]::OrdinalIgnoreCase) -and + -not $OutputDirectory.StartsWith((Join-Path $root '.github\codeql\out') + '\', [StringComparison]::OrdinalIgnoreCase) -and + $OutputDirectory -ne (Join-Path $root '.github\codeql\out')) { + throw 'Analysis outputs must not enter the runtime package.' +} +[void][IO.Directory]::CreateDirectory($OutputDirectory) +$sources = @(Get-ChildItem $sourceRoot -Recurse -File -Filter '*.cs' | Sort-Object FullName) +if (-not $sources.Count) { throw 'No production C# sources found.' } +$tracked = @(& git -C $root ls-files -- '*.cs') +if ($LASTEXITCODE) { throw 'Cannot inspect the tracked C# inventory.' } +$runtimeTracked = @($tracked | Where-Object { $_ -notmatch '^(tests|tools|\.github|lib)/' -and $_ -notmatch '^package/lib/' }) +foreach ($relative in $runtimeTracked) { + if ([IO.Path]::GetFullPath((Join-Path $root $relative)) -notin $sources.FullName) { + throw "Production C# source omitted by the build: $relative" + } +} +$references = @(Get-ChildItem (Join-Path $PSHOME 'ref') -File -Filter '*.dll' | Sort-Object Name) +if (-not $references.Count) { throw 'This PowerShell installation has no reference assemblies.' } +if ($configuration.requiresPowerShell) { + $references += Get-Item (Join-Path $PSHOME 'System.Management.Automation.dll') +} +if ($configuration.requiresSqlite) { + foreach ($name in 'Microsoft.Data.Sqlite', 'SQLitePCLRaw.core', 'SQLitePCLRaw.provider.e_sqlite3', 'SQLitePCLRaw.batteries_v2') { + $references += Get-Item (Join-Path $root "package\lib\sqlite\$name.dll") + } +} +function Get-FileEvidence($File) { + [ordered]@{ + file = $File.FullName.Substring($root.Length + 1) + sha256 = (Get-FileHash $File.FullName -Algorithm SHA256).Hash.ToLowerInvariant() + } +} +$sourceEvidence = @($sources | ForEach-Object { Get-FileEvidence $_ }) +$referenceEvidence = @($references | ForEach-Object { + $assembly = [Reflection.AssemblyName]::GetAssemblyName($_.FullName) + [ordered]@{ + name = $assembly.Name; version = $assembly.Version.ToString() + path = $_.FullName; sha256 = (Get-FileHash $_.FullName -Algorithm SHA256).Hash.ToLowerInvariant() + } +}) +$sdk = (& dotnet --version).Trim() +if ($LASTEXITCODE) { throw 'A .NET SDK is required for the existing Roslyn compiler task.' } +$manifest = [ordered]@{ + purpose = 'Analysis-only compilation; no runtime loading, restore, deployment or package changes.' + powerShell = $PSVersionTable.PSVersion.ToString() + framework = [Runtime.InteropServices.RuntimeInformation]::FrameworkDescription + sdk = $sdk; sources = $sourceEvidence; references = $referenceEvidence + compiledCount = 0; extractionProof = 'PENDING hosted CodeQL tracer and database validation.' +} +$manifest | ConvertTo-Json -Depth 8 | Set-Content (Join-Path $OutputDirectory 'build-manifest.json') -Encoding utf8 +$arguments = @( + 'msbuild', (Join-Path $root '.github\codeql\Production.proj'), '-nologo', '-verbosity:minimal', + "-property:ProductionSourceRoot=$($configuration.productionSourceRoot)", + "-property:PowerShellHome=$PSHOME", + "-property:RequiresPowerShell=$($configuration.requiresPowerShell.ToString().ToLowerInvariant())", + "-property:RequiresSqlite=$($configuration.requiresSqlite.ToString().ToLowerInvariant())", + "-property:OutputDirectory=$OutputDirectory" +) +& dotnet @arguments +if ($LASTEXITCODE) { throw "Analysis-only compilation failed with exit code $LASTEXITCODE." } +$compiled = @(Get-Content (Join-Path $OutputDirectory 'compiled-sources.txt') | Where-Object { $_ }) +if (@(Compare-Object $sources.FullName $compiled).Count -ne 0) { + throw 'Compiler source inventory does not equal the production inventory.' +} +$compiledReferences = @(Get-Content (Join-Path $OutputDirectory 'compiler-references.txt') | Where-Object { $_ }) +if (@(Compare-Object $references.FullName $compiledReferences).Count -ne 0) { + throw 'Compiler references do not equal the runtime reference inventory.' +} +$after = @($sources | ForEach-Object { Get-FileEvidence $_ }) +if (($sourceEvidence | ConvertTo-Json -Compress) -ne ($after | ConvertTo-Json -Compress)) { + throw 'Production source changed during analysis compilation.' +} +$manifest.compiledCount = $compiled.Count +$manifest['assemblySha256'] = (Get-FileHash (Join-Path $OutputDirectory 'Production.Analysis.dll')).Hash.ToLowerInvariant() +$manifest | ConvertTo-Json -Depth 8 | Set-Content (Join-Path $OutputDirectory 'build-manifest.json') -Encoding utf8 +Write-Output "Compiled $($compiled.Count) production C# files against exact runtime references; hosted extraction proof remains pending." diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 063cffe..2b6be33 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -1,89 +1,107 @@ -{ - "name": "CodeQL (advisory)", - "on": { - "pull_request": { - "branches": ["main"] - }, - "push": { - "branches": ["main"] - }, - "schedule": [ - { - "cron": "41 5 * * 3" - } - ], - "workflow_dispatch": {} - }, - "permissions": {}, - "concurrency": { - "group": "codeql-${{ github.workflow }}-${{ github.ref }}", - "cancel-in-progress": true - }, - "jobs": { - "analyze": { - "name": "CodeQL ${{ matrix.language }} (advisory)", - "if": "${{ contains(fromJSON('[\"Nico77600/ExchangeLogReport\",\"Nico77600/MailboxMessageReport\",\"Nico77600/MeetingCleanup\",\"Nico77600/MeetingCleanupOnPrem\",\"Nico77600/MessageTraceReport\",\"Nico77600/PurviewDlpReport\",\"Nico77600/PurviewDlpReport-Fabric\",\"Nico77600/RecipientLimitReport\"]'), github.repository) && (github.event_name == 'pull_request' || github.ref == 'refs/heads/main') }}", - "runs-on": "${{ matrix.runner }}", - "timeout-minutes": 25, - "permissions": { - "contents": "read", - "security-events": "write" - }, - "strategy": { - "fail-fast": false, - "max-parallel": 2, - "matrix": { - "include": [ - { - "language": "actions", - "runner": "ubuntu-latest", - "config": { - "name": "GitHub Actions workflows", - "paths": [".github/workflows"] - } - }, - { - "language": "csharp", - "runner": "windows-2022", - "config": { - "name": "Production C# loose-source pilot", - "paths": ["package/src"], - "paths-ignore": ["tests", "package/lib", "tools"] - } - } - ] - } - }, - "steps": [ - { - "name": "Check out event revision", - "uses": "actions/checkout@11d5960a326750d5838078e36cf38b85af677262", - "with": { - "persist-credentials": false - } - }, - { - "name": "Initialize source-only analysis", - "uses": "github/codeql-action/init@24c54180a607b1449ed407dd24f251e4e9147c8d", - "with": { - "languages": "${{ matrix.language }}", - "build-mode": "none", - "threads": 2, - "dependency-caching": "false", - "config": "${{ toJson(matrix.config) }}" - } - }, - { - "name": "Analyze and upload advisory results", - "uses": "github/codeql-action/analyze@24c54180a607b1449ed407dd24f251e4e9147c8d", - "with": { - "category": "/language:${{ matrix.language }}", - "upload": "always", - "upload-database": "false", - "wait-for-processing": "true" - } - } - ] - } - } -} +{ + "name": "CodeQL (advisory)", + "on": { + "pull_request": { + "branches": ["main"] + }, + "push": { + "branches": ["main"] + }, + "schedule": [ + { + "cron": "41 5 * * 3" + } + ], + "workflow_dispatch": {} + }, + "permissions": {}, + "concurrency": { + "group": "codeql-${{ github.workflow }}-${{ github.ref }}", + "cancel-in-progress": true + }, + "jobs": { + "analyze": { + "name": "CodeQL ${{ matrix.language }} (advisory)", + "if": "${{ contains(fromJSON('[\"Nico77600/ExchangeLogReport\",\"Nico77600/MailboxMessageReport\",\"Nico77600/MeetingCleanup\",\"Nico77600/MeetingCleanupOnPrem\",\"Nico77600/MessageTraceReport\",\"Nico77600/PurviewDlpReport\",\"Nico77600/PurviewDlpReport-Fabric\",\"Nico77600/RecipientLimitReport\"]'), github.repository) && (github.event_name == 'pull_request' || github.ref == 'refs/heads/main') }}", + "runs-on": "${{ matrix.runner }}", + "timeout-minutes": 25, + "permissions": { + "contents": "read", + "security-events": "write" + }, + "strategy": { + "fail-fast": false, + "max-parallel": 2, + "matrix": { + "include": [ + { + "language": "actions", + "build-mode": "none", + "runner": "ubuntu-latest", + "config": { + "name": "GitHub Actions workflows", + "paths": [".github/workflows"] + } + }, + { + "language": "csharp", + "build-mode": "manual", + "runner": "windows-2022", + "config": { + "name": "Production C# with exact runtime references" + } + } + ] + } + }, + "steps": [ + { + "name": "Check out event revision", + "uses": "actions/checkout@11d5960a326750d5838078e36cf38b85af677262", + "with": { + "persist-credentials": false + } + }, + { + "name": "Initialize language-specific analysis", + "uses": "github/codeql-action/init@24c54180a607b1449ed407dd24f251e4e9147c8d", + "with": { + "languages": "${{ matrix.language }}", + "build-mode": "${{ matrix.build-mode }}", + "threads": 2, + "dependency-caching": "false", + "config": "${{ toJson(matrix.config) }}" + } + }, + { + "name": "Compile all production C# without runtime execution", + "if": "${{ matrix.language == 'csharp' }}", + "shell": "pwsh", + "run": "& .\\.github\\scripts\\Build-CodeQLProduction.ps1" + }, + { + "name": "Analyze and upload advisory results", + "uses": "github/codeql-action/analyze@24c54180a607b1449ed407dd24f251e4e9147c8d", + "with": { + "category": "/language:${{ matrix.language }}", + "upload": "always", + "upload-database": "false", + "wait-for-processing": "true" + } + }, + { + "name": "Preserve C# compilation inventory", + "if": "${{ always() && matrix.language == 'csharp' }}", + "uses": "actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02", + "with": { + "name": "codeql-production-build", + "path": ".github/codeql/out/build-manifest.json\n.github/codeql/out/compiled-sources.txt\n.github/codeql/out/compiler-references.txt", + "if-no-files-found": "warn", + "include-hidden-files": true, + "retention-days": 14 + } + } + ] + } + } +} diff --git a/AGENTS.md b/AGENTS.md index fd54d95..dfc5623 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,46 +1,48 @@ -# Development and publication - -## Development - -- Preserve existing test, packaging, installation, and local deployment procedures. -- Run the project's existing validation commands before promoting a change. -- Keep private configuration, credentials, customer data, and backup files out of commits and release packages. -- Commits carry only the repository owner's identity. Do not add co-author trailers for assistants or tools. -- Preview public changes and obtain the repository owner's approval before publishing. - -## Changes to main - -1. Fetch public main and prepare a short-lived branch from the current public history. -2. Include only the intended changes; do not import private development history. -3. Push the branch, open a pull request targeting main, and review the diff and available checks. -4. Merge the pull request. The initial solo-maintainer baseline requires no second-person approval. -5. Refresh the public clone after merging without discarding unrelated local changes. - -Direct pushes, force pushes, and deletion of public main are prohibited. Do not bypass or weaken repository rules. New required CI checks and Git commit-signing requirements must be established and approved before they become blocking. - -## Releases - -1. Use a new version and create its v-prefixed tag on the intended commit already merged into public main. -2. Never move, delete, or reuse an existing version tag. -3. Use the existing packaging procedure on the reviewed release source. Complete any configured signing before packaging. -4. Create a draft release and upload all intended assets before publication. Check the tag, commit, version, asset inventory, and hashes. -5. Publish the complete draft. Published assets are immutable; a correction requires a new version. -6. If configured, confirm the attestation workflow succeeds for the distributed files. A source-only fallback does not attest a missing release ZIP. - -## Security and recovery - -Keep secret scanning and push protection enabled. Do not routinely bypass alerts. Dependency scanning is not a replacement for the project's tests and analysis. Follow SECURITY.md for sensitive reports. Maintain versioned backups of public Git history, release assets, and settings outside the repository; keep an additional copy off the development machine. - -For a new public repository, configure these branch/tag protections and future-release immutability before the first release. Enable dependency graph, Dependabot alerts, secret scanning, push protection, and private vulnerability reporting. Personal-account repositories do not automatically inherit these settings. Automatic merges and automated dependency-update pull requests are not part of this baseline. - -## CI and provenance - -GitHub Actions runs the existing tests and package builder on a disposable Windows runner using the project's configured PowerShell edition. It does not connect to the development lab or use publication credentials. CI helpers live under .github/scripts and are not part of the runtime package. - -PowerShell syntax and analyzer execution must succeed. PSScriptAnalyzer diagnostics and CodeQL findings are initially advisory, not automatically fixed. CodeQL analyzes workflows and, where configured, C# sources; it does not analyze PowerShell. An unsuccessful scan is not evidence of security or complete coverage. - -Required validation checks are enabled only after successful GitHub runs. Do not bypass them or weaken existing tests to publish. If a check fails, inspect its evidence and address the cause. - -The release attestation workflow does not build or execute repository code. It attests the actual published ZIP bytes; it does not prove their original build. Manual attestation must run from the default branch with an explicit existing release tag. Missing releases or ZIPs fail explicitly; no source archive is substituted. - -Actions are pinned to full commit IDs. Updates require review of the action source and an explicit pin change. Repository policies may reject unpinned or unapproved actions; never relax them to merge a contribution. +# Development and publication + +## Development + +- Preserve existing test, packaging, installation, and local deployment procedures. +- Run the project's existing validation commands before promoting a change. +- Keep private configuration, credentials, customer data, and backup files out of commits and release packages. +- Commits carry only the repository owner's identity. Do not add co-author trailers for assistants or tools. +- Preview public changes and obtain the repository owner's approval before publishing. + +## Changes to main + +1. Fetch public main and prepare a short-lived branch from the current public history. +2. Include only the intended changes; do not import private development history. +3. Push the branch, open a pull request targeting main, and review the diff and available checks. +4. Merge the pull request. The initial solo-maintainer baseline requires no second-person approval. +5. Refresh the public clone after merging without discarding unrelated local changes. + +Direct pushes, force pushes, and deletion of public main are prohibited. Do not bypass or weaken repository rules. New required CI checks and Git commit-signing requirements must be established and approved before they become blocking. + +## Releases + +1. Use a new version and create its v-prefixed tag on the intended commit already merged into public main. +2. Never move, delete, or reuse an existing version tag. +3. Use the existing packaging procedure on the reviewed release source. Complete any configured signing before packaging. +4. Create a draft release and upload all intended assets before publication. Check the tag, commit, version, asset inventory, and hashes. +5. Publish the complete draft. Published assets are immutable; a correction requires a new version. +6. If configured, confirm the attestation workflow succeeds for the distributed files. A source-only fallback does not attest a missing release ZIP. + +## Security and recovery + +Keep secret scanning and push protection enabled. Do not routinely bypass alerts. Dependency scanning is not a replacement for the project's tests and analysis. Follow SECURITY.md for sensitive reports. Maintain versioned backups of public Git history, release assets, and settings outside the repository; keep an additional copy off the development machine. + +For a new public repository, configure these branch/tag protections and future-release immutability before the first release. Enable dependency graph, Dependabot alerts, secret scanning, push protection, and private vulnerability reporting. Personal-account repositories do not automatically inherit these settings. Automatic merges and automated dependency-update pull requests are not part of this baseline. + +## CI and provenance + +GitHub Actions runs the existing tests and package builder on a disposable Windows runner using the project's configured PowerShell edition. It does not connect to the development lab or use publication credentials. CI helpers live under .github/scripts and are not part of the runtime package. + +PowerShell syntax and analyzer execution must succeed. PSScriptAnalyzer diagnostics and CodeQL findings are initially advisory, not automatically fixed. CodeQL analyzes workflows and, where configured, C# sources; it does not analyze PowerShell. An unsuccessful scan is not evidence of security or complete coverage. + +Required validation checks are enabled only after successful GitHub runs. Do not bypass them or weaken existing tests to publish. If a check fails, inspect its evidence and address the cause. + +The release attestation workflow does not build or execute repository code. It attests the actual published ZIP bytes; it does not prove their original build. Manual attestation must run from the default branch with an explicit existing release tag. Missing releases or ZIPs fail explicitly; no source archive is substituted. + +Actions are pinned to full commit IDs. Updates require review of the action source and an explicit pin change. Repository policies may reject unpinned or unapproved actions; never relax them to merge a contribution. + +The advisory C# job uses manual compilation through `.github\scripts\Build-CodeQLProduction.ps1` and `.github\codeql\Production.proj`. It compiles every production `package\src\*.cs` recursively against the running PowerShell framework references and the exact shipped SQLite assemblies, without NuGet restore or executing/loading the tool. Outputs stay outside the runtime package; do not distribute them. The build manifest checks source/reference inventories, but only the hosted CodeQL tracer/database diagnostics can establish complete extraction. CodeQL does not analyze PowerShell.