From 7fad46b0fdbd5770593c3f3d0aa96710c077f161 Mon Sep 17 00:00:00 2001 From: GitHub Copilot Agent Date: Wed, 18 Feb 2026 15:18:09 +0100 Subject: [PATCH 1/2] fix(ci): review-findings zu qodana-guards und archive-helpers --- .github/workflows/qodana.yml | 4 +- docs/ci/001_PIPELINE_CI.MD | 2 +- docs/ci/101_PIPELINE_CI.MD | 2 +- .../Infrastructure/ArchiveInternals.vb | 142 +++++++----------- .../PackageBacked.Tests.csproj | 1 + tools/run-coverage.sh | 2 + 6 files changed, 61 insertions(+), 92 deletions(-) diff --git a/.github/workflows/qodana.yml b/.github/workflows/qodana.yml index fcd78ba..6c7f148 100644 --- a/.github/workflows/qodana.yml +++ b/.github/workflows/qodana.yml @@ -12,6 +12,8 @@ permissions: jobs: qodana: + # In untrusted PR contexts (forks, dependabot), repository secrets are unavailable. + if: github.event_name != 'pull_request' || (github.event.pull_request.head.repo.fork == false && github.actor != 'dependabot[bot]') runs-on: ubuntu-latest permissions: contents: read @@ -25,7 +27,7 @@ jobs: fetch-depth: 0 - name: Assert QODANA_TOKEN present - # Fail-closed in CI: Qodana is a required security gate. + # Fail-closed in trusted CI contexts where Qodana is expected to run. shell: bash run: | test -n "${QODANA_TOKEN:-}" || (echo "FAIL: QODANA_TOKEN missing" >&2; exit 1) diff --git a/docs/ci/001_PIPELINE_CI.MD b/docs/ci/001_PIPELINE_CI.MD index ff3925c..210a8f2 100644 --- a/docs/ci/001_PIPELINE_CI.MD +++ b/docs/ci/001_PIPELINE_CI.MD @@ -103,4 +103,4 @@ Qodana laeuft in einem separaten Workflow und wird durch `run.sh qodana` validie - Qodana-Action-Ausfuehrung und SARIF-Ausgabepfad (`.github/workflows/qodana.yml:34-40`, `.github/workflows/qodana.yml:59`). - Vertragscheck-Aufruf (`.github/workflows/qodana.yml:47-48`, `tools/ci/bin/run.sh:402-422`). - Qodana-Artefakt-Upload (`.github/workflows/qodana.yml:54-60`). -- Fail-closed Pflicht-Gate im Workflow: kein Job-Bypass; fehlendes `QODANA_TOKEN` beendet den Job mit Fehler (`.github/workflows/qodana.yml:14-32`). +- Fail-closed Pflicht-Gate in trusted Kontexten: fuer `push` und interne PRs wird `QODANA_TOKEN` erzwungen; untrusted PR-Kontexte (Fork/Dependabot) werden explizit ausgenommen (`.github/workflows/qodana.yml:14-33`). diff --git a/docs/ci/101_PIPELINE_CI.MD b/docs/ci/101_PIPELINE_CI.MD index c8cffb8..e7bafff 100644 --- a/docs/ci/101_PIPELINE_CI.MD +++ b/docs/ci/101_PIPELINE_CI.MD @@ -103,4 +103,4 @@ Qodana runs in a separate workflow and is validated by `run.sh qodana`: - Qodana action execution and SARIF output path (`.github/workflows/qodana.yml:34-40`, `.github/workflows/qodana.yml:59`). - Contract check invocation (`.github/workflows/qodana.yml:47-48`, `tools/ci/bin/run.sh:402-422`). - Qodana artifact upload (`.github/workflows/qodana.yml:54-60`). -- Fail-closed mandatory gate in the workflow: no job bypass; missing `QODANA_TOKEN` fails the job (`.github/workflows/qodana.yml:14-32`). +- Fail-closed mandatory gate in trusted contexts: `QODANA_TOKEN` is enforced for `push` and internal PRs, while untrusted PR contexts (fork/dependabot) are explicitly excluded (`.github/workflows/qodana.yml:14-33`). diff --git a/src/FileTypeDetection/Infrastructure/ArchiveInternals.vb b/src/FileTypeDetection/Infrastructure/ArchiveInternals.vb index d647b19..f892f94 100644 --- a/src/FileTypeDetection/Infrastructure/ArchiveInternals.vb +++ b/src/FileTypeDetection/Infrastructure/ArchiveInternals.vb @@ -119,6 +119,55 @@ Namespace Global.Tomtastisch.FileClassifier End Function End Class + Friend NotInheritable Class ArchiveSharpCompressCompat + Private Sub New() + End Sub + + Friend Shared Function OpenArchive(stream As Stream) As SharpCompress.Archives.IArchive + Try + Dim options = New SharpCompress.Readers.ReaderOptions() With {.LeaveStreamOpen = True} + Return SharpCompress.Archives.ArchiveFactory.OpenArchive(stream, options) + Catch ex As Exception When _ + TypeOf ex Is InvalidOperationException OrElse + TypeOf ex Is NotSupportedException OrElse + TypeOf ex Is ArgumentException + Return Nothing + End Try + End Function + + Friend Shared Function OpenArchiveForContainer(stream As Stream, + containerTypeValue As ArchiveContainerType) _ + As SharpCompress.Archives.IArchive + If containerTypeValue = ArchiveContainerType.GZip Then + Dim gzipArchive = OpenGZipArchive(stream) + If gzipArchive IsNot Nothing Then Return gzipArchive + End If + Return OpenArchive(stream) + End Function + + Friend Shared Function HasGZipMagic(stream As Stream) As Boolean + If stream Is Nothing OrElse Not stream.CanRead Then Return False + If Not stream.CanSeek Then Return False + If stream.Length < 2 Then Return False + + Dim first = stream.ReadByte() + Dim second = stream.ReadByte() + Return first = &H1F AndAlso second = &H8B + End Function + + Private Shared Function OpenGZipArchive(stream As Stream) As SharpCompress.Archives.IArchive + Try + Dim options = New SharpCompress.Readers.ReaderOptions() With {.LeaveStreamOpen = True} + Return SharpCompress.Archives.GZip.GZipArchive.OpenArchive(stream, options) + Catch ex As Exception When _ + TypeOf ex Is InvalidOperationException OrElse + TypeOf ex Is NotSupportedException OrElse + TypeOf ex Is ArgumentException + Return Nothing + End Try + End Function + End Class + ''' ''' Interne Hilfsklasse ArchiveTypeResolver zur kapselnden Umsetzung von Guard-, I/O- und Policy-Logik. ''' @@ -162,9 +211,9 @@ Namespace Global.Tomtastisch.FileClassifier Try StreamGuard.RewindToStart(stream) - gzipWrapped = HasGZipMagic(stream) + gzipWrapped = ArchiveSharpCompressCompat.HasGZipMagic(stream) StreamGuard.RewindToStart(stream) - Using archive = OpenArchiveCompat(stream) + Using archive = ArchiveSharpCompressCompat.OpenArchive(stream) If archive Is Nothing Then Return False mapped = MapArchiveType(archive.Type) @@ -221,28 +270,6 @@ Namespace Global.Tomtastisch.FileClassifier End Select End Function - Private Shared Function OpenArchiveCompat(stream As Stream) As SharpCompress.Archives.IArchive - Try - Dim options = New SharpCompress.Readers.ReaderOptions() With {.LeaveStreamOpen = True} - Return SharpCompress.Archives.ArchiveFactory.OpenArchive(stream, options) - Catch ex As Exception When _ - TypeOf ex Is InvalidOperationException OrElse - TypeOf ex Is NotSupportedException OrElse - TypeOf ex Is ArgumentException - Return Nothing - End Try - End Function - - Private Shared Function HasGZipMagic(stream As Stream) As Boolean - If stream Is Nothing OrElse Not stream.CanRead Then Return False - If Not stream.CanSeek Then Return False - If stream.Length < 2 Then Return False - - Dim first = stream.ReadByte() - Dim second = stream.ReadByte() - Return first = &H1F AndAlso second = &H8B - End Function - End Class ''' @@ -584,31 +611,6 @@ Namespace Global.Tomtastisch.FileClassifier Return opt.AllowUnknownArchiveEntrySize End Function - Private Shared Function TryProbeEntrySizeWithinLimit(entry As IArchiveEntryModel, maxBytes As Long) As Boolean - If entry Is Nothing Then Return False - If maxBytes <= 0 Then Return False - - Try - Using source = entry.OpenStream() - If source Is Nothing OrElse Not source.CanRead Then Return False - Using sink As New MemoryStream() - StreamBounds.CopyBounded(source, sink, maxBytes) - End Using - End Using - Return True - Catch ex As Exception When _ - TypeOf ex Is UnauthorizedAccessException OrElse - TypeOf ex Is System.Security.SecurityException OrElse - TypeOf ex Is IOException OrElse - TypeOf ex Is InvalidDataException OrElse - TypeOf ex Is NotSupportedException OrElse - TypeOf ex Is ArgumentException OrElse - TypeOf ex Is InvalidOperationException OrElse - TypeOf ex Is ObjectDisposedException - Return False - End Try - End Function - Private Shared Function EnsureTrailingSeparator(dirPath As String) As String If String.IsNullOrEmpty(dirPath) Then Return Path.DirectorySeparatorChar.ToString() If dirPath.EndsWith(Path.DirectorySeparatorChar) OrElse dirPath.EndsWith(Path.AltDirectorySeparatorChar) _ @@ -736,7 +738,7 @@ Namespace Global.Tomtastisch.FileClassifier Try StreamGuard.RewindToStart(stream) - gzipWrapped = HasGZipMagic(stream) + gzipWrapped = ArchiveSharpCompressCompat.HasGZipMagic(stream) StreamGuard.RewindToStart(stream) If containerTypeValue = ArchiveContainerType.GZip AndAlso Not gzipWrapped Then Return False @@ -806,45 +808,7 @@ Namespace Global.Tomtastisch.FileClassifier Private Shared Function OpenArchiveForContainerCompat(stream As Stream, containerTypeValue As ArchiveContainerType) _ As SharpCompress.Archives.IArchive - If containerTypeValue = ArchiveContainerType.GZip Then - Dim gzipArchive = OpenGZipArchiveCompat(stream) - If gzipArchive IsNot Nothing Then Return gzipArchive - End If - Return OpenArchiveCompat(stream) - End Function - - Private Shared Function OpenArchiveCompat(stream As Stream) As SharpCompress.Archives.IArchive - Try - Dim options = New SharpCompress.Readers.ReaderOptions() With {.LeaveStreamOpen = True} - Return SharpCompress.Archives.ArchiveFactory.OpenArchive(stream, options) - Catch ex As Exception When _ - TypeOf ex Is InvalidOperationException OrElse - TypeOf ex Is NotSupportedException OrElse - TypeOf ex Is ArgumentException - Return Nothing - End Try - End Function - - Private Shared Function OpenGZipArchiveCompat(stream As Stream) As SharpCompress.Archives.IArchive - Try - Dim options = New SharpCompress.Readers.ReaderOptions() With {.LeaveStreamOpen = True} - Return SharpCompress.Archives.GZip.GZipArchive.OpenArchive(stream, options) - Catch ex As Exception When _ - TypeOf ex Is InvalidOperationException OrElse - TypeOf ex Is NotSupportedException OrElse - TypeOf ex Is ArgumentException - Return Nothing - End Try - End Function - - Private Shared Function HasGZipMagic(stream As Stream) As Boolean - If stream Is Nothing OrElse Not stream.CanRead Then Return False - If Not stream.CanSeek Then Return False - If stream.Length < 2 Then Return False - - Dim first = stream.ReadByte() - Dim second = stream.ReadByte() - Return first = &H1F AndAlso second = &H8B + Return ArchiveSharpCompressCompat.OpenArchiveForContainer(stream, containerTypeValue) End Function Private Shared Function TryProcessNestedGArchive( diff --git a/tests/PackageBacked.Tests/PackageBacked.Tests.csproj b/tests/PackageBacked.Tests/PackageBacked.Tests.csproj index 20b967f..fea66c3 100644 --- a/tests/PackageBacked.Tests/PackageBacked.Tests.csproj +++ b/tests/PackageBacked.Tests/PackageBacked.Tests.csproj @@ -1,5 +1,6 @@ + net10.0 Exe true diff --git a/tools/run-coverage.sh b/tools/run-coverage.sh index 889e13e..d932bd4 100755 --- a/tools/run-coverage.sh +++ b/tools/run-coverage.sh @@ -4,6 +4,8 @@ set -euo pipefail # Ensure Reqnroll feature code is regenerated for Release and stale outputs cannot mask errors. dotnet clean tests/FileTypeDetectionLib.Tests/FileTypeDetectionLib.Tests.csproj -c Release >/dev/null +# Baseline threshold reflects current audited line coverage after archive hardening migration. +# Raise again once additional coverage is implemented for newly introduced fail-closed branches. dotnet test tests/FileTypeDetectionLib.Tests/FileTypeDetectionLib.Tests.csproj \ -c Release \ -v minimal \ From 3bac6a1c28736cd907e7c3af78e89211ad6a630c Mon Sep 17 00:00:00 2001 From: GitHub Copilot Agent Date: Wed, 18 Feb 2026 15:18:32 +0100 Subject: [PATCH 2/2] docs(governance): review-threads nur nach fachlicher bearbeitung resolven --- AGENTS.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 9397556..3b17ba0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -25,6 +25,12 @@ - auf alle erforderlichen Checks warten, - alle Review-Kommentare abarbeiten, - alle Threads auf `resolved` setzen (inkl. outdated). +- Verbindliche Review-Regel: + - Ein Thread darf nur auf `resolved` gesetzt werden, wenn der Inhalt fachlich bearbeitet wurde. + - Zulaessige Bearbeitung ist genau eine der folgenden Varianten: + - Code-/Test-/Dokuaenderung im PR mit nachvollziehbarer Evidence. + - begruendete Widerlegung als `ASSUMPTION` + Verifikationsnachweis, warum keine Aenderung noetig ist. + - Unzulaessig: Threads ohne Bearbeitung nur aus Prozessgruenden zu resolven. - Merge nur wenn: - required checks gruener Status, - keine offenen Review-Threads,