chore(release): document hardening and verify complete source package composition

This commit is contained in:
2026-09-08 02:06:35 +02:00
parent 6a8f53b87d
commit 32fe4b7238
9 changed files with 585 additions and 13 deletions
@@ -86,6 +86,16 @@ contract. The coordinated release synchronizes `peerDependencies` and
tag, then synchronizes each lockfile root from the final package metadata. A
distinct root package remains independent.
Every module referenced by Core's Git-based `package.release.json` must expose
its WebUI identity at the repository root, including matching peer requirements
and `webui/`-prefixed entry exports (also CSS subpaths). npm resolves Git
dependencies from the repository root, while the native-package workflow packs
`webui/`; success in one path does not verify the other. Run
`python tools/checks/check-webui-package-facades.py` after changing either
manifest or the release composition. The focused gate also runs this check.
Adding or correcting a facade in an already published repository requires a
new patch tag; never repair an existing immutable tag in place.
It builds one wheel and, where applicable, one npm tarball. The workflow records
the source tag, source commit, filename, size, and SHA-256 in
`package-artifacts.json` before publishing. Gitea rejects a second upload of the
+80
View File
@@ -0,0 +1,80 @@
# GovOPlaN 0.1.45 — usability, reliability and security hardening
Release coordination: [GovOPlaN #51](https://git.add-ideas.de/GovOPlaN/govoplan/issues/51).
The exact independently versioned composition is recorded in
`packages/govoplan-meta/pyproject.toml`; unchanged modules retain their versions.
This source release does not by itself establish a deployed or independently
approved production environment. Package, signed catalog and runtime publication
results are recorded separately in the coordination issue.
## Runtime publication hold
The [runtime image audit](../security/RUNTIME_IMAGE_AUDIT_2026-09-08.md) completed
eleven registry-only amd64 scans, but found unresolved vulnerabilities and
inventory gaps. Runtime publication remains held separately from this source
release. Patch-only image updates are insufficient; maintained minor-line
changes, narrowly evidenced finding decisions, arm64/final-layer scans and
deployment checks remain necessary. No audited candidate was automatically
adopted and no image was executed during those scans.
The remaining gates are tracked in
[GovOPlaN #52](https://git.add-ideas.de/GovOPlaN/govoplan/issues/52).
## Included changes
- Shared page/action placement, reusable navigation grouping/editing, table and
dialog sizing, field alignment, multi-select filters and predictable tree
selection. Files, Mail, Search, Notifications and domain pages use the same
contracts, with browser regression coverage.
- Campaign draft saving and independent Mail/ZIP-policy repair, persistent and
bulk message review, clearer delivery eligibility, bounded configurable
synchronous delivery, guarded workerless recovery, lightweight SMTP/IMAP
progress, reused IMAP connections and recipient-complete reporting.
- Files archive staging/reuse, unpacking previously uploaded archives, numeric
progress and bounded traversal. Optional native archive acceleration retains
the same validation rules; portable fallbacks remain available.
- Mail credential references and IMAP folder-name decoding; help topics can be
found by area and tags without expanding every occurrence of the same topic.
- Authentication provenance/scope and browser-cache hardening, patched rich-text
dependencies, spreadsheet/archive/template/Dataflow resource limits, batched
Docs/Notifications queries and safe Reporting bind names. See the
[security/performance review](../security/SECURITY_PERFORMANCE_REVIEW_2026-09-08.md)
for measurements, test evidence and remaining limitations.
- A deterministic governance-journey clock fixture, fresh-process Campaign
import coverage, and a new Cases patch aligning its root npm facade with its
Python/WebUI package. Historical published tags are not rewritten.
- Git-root WebUI package facades are aligned with their owning packages, with
a cross-composition parity check. Tasks is included in default module
discovery; it remains subject to enabled modules and normal permissions.
## Upgrade and verification
Back up the database and file storage before upgrading. Apply the complete
selected migration graph before starting the new API/workers. This release
includes additive repair migrations `c58a2d7e9f10` (Core ownership history) and
`d8f1b4e7a0c3` (Access external-function mappings), plus Campaign delivery-state
migrations. Existing business evidence is retained; a schema downgrade is not
a substitute for a reviewed backup/restore plan. Restart API and worker
processes together after upgrading their matching packages.
Updated UI consumers require Core 0.1.45 where they use its new shared contracts.
Tenant keys that previously relied on unintended system permissions/wildcards
must be corrected; the release does not preserve that unsafe behavior. Extremely
sparse spreadsheets, oversized generated output and excessive archive paths
can now fail early with a diagnostic.
For archive staging across multiple hosts, provide shared POSIX storage with
working locks or sticky routing. Background delivery still needs configured
workers; increasing the synchronous limit does not create a worker or guarantee
delivery after a process failure. An unknown SMTP outcome must be reconciled,
not automatically resent.
After deployment, manually verify login/logout and least-privilege API keys,
Campaign Settings and independent Mail/ZIP saves, archive upload/unpack,
recipient-complete reports, and SMTP/IMAP progress with an explicitly approved
test mailbox. No release verification sends real campaign mail automatically.
Hard process isolation, forced-password-change/recovery enforcement, bounded
Xrechnung subprocess output and large-history pagination remain separate open
issues. This release is not a claim that all security or performance debt is
resolved. Production-image scans and multi-host evidence must refer to the
actual signed runtime being deployed.
@@ -0,0 +1,125 @@
# Runtime image candidate audit — 8 September 2026
Release coordination: [GovOPlaN #51](https://git.add-ideas.de/GovOPlaN/govoplan/issues/51).
Canonical remediation: [GovOPlaN #52](https://git.add-ideas.de/GovOPlaN/govoplan/issues/52).
This follow-up to the [source security/performance review](SECURITY_PERFORMANCE_REVIEW_2026-09-08.md)
records registry-only scans of nine proposed runtime dependencies and two
same-minor patch candidates. **Runtime publication is held:** patch-only updates
do not resolve the baseline. Source/package publication is a separate outcome.
No images were executed, rebuilt, selected for CI, or published by this audit.
## Method and reproducible evidence
Official Trivy **0.74.0** was installed only in a private local task directory,
without sudo or Docker access. Its Linux-64bit release archive matched both the
official checksums file and GitHub release asset metadata:
- Archive SHA256: `2ae6fe3ee734b7fdf11335663e18c75ea12dccc76062f09f164a3b0f8be4371a`.
- Checksums-file SHA256: `bc701c3c3ee8b9acbea2c23257e41381e3854888f51281616a6ba5dc96963821`.
- Vulnerability database schema 2, updated `2026-09-07T19:06:01.154199452Z`,
downloaded from `mirror.gcr.io/aquasec/trivy-db:2`.
- Scan flags: `--image-src remote --platform linux/amd64 --scanners vuln
--format json --no-progress --timeout 8m --max-image-size 2GB --exit-code 0`.
Findings were counted from validated JSON; exit zero did not mean clean.
- Existing Docker credentials were not read; no private keys or secrets were
used. Checksums over official HTTPS metadata were verified, not independent
Sigstore signatures. See the [official release](https://github.com/aquasecurity/trivy/releases/tag/v0.74.0)
and [registry-only scan documentation](https://trivy.dev/docs/latest/target/container_image/).
Raw evidence is retained locally, not committed:
`/home/zemion/.cache/govoplan-trivy-remote.yKZgjDOg/scan/`.
It contains eleven `reports/*-amd64.json` reports/logs, scanner scripts,
`patch-candidate-inspection.json`, exact successor registry indices, and
`evidence-checksums.json`. Summary SHA256 values:
- `summary.json`: `f2785a731d637452ab9c0b1f5399772c0f8828a63ca83d5fa7496abdad1c757a`.
- `patch-summary.json`: `b3fc6273fcdad98864040ccdf3477ecf379afd46e9f94444b1f2910f48c1d85b`.
All eleven executions succeeded without timeout/rate-limit failure. Initial
summary fields distinguish `scan_execution_complete: true` from
`coverage_complete: false`: Garage has no detectable package inventory.
Checksums preserve evidence identity, not indefinite storage availability.
## Exact requested pins and results
All references below use `docker.io/`. Counts are package-vulnerability records,
not distinct CVEs or confirmed exploitable application defects. A vulnerability
can appear against several installed packages. Unfixed/unknown records remain.
| Image tag | Exact index SHA256 | Critical / High / Medium / Low / Unknown | Fixable C/H |
| --- | --- | --- | ---: |
| `library/python:3.12-slim-bookworm` | `782412e85d0f0984994c290652577d4018aff08145c85b262bb63dc0c7522254` | 5 / 55 / 102 / 103 / 5 | 0 |
| `library/postgres:16-alpine` | `cf78e76683b9ca8c5733cbbdce6c9262b45b6767934dd0a95e671f9a0fc20685` | 1 / 30 / 28 / 14 / 1 | 31 |
| `library/redis:7-alpine` | `ff02b58f971e7d7d156a1267e283fcbbeee91773b6aa36c49dac28ecfe28eadf` | 0 / 0 / 0 / 0 / 0 | 0 |
| `nginxinc/nginx-unprivileged:1.29-alpine` | `0c79d56aee561a1d81c63f00eee5fb5fe29279560cdc55e91425133104c7fbe6` | 0 / 33 / 74 / 37 / 20 | 33 |
| `library/haproxy:3.2.21-alpine` | `66e25cc9a8332635f4e897f7f4b1e5622c25f09f0ee23cddc6ce9bdb3a24772a` | 0 / 2 / 6 / 12 / 0 | 2 |
| `library/caddy:2.10.2-alpine` | `4c6e91c6ed0e2fa03efd5b44747b625fec79bc9cd06ac5235a779726618e530d` | 7 / 75 / 67 / 37 / 4 | 82 |
| `dxflrs/garage:v2.3.0` | `866bd13ed2038ba7e7190e840482bc27234c4afaf77be8cfa439ae088c1e4690` | **Unknown: no inventory** | — |
| `greenmail/standalone:2.1.9` | `3ac5a83dd6727cf95e4d50e18907fb8ee7bbf5f67e8534714dee2fb1b5b2e1d4` | 0 / 0 / 116 / 35 / 0 | 0 |
| `tonistiigi/binfmt:qemu-v10.2.3-68` | `400a4873b838d1b89194d982c45e5fb3cda4593fbfd7e08a02e76b03b21166f0` | 0 / 9 / 2 / 1 / 1 | 9 |
## Patch-only options and limits
Complete publisher tag listings were inspected for nginx 1.29, Caddy 2.10,
HAProxy 3.2, GreenMail 2.1 and binfmt qemu10.2. Two newer candidates were
scanned; their registry index bytes matched both registry and publisher digests,
and contained amd64 and arm64 manifests:
- `library/haproxy:3.2.23-alpine@sha256:6343ce34a132a5dceaa24767d739df2bd519f8f7c1079ae39e4821334e8eb42e`:
same Alpine 3.24.1, 24 detected OS packages, zero reported findings. This is
a useful candidate, not a completed compatibility test or application audit.
- `greenmail/standalone:2.1.13@sha256:3df66b7edd01c8a301343ca5e3601d8674760d4708655573560c24745e624fb2`:
upstream changes Ubuntu 22.04 to Debian 13.6; **3 C / 80 H / 98 M / 85 L /
5 unknown**, 30 fixable C/H records. Not selected as a no-base-change update.
- nginx's newest matching Alpine patch is already 1.29.8 at the scanned pin;
Caddy 2.10 remains 2.10.2; binfmt qemu10.2 remains 10.2.3-68. No newer matching
publisher images were found. The current [official Caddy image catalogue](https://raw.githubusercontent.com/docker-library/official-images/master/library/caddy)
uses 2.11.4; switching minor lines requires new scans and compatibility checks.
Priority remediation: Caddy's own seven HIGH records require fixes through
2.11.4, with additional bundled Go/library fixes that must be re-scanned;
nginx's packages include curl/libcurl fixes through 8.22.0-r0, OpenSSL 3.5.8-r0,
c-ares 1.34.8-r0, expat 2.8.1-r0 and libuuid 2.41.6-r1. PostgreSQL's OS records
require OpenSSL 3.5.8-r0 and libuuid 2.42.3-r1; its CRITICAL plus 21 HIGH Go
records concern the **gosu helper**, not PostgreSQL server code. binfmt's nine
HIGH records concern its Go 1.26.4 build, with fixes through 1.26.6. Package
presence does not establish vulnerable-symbol reachability. No unscanned tag
is claimed to meet every fix requirement.
## Python triage and coverage caveats
Python image metadata identifies CPython 3.12.14, but Trivy inventories only
Debian packages and pip, **not CPython/stdlib**. All 60 C/H records concern
Debian packages: 21 CVEs, 50 `affected` records, 9 `fix_deferred`, 1
`will_not_fix`, without a recorded fixed Bookworm version. Five util-linux CVEs
repeat across eight binary packages. These remain installed; they are not all
removed build dependencies. Pip 25.0.1 separately has five MEDIUM/one LOW
records, with fixes through 26.2.0; it is install tooling, and the API image uses
an offline `--no-index` wheelhouse rather than an arbitrary package index.
Narrow triage examples, **not blanket exemptions**:
- Debian states [CVE-2023-45853](https://security-tracker.debian.org/tracker/CVE-2023-45853)
does not affect the built Bookworm zlib binaries because vulnerable minizip
code is not included. Other bundled minizip implementations are separate.
- [CVE-2026-8376](https://security-tracker.debian.org/tracker/CVE-2026-8376)
explicitly requires 32-bit Perl; this scan targets amd64.
- [CVE-2025-7458](https://security-tracker.debian.org/tracker/CVE-2025-7458)
requires crafted arbitrary SQLite SQL; the managed runtime uses PostgreSQL,
but alternate SQLite use must be reviewed.
- Perl's regex and Archive::Tar records need exact binary/module applicability
checks; vendor-deferred status alone is not a finding dismissal.
Only amd64 was scanned. arm64, newly built GovOPlaN API/Web layers and optional
dependency combinations remain unverified. Garage has no inventory; Redis,
HAProxy and PostgreSQL source-built executables, CPython and QEMU static
binaries need supplemental SBOM/source coverage. Zero detected OS findings is
not zero application vulnerabilities. Trivy also lacks Alpine 3.24 EOL metadata
and nginx CVE-2026-80256 detail; unknowns are retained. There were no runtime,
exploitability, secret, misconfiguration, malware or signature-policy checks.
Before lifting the runtime hold: approve and test maintained image-line changes
where necessary, fix or narrowly disposition findings with evidence, close
inventory gaps, scan both architectures and final runtime layers, then run
deployment/ingress smoke checks. Do not silently change base OS, use unpinned
`latest`, rebuild third-party images, or accept all HIGH/CRITICAL findings.
@@ -0,0 +1,176 @@
# Security and performance review — 8 September 2026
Coordinated status: [Core #296](https://git.add-ideas.de/GovOPlaN/govoplan-core/issues/296).
This records a workspace-wide automated scan, targeted manual boundary review,
and a verified implementation pass. It is not a penetration test, an exhaustive
line-by-line review, or a security certification. The audit was completed on
local, unpublished changes, preserving existing worktree changes. Subsequent
release preparation/publication is tracked in
[GovOPlaN #51](https://git.add-ideas.de/GovOPlaN/govoplan/issues/51) and the
[0.1.45 release notes](../releases/0.1.45.md).
## Implemented findings
| Area | Finding and change | Evidence / ownership |
| --- | --- | --- |
| Authentication — high | Preserve service-account provenance and current scope ceilings instead of recalculating them as ordinary membership permissions. Tenant API keys cannot retain canonical system permissions or unsafe wildcard grants. | Previously failing isolated regressions; [Access #21](https://git.add-ideas.de/GovOPlaN/govoplan-access/issues/21). |
| Authentication — medium | Warm-cache API keys must follow the same explicit-header credential rules as cold authentication. A session cookie cannot turn an API key into a session credential. | Regression covering source-dependent authentication. |
| Browser authority/cache — medium | Clear reusable data on auth changes and write settlement; fence late 200/304 writes and obsolete 401 side effects. Honor server no-store/no-cache and explicit fresh-read requests. Interactive login/logout remove retained automation keys that could shadow cookie-session identity. | 23 real-client regressions. Unchanged settings keep their object identity, preventing profile-fetch loops. Core `docs/API_CLIENT_CACHE_CONTRACT.md`; owning Access EN/DE session/field documentation. |
| Spreadsheet resource exhaustion | Validate actual XLSX coordinates before openpyxl traversal; ignore misleading declared dimensions; count blank row gaps toward the existing limits. | [Connectors #18](https://git.add-ideas.de/GovOPlaN/govoplan-connectors/issues/18), 13 tests and 2 subtests. |
| Template resource exhaustion | Enforce the existing 5 MiB output budget during substitution and item construction, including UTF-8, HTML escaping and separators. | [Templates #7](https://git.add-ideas.de/GovOPlaN/govoplan-templates/issues/7), full 20 tests; independent 3,000-case valid-output comparison. |
| Archive resource exhaustion | Inspect regular TAR member limits before traversing payloads. Limit archive paths to 4,096 UTF-8 bytes / 128 components and count derived directories against entry limits. | [Files #46](https://git.add-ideas.de/GovOPlaN/govoplan-files/issues/46), 55 archive and 15 documentation tests. Extension-header decoding still needs stronger isolation. |
| Dataflow resource exhaustion | Reject LPAD/RPAD target lengths above the existing 1,000,000-byte preview budget before fill evaluation/allocation. Preserve final serialized-byte checks. | [Dataflow #22](https://git.add-ideas.de/GovOPlaN/govoplan-dataflow/issues/22), full 104 tests and 39 subtests; 7 new guard tests independently rerun. |
| Docs performance / defense in depth | Batch revision reads per request, avoid loading pending draft bodies for readers, and validate tenant/entry/publication consistency while retaining owner/audience checks. | [Docs #22](https://git.add-ideas.de/GovOPlaN/govoplan-docs/issues/22), full 39 tests. |
| Notifications performance / defense in depth | Batch delivery-attempt loading while preserving recipient checks and rejecting inconsistent attempt references, including already-loaded relationships. | [Notifications #6](https://git.add-ideas.de/GovOPlaN/govoplan-notifications/issues/6), full 23 tests. |
| Session-list performance | Apply active/expiry predicates and the existing 100-row cap in SQL, before loading session history. | Query-shape regression in Access. |
| Reporting correctness | Use structural bind-name suffixes for recursive calculated measures, preserving valid dotted/hyphenated public keys and parameter uniqueness. | [Reporting #10](https://git.add-ideas.de/GovOPlaN/govoplan-reporting/issues/10), full 29 tests. |
| Audit hygiene | Redact Gitleaks logs and machine reports on current, history and legacy scanner paths. | 13 audit-wrapper tests enforce the flag. |
All changed module workflows/limits have owning EN/DE DocumentationTopic updates.
Independent review found no concrete regression in the backend changes.
## Measured performance changes
These are SQL-query counts in isolated 40-item fixtures, not production latency
or throughput claims. Authorization is still evaluated for each request.
| Projection | Before | After |
| --- | ---: | ---: |
| Docs reader entries | 41 SELECTs | 2 SELECTs |
| Docs editor entries | 81 SELECTs | 2 SELECTs |
| Notification list with attempts | 41 SELECTs | 2 SELECTs |
The Docs 401-entry batching regression uses 3 SELECTs. Resource guards reject
oversized work before the formerly expensive allocation/traversal. This does
not make every legitimate upload or campaign faster. Honoring no-cache can
increase server validation requests; ETags still avoid retransmitting unchanged
bodies. That authorization/freshness trade-off is deliberate.
The original audit snapshot measured 517,380 initial JavaScript bytes and
164,119 gzip bytes. Release preparation's pure-defaults split reduces this to
516,730 initial bytes and 163,908 gzip bytes. Restoring the missing Tasks
descriptor then measures 516,987 initial / 163,976 gzip bytes with all 46 module
descriptors lazy, within the unchanged 524,288 / 164,128 caps. The gzip margin is still small; future
startup work should reduce eager dependencies, not raise the cap automatically.
The full 209-case browser suite passed before the split, followed by 13 focused
browser checks after it. Radon recorded 238 rank-D-or-higher entries; complexity
is a review-priority signal, not a performance measurement.
## Dependency remediation
Core's full npm audit went from 30 affected package entries to zero. Most initial
entries were transitive effects of the same Tiptap advisory, not 30 independent
application exploits. The website went from two affected entries to zero; both
Mail lockfiles also report zero.
- Tiptap packages are aligned at 3.31.3, with direct minimum ranges raised to
3.30.4 in both development and release manifests, with a parity regression.
Added an actual installed-library prototype-attribute regression for
the [maintainer's security advisory](https://github.com/ueberdosis/tiptap/security/advisories/GHSA-cp6q-959q-f8rh).
- Core now resolves xmldom 0.9.12, browserslist 4.28.9 and nanoid 3.3.18.
The website's affected browserslist/nanoid dependencies are patched too.
- Development/audit requirements now require pip >=26.2; the local development
environment uses 26.2.1. The installed audit originally flagged
[CVE-2026-13346](https://github.com/advisories/GHSA-qwm4-qh6w-59xr), requiring an
attacker-controlled package index. This is an installation-tool vulnerability,
not evidence of an exposed application endpoint.
The final installed Python audit enumerated 188 distributions: 137 were
auditable with zero known vulnerabilities, and 51 local distributions were not
available in PyPI. Those skips are covered by source review, not by a claim of
dependency-advisory coverage. Production images and every optional dependency
combination were not independently resolved or scanned.
## Scan coverage and limitations
Evidence directory:
`/mnt/DATA/tmp/govoplan-security-performance-20260908-gsk8jn/`.
The final `final-quick/manifest.json` captures 79 repositories, tool versions,
start/end repository fingerprints, report checksums, 168 report artifacts and
163 validated JSON/SARIF reports. It records an unchanged workspace, complete
coverage for its four required scanners, no execution errors and no missing
reports. It ran in report-only mode: exit zero does **not** mean zero warnings.
- Final production Bandit: 447,008 Python lines; 67 warnings (63 low, 4 medium),
no high findings. Ruff security rules: 54 warnings. SQL-construction warnings
were reviewed against identifier/operator validation and bound values in
DuckDB/Reporting; no injection fix was warranted there. XML import warnings
were checked: feed/BPMN input parsing uses defusedxml; stdlib imports support
types/output construction. Operator-owned fenced-run argv is not a public
arbitrary-command endpoint. Xrechnung output buffering remains a follow-up.
Assertions and error-swallowing markers remain review/maintenance warnings,
not proof that all such code is harmless.
- Final local Semgrep rules: no findings. The broader OWASP-rule pass applied
272 rules to 4,169 tracked targets. Its seven warnings recommended weakening
owner-only 0700 permissions; they were rejected as false positives. One
Calendar rule timeout was rerun with a 60-second budget: zero findings/errors.
Bash and conformance TypeScript checks passed despite two scanner-specific
parser limitations. Ignored/dependency/generated paths are not a complete
line-by-line source audit.
- Gitleaks: 79 Git histories plus 79 worktrees, 158 redacted reports, zero
detected secrets. This does not establish that deployed credentials are safe
or that formerly exposed credentials have been rotated.
- Tool versions included Semgrep 1.176.1, Bandit 1.9.4, Ruff 0.15.21 and
Gitleaks 8.30.1. The downloaded Gitleaks binary archive matched the official
release SHA-256 before execution.
- The containerized full-toolbox path could not access Docker's daemon. Its
full-mode Trivy/misconfiguration and additional OSV scans were **not** run.
A subsequent [registry-only runtime image audit](RUNTIME_IMAGE_AUDIT_2026-09-08.md)
successfully scanned nine pinned candidates and two same-minor successors
for amd64 without Docker. It found unresolved vulnerabilities and inventory
gaps; runtime publication is held. This does not complete full-toolbox,
arm64, final-runtime-image or deployment coverage.
No live application probes, database changes, file operations, mail sends,
IMAP appends, imports, notification delivery, deployments, commits or pushes
were performed. Browser tests used isolated mocked fixtures. Package installs,
builds and temporary audit-tool installation were local development operations.
## Verification and remaining work
- 209/209 browser conformance tests pass; production Core/website builds,
conformance TypeScript, 24 Core client/dependency regressions, 4 real-client
Files reload checks, and 72/72 manifest checks pass.
- Access's full 91-test suite passed before the final documentation-only update;
the final documentation suite passed all 4 tests. Other module counts appear
above. The new authentication/resource tests include demonstrated pre-fix
failures rather than only structural assertions.
- The original focused workspace run stopped at the institutional
governance/Portal fixture (`tests/test_institutional_governance_journey.py:223`,
`IndexError`). Release preparation fixes its mixed clocks using the existing
temporal context, retaining validity-boundary exclusions; 7 journey tests and
ambient-year checks pass. Tracked in
[Meta #50](https://git.add-ideas.de/GovOPlaN/govoplan/issues/50).
- Campaign's apparent host-path issue was ruled out by existing tracked
API/build/snapshot guards and 11 passing tests under normal initialization.
Release preparation fixes the standalone import cycle through a deferred
resolver import without changing validation rules. Fresh-process coverage,
all 11 path tests and Campaign's full 611-test suite pass.
Next coordinated work:
1. [Hard resource isolation — Core #297](https://git.add-ideas.de/GovOPlaN/govoplan-core/issues/297):
regex CPU, aggregate allocation, TAR extension metadata, bounded workers and
cancellation, followed by production-like concurrent load tests.
2. [Forced password change/recovery — Access #22](https://git.add-ideas.de/GovOPlaN/govoplan-access/issues/22):
the current flag is advisory only. Do not enable enforcement without a usable
local-password/recovery flow and external-provider rules.
3. [Bound subprocess output — Xrechnung #2](https://git.add-ideas.de/GovOPlaN/govoplan-xrechnung/issues/2):
enforce the existing 2 MiB limit while draining stdout/stderr, not afterwards.
4. [Workflow revision batching/history projection — Workflow Engine #3](https://git.add-ideas.de/GovOPlaN/govoplan-workflow-engine/issues/3):
batch evidence lookups; separately define explicit history pagination and
authorized-total semantics. Docs/notification history volumes also remain.
5. Resolve the [runtime image audit](RUNTIME_IMAGE_AUDIT_2026-09-08.md) findings
tracked in [Meta #52](https://git.add-ideas.de/GovOPlaN/govoplan/issues/52)
and coverage gaps before lifting its publication hold; complete deployment
audits, review exposed development credentials and worker quotas, and
benchmark realistic tenant sizes/concurrency. The sanctions
transport's fixed HTTPS/redirect allowlist is not a demonstrated arbitrary-URL
issue, but migration to Core's pinned egress transport remains desirable.
Operational compatibility: tenant keys relying on accidental system/wildcard
permissions must be corrected rather than weakening the guard. Extreme sparse
spreadsheets, overly deep/long archive paths and oversized padding intermediates
can now fail early with diagnostics. No stored documents or configurations were
deleted or silently migrated.
+12 -12
View File
@@ -13,7 +13,7 @@ dependencies = [
"govoplan-core[server]==0.1.45",
"govoplan-tenancy==0.1.22",
"govoplan-organizations==0.1.21",
"govoplan-identity==0.1.20",
"govoplan-identity==0.1.21",
"govoplan-idm==0.1.25",
"govoplan-access==0.1.25",
"govoplan-admin==0.1.23",
@@ -31,7 +31,7 @@ dependencies = [
[project.optional-dependencies]
full = [
"govoplan-addresses==0.1.22",
"govoplan-approvals==0.1.20",
"govoplan-approvals==0.1.21",
"govoplan-assets==0.1.20",
"govoplan-booking==0.1.20",
"govoplan-cases==0.1.24",
@@ -45,30 +45,30 @@ full = [
"govoplan-decisions==0.1.19",
"govoplan-dist-lists==0.1.21",
"govoplan-dms==0.1.20",
"govoplan-encryption==0.1.19",
"govoplan-encryption==0.1.20",
"govoplan-erp==0.1.20",
"govoplan-evaluation==0.1.20",
"govoplan-facilities==0.1.20",
"govoplan-fit-connect==0.1.20",
"govoplan-forms==0.1.22",
"govoplan-forms-runtime==0.1.20",
"govoplan-forms==0.1.23",
"govoplan-forms-runtime==0.1.21",
"govoplan-grants==0.1.20",
"govoplan-helpdesk==0.1.21",
"govoplan-identity-trust==0.1.20",
"govoplan-identity-trust==0.1.21",
"govoplan-inspections==0.1.20",
"govoplan-learning==0.1.20",
"govoplan-mandates==0.1.19",
"govoplan-notifications==0.1.20",
"govoplan-parties==0.1.19",
"govoplan-payments==0.1.21",
"govoplan-payments==0.1.22",
"govoplan-permits==0.1.20",
"govoplan-poll==0.1.20",
"govoplan-portal==0.1.21",
"govoplan-postbox==0.1.22",
"govoplan-portal==0.1.22",
"govoplan-postbox==0.1.23",
"govoplan-procurement==0.1.20",
"govoplan-projects==0.1.19",
"govoplan-quick-access==0.1.20",
"govoplan-records==0.1.23",
"govoplan-projects==0.1.20",
"govoplan-quick-access==0.1.21",
"govoplan-records==0.1.24",
"govoplan-reporting==0.1.21",
"govoplan-resources==0.1.20",
"govoplan-rest==0.1.19",
+1 -1
View File
@@ -3,7 +3,7 @@
../govoplan-core[server]
govoplan-tenancy @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-tenancy.git@v0.1.22
govoplan-organizations @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-organizations.git@v0.1.21
govoplan-identity @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-identity.git@v0.1.20
govoplan-identity @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-identity.git@v0.1.21
govoplan-idm @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-idm.git@v0.1.25
govoplan-access @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-access.git@v0.1.25
govoplan-admin @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-admin.git@v0.1.23
+78
View File
@@ -0,0 +1,78 @@
from __future__ import annotations
import json
from pathlib import Path
import runpy
import tempfile
import unittest
META_ROOT = Path(__file__).resolve().parents[1]
CHECK = runpy.run_path(str(META_ROOT / "tools/checks/check-webui-package-facades.py"))
class WebuiPackageFacadeTests(unittest.TestCase):
def setUp(self) -> None:
self.temporary = tempfile.TemporaryDirectory()
self.addCleanup(self.temporary.cleanup)
self.repository = Path(self.temporary.name) / "govoplan-example"
source = self.repository / "webui/src"
source.mkdir(parents=True)
(source / "index.ts").write_text("export {};\n", encoding="utf-8")
(source / "styles.css").write_text(":root {}\n", encoding="utf-8")
self.webui = {
"name": "@govoplan/example-webui", "version": "0.1.2", "type": "module",
"main": "src/index.ts",
"exports": {".": {"import": "./src/index.ts"}, "./styles.css": "./src/styles.css"},
"peerDependencies": {"@govoplan/core-webui": "^0.1.45"},
"peerDependenciesMeta": {"@govoplan/core-webui": {"optional": True}},
}
self.root = {**self.webui, **{
field: CHECK["prefixed_entries"](self.webui[field])
for field in CHECK["ENTRY_FIELDS"] if field in self.webui
}}
self.write_manifests()
def write_manifests(self) -> None:
(self.repository / "webui/package.json").write_text(json.dumps(self.webui), encoding="utf-8")
(self.repository / "package.json").write_text(json.dumps(self.root), encoding="utf-8")
def issues(self) -> list[str]:
return CHECK["facade_issues"](self.repository, package_name="@govoplan/example-webui")
def test_matching_conditional_and_css_entries_are_accepted(self) -> None:
self.assertEqual([], self.issues())
def test_missing_root_or_generic_package_is_rejected(self) -> None:
(self.repository / "package.json").unlink()
self.assertIn("cannot read", " ".join(self.issues()))
self.root = {"name": "@govoplan/example", "version": "0.1.2"}
self.write_manifests()
self.assertIn("root name differs", " ".join(self.issues()))
self.assertIn("no WebUI entry point", " ".join(self.issues()))
def test_peer_drift_or_missing_entry_is_rejected(self) -> None:
self.root["peerDependencies"] = {"@govoplan/core-webui": "^9.0.0"}
self.write_manifests()
self.assertIn("peerDependencies differs", " ".join(self.issues()))
(self.repository / "webui/src/styles.css").unlink()
self.assertIn("missing exports entry", " ".join(self.issues()))
def test_entry_cannot_escape_webui_even_if_it_exists(self) -> None:
(self.repository / "outside.ts").write_text("export {};\n", encoding="utf-8")
self.root["main"] = "webui/../outside.ts"
self.write_manifests()
self.assertIn("escapes webui/", " ".join(self.issues()))
def test_release_composition_checks_only_declared_module_sources(self) -> None:
core = self.repository.parent / "govoplan-core/webui"
core.mkdir(parents=True)
(core / "package.release.json").write_text(json.dumps({"dependencies": {
"react": "19.2.7",
"@govoplan/example-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-example.git#v0.1.2",
}}), encoding="utf-8")
self.assertEqual((1, []), CHECK["check_composition"](self.repository.parent))
if __name__ == "__main__":
unittest.main()
+2
View File
@@ -47,6 +47,8 @@ cd "$META_ROOT"
"$PYTHON" tools/inventory/platform-interface-inventory.py --strict-declarations --strict-endpoints
"$PYTHON" tools/repo/sync-module-package-workflows.py --check
"$PYTHON" tools/release/generate-developer-meta-package.py --check
"$PYTHON" tools/checks/check-webui-package-facades.py
"$PYTHON" -m unittest tests.test_webui_package_facades
"$PYTHON" -m unittest tests.test_module_package_workflows tests.test_package_registry_release
"$PYTHON" -m unittest tests.test_deployment_installer
"$PYTHON" -m unittest tests.test_capability_fit_evidence
+101
View File
@@ -0,0 +1,101 @@
#!/usr/bin/env python3
"""Check that release Git dependencies expose their owning WebUI package."""
from __future__ import annotations
import argparse
import json
from pathlib import Path
import re
META_ROOT = Path(__file__).resolve().parents[2]
GIT_REPOSITORY = re.compile(r"/(govoplan-[a-z0-9-]+)\.git#v[^/]+$")
PARITY_FIELDS = (
"name", "version", "type", "dependencies", "optionalDependencies",
"peerDependencies", "peerDependenciesMeta",
)
ENTRY_FIELDS = ("main", "module", "types", "exports")
def prefixed_entries(value: object) -> object:
if isinstance(value, str):
return "./webui/" + value[2:] if value.startswith("./") else "webui/" + value
if isinstance(value, dict):
return {key: prefixed_entries(item) for key, item in value.items()}
if isinstance(value, list):
return [prefixed_entries(item) for item in value]
return value
def entry_paths(value: object) -> list[str]:
if isinstance(value, str):
return [value]
if isinstance(value, dict):
return [path for item in value.values() for path in entry_paths(item)]
if isinstance(value, list):
return [path for item in value for path in entry_paths(item)]
return []
def facade_issues(repository: Path, *, package_name: str) -> list[str]:
issues: list[str] = []
try:
root = json.loads((repository / "package.json").read_text(encoding="utf-8"))
webui = json.loads((repository / "webui/package.json").read_text(encoding="utf-8"))
except (OSError, ValueError) as exc:
return [f"{repository.name}: cannot read package facades: {exc}"]
if not isinstance(root, dict) or not isinstance(webui, dict):
return [f"{repository.name}: package manifests must be JSON objects"]
if webui.get("name") != package_name:
issues.append(f"{repository.name}: WebUI name does not match {package_name}")
for field in PARITY_FIELDS:
if root.get(field) != webui.get(field):
issues.append(f"{repository.name}: root {field} differs from owning WebUI package")
for field in ENTRY_FIELDS:
expected = prefixed_entries(webui.get(field))
if root.get(field) != expected:
issues.append(f"{repository.name}: root {field} must target the corresponding webui/ entry")
for entry in entry_paths(root.get(field)):
path = repository / entry
if not path.resolve().is_relative_to((repository / "webui").resolve()):
issues.append(f"{repository.name}: {field} entry escapes webui/: {entry}")
elif "*" not in entry and not path.is_file():
issues.append(f"{repository.name}: missing {field} entry: {entry}")
if not root.get("exports") and not root.get("main"):
issues.append(f"{repository.name}: root package has no WebUI entry point")
return issues
def check_composition(workspace: Path, *, core_root: Path | None = None) -> tuple[int, list[str]]:
core = core_root or workspace / "govoplan-core"
release = json.loads((core / "webui/package.release.json").read_text(encoding="utf-8"))
checked = 0
issues: list[str] = []
for name, reference in release.get("dependencies", {}).items():
if not name.startswith("@govoplan/"):
continue
match = GIT_REPOSITORY.search(reference) if isinstance(reference, str) else None
if match is None:
issues.append(f"{name}: release dependency is not a versioned GovOPlaN Git source")
continue
checked += 1
issues.extend(facade_issues(workspace / match.group(1), package_name=name))
return checked, issues
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--workspace-root", type=Path, default=META_ROOT.parent)
parser.add_argument("--core-root", type=Path)
args = parser.parse_args()
checked, issues = check_composition(args.workspace_root, core_root=args.core_root)
if issues:
print("\n".join(issues))
return 1
print(f"Release WebUI package facade checks passed for {checked} Git dependencies")
return 0
if __name__ == "__main__":
raise SystemExit(main())