Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
114 KiB
phase, plan, type, wave, depends_on, quick_id, description, date, files_modified, autonomous, requirements, estimate, must_haves
| phase | plan | type | wave | depends_on | quick_id | description | date | files_modified | autonomous | requirements | estimate | must_haves | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| quick-261009-p0m | 01 | execute | 1 | 261009-p0m | Sicherheitsprotokoll (docs/sicherheitsprotokoll.md) mit allen bisherigen Sicherheitspruefungen und der ersten Vollpruefung, CI-Job security (nur Bericht, nie blockierend) mit gepinnten und pruefsummengesicherten Scannern, ZAP-Grundpruefung gegen alpha vor Freigaben (erster Lauf in diesem Auftrag), Behebung der sicher behebbaren Baseline-Befunde (Abhaengigkeiten innerhalb der Hauptversion, AES-GCM-Taglaenge, Laufzeitabbilder ohne Entwicklungswerkzeuge), Ausnahmelisten fuer verifizierte Fehlalarme, CHANGELOG und Anleitungen | 2026-10-09 |
|
true |
|
|
|
Purpose: the user asked on 08.10. for a protocol of all security checks, CI security tooling (audit, Semgrep, Trivy, gitleaks, ZAP) and a first full baseline. The research (261009-p0m-RESEARCH.md) measured that baseline: no real secrets, but 151 known vulnerabilities in production dependencies (5 critical), one real hardening item in our own code and a 1.66 GB api image that ships development tooling.
Six tasks, executed strictly in order, each by a fresh executor, each ending in a green committed state. Task 1 is the tracer: the complete reporting chain (pinned download → scan → allowlist → summary line → report directory → CI job) proven inside the real runner image. Task 2 writes the protocol and its links. Task 3 adds the ZAP outside check and runs it against alpha for the first time. Tasks 4 and 5 fix findings and record before/after numbers in the protocol. Task 6 re-runs the CI script on the fixed state, closes the protocol and cleans up.
Locked decisions — user (08.10.2026, NON-NEGOTIABLE):
- D-01
docs/sicherheitsprotokoll.md: ONE document listing ALL security checks done so far (inventory: reviews, threat models, RLS/data-separation tests, security tests, WINDOWS ledger) plus the baseline full scan (all scanners, counts, triage) plus how each check works, in plain German with „Sie“ for a non-programmer owner and later customers. - D-02 Linked from
docs/README.md(the documentation index; there is no README at the repository root) and from the Betriebs- and the Entwicklungsanleitung. - D-03 The protocol is kept current: the Entwicklungsanleitung gets a short „So pflegen Sie dieses Protokoll“ rule — every security review, every recorded scan and every pre-release ZAP run adds an entry.
- D-04 CI security job (gitleaks full history, pnpm audit and osv-scanner, Semgrep, Trivy fs plus Trivy on the freshly built images): REPORTING ONLY — never fails or blocks the pipeline; results visible as summary lines in the log and as artifact (best effort). Pinned, checksum-verified tool versions per research.
- D-05 OWASP ZAP baseline against alpha before releases: a script, documented as a step in Kapitel 9 „Eine Version freigeben“ of the Betriebsanleitung. Basic Auth in front of alpha stays; nothing in this task suggests removing it.
- D-06 The FIRST ZAP baseline run against alpha is executed in this task (passive baseline only, from the dev host) and recorded in the protocol.
- D-07 The two resting product topics (multi-organisation operation and licensing) are not framed as open topics anywhere; existing data-separation tests may be described as existing protection, without activation state or next stages.
Locked decisions — orchestrator (NON-NEGOTIABLE):
- D-08 (a) Dependency updates WITHIN the current majors only (Next 15.5.x latest patch; NestJS/express/multer/nodemailer/undici/axios/handlebars/adm-zip and the others patch/minor) plus pnpm
overridesfor vulnerable transitive packages where a patched version exists in range; NO major upgrades (Next 16 and Prisma 7 stay out). Re-run the audit afterwards and record before/after counts. Full test suites, local stack rebuild and e2e smoke stay green. - D-09 (b) AES-GCM decrypt enforces a 16-byte authentication tag (
apps/api/src/crypto/crypto.service.ts), with spec. - D-10 (c) api production image without development dependencies — only if it does not break the Prisma migrate-and-start flow; verified by rebuilding and starting the local stack; otherwise documented as open item.
- D-11 (d) Items without a fix (xlsx 0.18.5, node-forge 1.4.0) and accepted design choices (opt-in TLS bypasses, test fixtures, gitleaks false positives) are documented in the protocol with reasoning („bewusst akzeptiert“ / „offen“), plus a gitleaks allowlist
.gitleaks.tomlfor the verified false positives so future CI reports stay meaningful. - D-12 The raw baseline JSON (6.7 MB) does not go into git; the protocol carries the summarised numbers; a small summary file stays in the quick directory.
- D-13 Docs mandatory: CHANGELOG („Unveröffentlicht“, security entries), Betriebsanleitung, Entwicklungsanleitung, README link. No module version bumps (no seed version, no module changelog entry) unless a module's user-visible behaviour changes — none of the tasks below changes one.
Planner's discretion (decided here, apply as written):
- D-14 File layout:
.gitea/scripts/security-scan.sh(CI and local),.gitea/scripts/zap-baseline.shplus.gitea/scripts/zap-hooks.py(local pre-release step) — same directory, POSIX sh and ASCII German comments aspublish-images.sh. All reports go to the repository-root directorysecurity-reports/(gitignored and dockerignored); the CI artifact is namedsicherheitsberichte. - D-15 Source scans (pnpm audit, osv-scanner, Semgrep, Trivy fs) read a
git archive HEADexport in a temporary directory, gitleaks reads the git history only. This makes CI and local runs scan exactly the committed state: untracked or private material in a developer working tree never reaches a scanner, a report or an artifact. The script exportsGIT_CONFIG_COUNT/KEY_0/VALUE_0withsafe.directoryfor the scan root so git and gitleaks work on a checkout owned by another user (CI container as root, local clone owned by uid 1000). - D-16 Pins (verified at planning against the official release checksum files): gitleaks 8.30.1
https://github.com/gitleaks/gitleaks/releases/download/v8.30.1/gitleaks_8.30.1_linux_x64.tar.gzSHA256551f6fc83ea457d62a0d98237cbad105af8d557003051f41f3e7ca7b3f2470eb; trivy 0.75.0https://github.com/aquasecurity/trivy/releases/download/v0.75.0/trivy_0.75.0_Linux-64bit.tar.gzSHA256c6e65abddb348e25f10549df887045629cf28cc72453cd1c63acb717316b3f3f; osv-scanner 2.6.0https://github.com/google/osv-scanner/releases/download/v2.6.0/osv-scanner_linux_amd64SHA256ca69b3d3cd08f889a49dc0a383122f71cc528b83803671df5fd874d97485b108; semgrep 1.180.0 viapipx install semgrep==1.180.0; pnpm 9.15.0 viacorepack pnpm@9.15.0(or a pnpm 9.15.x already on PATH). Tool directory:/opt/hostedtoolcache/tessera-securitywhen/opt/hostedtoolcacheis writable (CI: the persistentact-toolcachevolume), otherwise${XDG_CACHE_HOME:-$HOME/.cache}/tessera-security;SECURITY_TOOL_DIRoverrides. Binaries live at{tooldir}/gitleaks-8.30.1/gitleaks,{tooldir}/trivy-0.75.0/trivy,{tooldir}/osv-scanner-2.6.0/osv-scanner; Trivy cache{tooldir}/trivy-cache(itsfanallayer cache is deleted after each run,dbstays). No marketplace actions for scanners (mutable tags, fetched from github.com). - D-17 Summary line contract (one line per tool, ASCII, counts only, in the log and in
{reportdir}/summary.txt):SECURITY-SUMMARY gitleaks findings={n};SECURITY-SUMMARY pnpm-audit-prod critical={n} high={n} moderate={n} low={n}(frommetadata.vulnerabilities);SECURITY-SUMMARY osv-scanner packages={n}(vulnerable package@version);SECURITY-SUMMARY semgrep findings={n} errors={n};SECURITY-SUMMARY trivy-fs critical={n} high={n} medium={n} low={n} misconfig={n} secrets={n};SECURITY-SUMMARY trivy-image-api critical={n} high={n} medium={n} low={n}and the same fortrivy-image-web; a tool that could not run printsSECURITY-SUMMARY {tool} skipped reason={word}; last lineSECURITY-SUMMARY fertig (nur Bericht, Exit 0).--print-planprintswerkzeug {name} {version}lines,scan-image {ref}per image orscan-image keine (nur main und Tags v*), andberichte {dir}— without network access. - D-18 Job placement:
securitywithneeds: publishand an explicitif: gitea.ref == 'refs/heads/main' || startsWith(gitea.ref, 'refs/tags/v')— the explicit condition is required because in Gitea a skippedneedsdoes not block (that is whypublishruns on pushes tolive, docs/ci-cd-setup.md §4); job-levelcontinue-on-error: true; checkout withfetch-depth: 0; one run stepsh .gitea/scripts/security-scan.sh all || true; thenactions/upload-artifact@v3(v4 is rejected by Gitea) withif: always(),continue-on-error: true, namesicherheitsberichte, pathsecurity-reports/,retention-days: 30. The job references no secret and no other job lists it inneeds. - D-19 Allowlist policy: the Zertifikatsmanager fixture directory
apps/api/src/cert-manager/__fixtures__/is allowlisted as a directory (it exists to hold test keys); every other false positive is allowlisted per exact file (anchored regex^…$with escaped dots) together withtargetRules, and for a file that is not a test or planning document (apps/web/src/messages/de.json) additionally with aregexesentry matching only the verified line (regexTarget = "line"). Every allowlist has a Germandescription. Trivy skips only the fixture directory for its secret scanner..semgrepignorekeeps:include .gitignoreand excludes both fixture directories,*.spec.ts,*.test.ts,*.test.tsxand.planning/. - D-20 ZAP: pinned image
ghcr.io/zaproxy/zaproxy@sha256:7aaa659b0d43078febd82e29bad112285c370727e86ab8340444220e17d9f0d2(2.17.0);zap-baseline.pywith--autooff --hook=…(research:-zconfiguration is silently ignored in 2.17, hooks work), traditional spider only, spider minutes default 3,-I(warnings never fail),-T 15, reports-r zap.html -w zap.md -J zap.json; the hook switches the spider's form processing and form POSTs off; no AJAX spider (it would type into and submit the login form). Default targethttps://alpha.tessera.ctl.de,ZAP_TARGEToverrides. PreflightGET /login: 200 → run without credentials (today's measured state); 401 → only withZAP_BASIC_AUTH_FILE(a file outside the repository withbenutzer:passwort), whose header reaches the container through a temporary env-file (mode 0600, removed by trap) and the hook's replacer rule — never on a command line, never in any output; otherwise stop with a German message. - D-21 CHANGELOG: security entries are bullets with the lead-in „Sicherheit: “ inside the existing groups „Neu“, „Geändert“, „Behoben“ of „Unveröffentlicht“ — not a separate fourth group, because
apps/web/src/lib/release-notes.tsshows only these three groups in the „Was ist neu“ window (any other group would silently disappear there) and the Entwicklungsanleitung fixes this structure; an existing bullet already uses this lead-in. This is how the orchestrator's „Sicherheit section“ is realised. - D-22 The two packages
@nestjs-modules/mailerandews-javascript-apiare removed: verified at planning that no file underapps/api/srcimports them (only comments mention them;mail.module.tssays the mailer package „wird von keinem Modul mehr benutzt“, Exchange runs over raw SOAP plus httpntlm). Their trees carry handlebars (critical), liquidjs, mjml, preview-email (nodemailer 8.0.11 without an in-major fix, deepmerge-ts, uuid 9), cheerio 1.0.0 with undici 6, axios, @xmldom/xmldom, moment and uuid 8 — removal is the most conservative fix. - D-23 Runtime images: the api Dockerfile gets a
prod-depsstage (production-only install of@tessera/api...with the Prisma client generated in that stage, because the virtual-store directory name of@prisma/clientdepends on resolved peers), the runner stage starts fromnode:24-alpineinstead ofbase(no corepack-prepared pnpm), and both runtime stages (api and web) remove the package managers shipped with the Node image (npm, npx, corepack, yarn) after listing what the image actually contains. If the api image cannot pass the fresh-database start and the e2e smoke within Task 5, both Dockerfiles are reverted and the item becomes „offen“ with the reason (D-10). - D-24 Desktop lockfile:
rustls0.23.41 → 0.23.45 (same minor, fixes a TLS 1.3 handshake advisory) viacargo update -p rustls --precise 0.23.45;glib0.18.5 needs 0.20 (outside the semver range, part of the GTK stack of Tauri) → „offen“. The changed Cargo.lock makes the next CI push on main rebuild the desktop packages — expected, noted in the SUMMARY. - D-25 Evidence files (counts and status words only, never a token or credential) live in
.planning/quick/261009-p0m-sicherheitsprotokoll-und-ci-sicherheitsp/checks/and are committed with their task;baseline/.gitignorekeeps every*.jsonthere out of git. - D-26 CLAUDE.md (its dated installed-stack table) is not edited by this plan; the SUMMARY tells the orchestrator which rows lag after Task 4.
Output: one script for the CI security job plus allowlists and the job itself, a ZAP script with hook, the protocol with links and maintenance rule, dependency and image fixes, the crypto hardening, evidence files and summary lines, CHANGELOG and guide updates. Commits on main, NOT pushed, nothing deployed.
<execution_context>
@/.claude/gsd-core/workflows/execute-plan.md
@/.claude/gsd-core/templates/summary.md
</execution_context>
Discovered facts the executor can rely on (verified during planning on 2026-10-09, HEAD d15a470):
- Working tree: besides the repository it contains untracked private material that must never reach a scanner, report, artifact, commit or evidence file, and must not be named anywhere. Scan only through the script (git-archive export plus history) or a fresh
git clone --no-localof the repository; never point a scanner at the working tree directory. - CI (
.gitea/workflows/ci.yml): jobsquality→test→desktop(if main or v*) →publish;publishbuilds with.gitea/scripts/publish-images.sh, which tagslocalhost:3002/schalli/tessera-ctl/{web,api}:{channel}(main:betapluslatest; tags v*:liveplusvX.Y.Z) in the HOST docker daemon (the runner mounts/var/run/docker.sock), so the images are present locally right after publish. The file header keeps a running list of quick ids — add one line for this job.docs/ci-cd-setup.md§4 lists four jobs in a numbered list and explains that a skippedneedsdoes not block; §5 covers the docker socket; §6 is troubleshooting; that document writes umlauts as ae/oe/ue. - Runner image
gitea/runner-images:ubuntu-latest(present locally, digestsha256:15f5ee61…): node 24.16 with corepack on PATH (no pnpm), pipx, jq, python3 3.12, git, sha256sum, curl; no PyYAML. Job containers run on docker networkgitea;act-toolcachevolume is mounted at/opt/hostedtoolcache. Runner: Gitea 1.26.2 with act_runner (job summaries need Gitea 1.27 → log lines instead). - Host: python3 3.13 (tomllib), jq, curl, pnpm 9.15.0, cargo with an existing
apps/desktop/src-tauri/target; no pipx (semgrep is skipped on the host; that is fine).js-yaml@4.2.0is innode_modules/.pnpm(YAML parsing for the ci.yml gate). Disk: 79 % used, 38 GB free; >85 % must be reported to the user. - Locally present research images (pruned in Task 6 by exact name, never with
-a):semgrep/semgrep:1.180.0,aquasec/trivy:0.75.0,ghcr.io/aquasecurity/trivy:0.75.0,ghcr.io/google/osv-scanner:v2.6.0,ghcr.io/gitleaks/gitleaks:latest,ghcr.io/gitleaks/gitleaks:v8.30.1(andcurlimages/curlif present). Keepghcr.io/zaproxy/zaproxy(pinned digest above, runs as userzap, uid 1000) andgitea/runner-images:ubuntu-latest. - Baseline gitleaks findings (20, all false positives, redacted file
baseline/gitleaks-history.json):private-keyin the nine.pemfiles underapps/api/src/cert-manager/__fixtures__/, inapps/api/src/cert-manager/cert-keys.spec.ts(3),cert-output.spec.ts(1),cert-templates.spec.ts(2) and.planning/quick/261009-ikt-cert-manager-umbau-mehrere-dateien-zip-f/261009-ikt-RESEARCH.md(1);generic-api-keyinapps/web/src/messages/de.json(an i18n label),apps/web/src/app/(portal)/modules/proxmox/settings/components/ServerForm.test.tsxand.planning/phases/12-tender-notifications/12-VALIDATION.md;curl-auth-userin.planning/quick/261008-mzu-modul-nextcloud-dateien-eigenstaendiger-/261008-mzu-RESEARCH.md(an example against the local throwaway Nextcloud test container). Trivy fs secrets: 7 HIGH, all in the fixture directory. Semgrep: fixture hits, spec/test hits andapps/api/src/tenders/__fixtures__/cosinex-search.htmlare noise; real-source hits: 13 in ci.yml (12 mutable action tags, 1 curl pipe shell),gcm-no-tag-lengthin crypto.service.ts, 4bypass-tls-verification(ldap.service.ts, proxmox-auth.ts, proxmox-client.service.ts, icon-discovery.service.ts — opt-in per connection),js-open-redirectin the login page (guarded bysanitizeNextPath()inapps/web/src/lib/safe-next.ts), 3detect-non-literal-regexp(exchange providers, code constants),prototype-pollution-loopinautodns-parse.ts(read-only walk), 3 pnpm-workspace hardening keys. - Production audit (baseline,
pnpm audit --prod: C5 H73 M68 L5, 30 packages) and dependency chains (pnpm why): next 15.5.19 (direct web, fix 15.5.27; 15.5.27 also allows sharp^0.34.3 || ^0.35.4); proxy-addr 2.0.7 via express 5.2.1, which@nestjs/platform-expresspins exactly (11.1.27 and 11.2.7 both pin express 5.2.1 → override); multer pinned 2.1.1 by platform-express 11.1.27 and 2.4.0 by 11.2.7; nodemailer 9.0.1 direct and via imapflow, 8.0.11/9.0.0 via@nestjs-modules/mailer→ preview-email/mailparser; handlebars, liquidjs, mjml, cheerio 1.0.0 → undici 6.27.0, uuid 9, deepmerge-ts, brace-expansion 5.0.6 via@nestjs-modules/mailer; axios 1.18.1, @xmldom/xmldom 0.8.13, moment, uuid 8 viaews-javascript-api; underscore 1.12.1 via httpntlm; ip-address 10.2.0 via imapflow → socks; undici 7.28.0 direct (exact pin on purpose, see the „undici-Dispatcher-Falle“ in the Entwicklungsanleitung); xlsx 0.18.5 and node-forge 1.4.0 direct with no fixed version on npm. Newest in-major versions at planning (npm view): next 15.5.27, nodemailer 9.1.1, undici 7.30.0, @nestjs/core and platform-express 11.2.7, adm-zip 0.6.1, csv-parse 7.0.3, axios 1.20.0, handlebars 4.7.10, multer 2.4.0, proxy-addr 2.0.8, @xmldom/xmldom 0.8.15, ip-address 10.7.3, liquidjs 10.30.0, js-yaml 4.3.2, svgo 4.1.0, nanoid 3.3.20, browserslist 4.29.3, qs 6.16.0, underscore 1.13.8, linkify-it 5.0.2, moment 2.31.0, source-map-js 1.2.2, postcss 8.5.29, postcss-selector-parser 7.1.6, brace-expansion 2.1.7 and 5.0.12, sharp 0.35.5. There is nopnpm.overridesin the root package.json yet. Prisma is pinned 6.19.3 (CLIprismais a production dependency of the api, needed by migrate-and-start). - Image baseline (research): api:beta Node C6 H116 M98 L7 (1.66 GB; tinypool, tar 6.2.1 and pnpm 9.15.9 come from devDependencies and the corepack-prepared pnpm of the
basestage), web:beta Node C2 H19 M21 L1 (npm's bundled packages of the Node base image: tar 7.5.19, brace-expansion 5.0.7, http-cache-semantics, glob, minimatch). Cargo.lock: rustls 0.23.41 (fix 0.23.45), glib 0.18.5 (fix 0.20.0). apps/api/Dockerfile: stages base (node:24-alpine + corepack pnpm@9) → deps (full install--filter=@tessera/api...) → builder (prisma generate, nest build) → runnerFROM basecopying node_modules from deps and the generated.prismadirectory from a path that contains the resolved peer versions (@prisma+client@6.19.3_prisma@6.19.3_typescript@5.9.3__typescript@5.9.3); CMDsh apps/api/scripts/migrate-and-start.sh(usesapps/api/node_modules/.bin/prisma migrate deployandnode apps/api/dist/main.js).apps/api/package.jsonhaspostinstall: test -f prisma/schema.prisma && prisma generate || true. Runtime needs: express is loaded throughcreateRequirefrom the copy of@nestjs/platform-express(cert-json-body.ts);apps/api/scripts/rls-preflight.mjsimports@prisma/client; compose healthchecks use busyboxwget(api only; the web service has none in docker-compose.prod.yml).apps/web/Dockerfilerunner already starts fromnode:24-alpinewith the standalone output. No guide tells anyone to run npm, npx or pnpm inside a container.- Crypto:
decryptsplitsiv:authTag:ciphertext, builds the tag withBuffer.from(hex)and callscreateDecipheriv('aes-256-gcm', key, iv)withoutauthTagLength. Verified with Node 24.16: a real tag truncated to 4 bytes still decrypts (only a DEP0182 deprecation warning). Every value ever written used a 12-byte IV and the default 16-byte tag (original implementation9ec6313, unchanged since). Both crypto files failbiome checktoday on formatting only. apps/api/src/cert-manager/zip-expand.tsreads ZIP entries in memory (getEntriesplus its own capped inflate) and never writes entries to disk.- CHANGELOG: „## Unveröffentlicht“ with „### Neu“, „### Geändert“, „### Behoben“; an existing „Behoben“ bullet starts with „Sicherheit: “. Rules (Entwicklungsanleitung „Änderungsliste“): plain language, „Sie“, real umlauts, no file names, no commit ids, no unexplained jargon.
- Guides:
docs/README.mdis the index (table of four guides plus paragraphs „Daneben liegt …“, „Ebenfalls dabei: …“).docs/anleitung-betrieb.md: intro paragraph, TOC with ten chapters, Kapitel 7 „Protokolle und Fehlersuche“ (~394), Kapitel 8 „Abgrenzung zur CI/CD-Pipeline“ (~424), Kapitel 9 with „### Eine Version freigeben“ (~511: step 1 „Änderungsliste abschließen“, then merge/tag commands; „Das Freigeben erledigt Claude“) and „### Woran Sie erkennen, welche Version läuft“ (/health/version).docs/anleitung-entwicklung.md: TOC 1–9, „## Tests“ (~781, table of Wächter-Tests), „## Konventionen und Fallstricke“ (~837, paragraphs „Titel (quick-id): …“). - e2e smoke scripts (local stack, test values only, no
.envreads):.planning/quick/261008-w5w-modul-changelog-und-modulversionen-nacht/e2e/e2e-changelog.sh,.planning/quick/261008-who-eigene-module-beim-start-vorladen/e2e/e2e-preload-api.sh,.planning/quick/261009-ikt-cert-manager-umbau-mehrere-dateien-zip-f/e2e/e2e-cert.sh all(adm-zip, multer, node-forge, undici SSRF guard, thebuildJSON reader), Nextcloud:.planning/quick/261008-mzu-modul-nextcloud-dateien-eigenstaendiger-/e2e/{nc-test-setup.sh,e2e-files.sh,e2e-transfer.sh}and.planning/quick/261009-dkv-modul-dateien-etappe-2a-teilen-von-datei/e2e/e2e-shares.sh all(undici transport, Next.js proxy); the harnesse2e-lib.shin the mzu e2e directory providese2e_login,e2e_wait_healthand friends. Mail:POST /auth/request-resetwith{ "email": … }is public and sends through the API's mail path; mailhog answers onhttp://localhost:8025/api/v2/messages(currently 0 messages). Stack: db, api :3001, web :3000, mailhog and the containertessera-nc-testrun;docker compose up -d --build --wait api webrebuilds and waits. - Git: commit only the task's files (
git addnew files, thengit commit -m "…" -- {every file of the task}), German subject with scopequick-261009-p0m(for exampleci(quick-261009-p0m): …), body ends withCo-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>. Never push (the user bundles pushes), never deploy, never read.envfiles.biome checkthe files you changed; where it fails only on pre-existing formatting,biome format --writeexactly those files.
Write .gitea/scripts/security-scan.sh (POSIX sh, executable, ASCII German comments) implementing D-04 and D-14 to D-18. Header comment in the style of publish-images.sh: quick id, purpose („nur Bericht, bricht nie ab“), modes, environment variables (GITHUB_REF, SCAN_IMAGES, SECURITY_REPORT_DIR, SECURITY_TOOL_DIR), local call examples, and that the script knows and prints no secret. Modes: install, run, all (install then run) and --print-plan (D-17, no network). Do not use set -e; every tool runs in its own guarded function, every non-zero tool exit is expected and swallowed, and the script ends with exit 0 in every mode. The script never writes into the current directory except the report directory (default security-reports, made absolute) and uses mktemp -d for its work directory (removed at the end).
Install (D-16): per binary tool, download with curl -fsSL --retry 2 into {tooldir}, compare sha256sum with the literal from D-16 before extracting, keep the verified archive and verify it again on every later run before reuse, extract only the binary into {tooldir}/{tool}-{version}/; a failed download or a mismatch prints SECURITY-SUMMARY {tool} skipped reason=download|checksum and the run continues. Semgrep: pipx install semgrep==1.180.0 with PIPX_HOME/PIPX_BIN_DIR below {tooldir}, reused when semgrep --version reports 1.180.0; no pipx → skipped. pnpm: corepack pnpm@9.15.0 with COREPACK_HOME below {tooldir}, or a pnpm 9.15.x on PATH; neither → skipped.
Run: export the git safe.directory for the scan root (D-15), export HEAD with git archive HEAD into {work}/src, then: gitleaks in git mode over the full history of the checkout with --config .gitleaks.toml --redact --no-banner --exit-code 0 and a JSON report; pnpm audit --prod --json inside the export; osv-scanner scan source with --lockfile for pnpm-lock.yaml and apps/desktop/src-tauri/Cargo.lock of the export, JSON; semgrep scan inside the export with --config p/default --config p/typescript --config p/nodejs --config p/secrets --config p/dockerfile --metrics=off --json and a per-file --timeout (never --error); trivy fs over the export with --scanners vuln,misconfig,secret --exit-code 0, skipping the Zertifikatsmanager fixture directory (D-19), cache {tooldir}/trivy-cache; trivy image --image-src docker --scanners vuln,secret --exit-code 0 for each image of the plan — refs/heads/main → localhost:3002/schalli/tessera-ctl/api:beta and …/web:beta, refs/tags/v* → …:live, SCAN_IMAGES (space-separated) overrides, any other ref → none. Report files: gitleaks.json, pnpm-audit-prod.json, osv-scanner.json, semgrep.json, trivy-fs.json, trivy-image-api.json, trivy-image-web.json (image file names from the last path segment before the colon). Afterwards delete {tooldir}/trivy-cache/fanal. One inline python3 -I helper reads the JSON files and prints exactly the D-17 lines (a missing or unreadable report → that tool's skipped line), writes them to {reportdir}/summary.txt, and the final line SECURITY-SUMMARY fertig (nur Bericht, Exit 0); then print where the reports are (directory, CI artifact sicherheitsberichte).
Write .gitleaks.toml per D-19: [extend] useDefault = true, then [[allowlists]] entries with German description (one for the fixture directory as a path; one with targetRules = ["private-key"] for the three cert spec files and the ikt research note; one with targetRules = ["generic-api-key"] for the proxmox ServerForm test and the 12-VALIDATION table; one for de.json with targetRules = ["generic-api-key"] plus a regexes entry matching only the verified line and regexTarget = "line"; one with targetRules = ["curl-auth-user"] for the mzu research note). All non-directory paths are anchored regexes with escaped dots. Read the finding lines from baseline/gitleaks-history.json (redacted) to write them; never copy a matched value anywhere. If the host run over the current history shows a finding that is not one of the 20 baseline findings, inspect it: a real secret is reported to the orchestrator and not allowlisted; a verified false positive gets the same narrow treatment.
Write .semgrepignore per D-19.
Edit .gitea/workflows/ci.yml: add one header comment line (quick-261009-p0m: Job security …, nur Bericht, nach publish, nie blockierend) and the job security named Sicherheitspruefung (nur Bericht) exactly as D-18 describes; leave every other job byte-for-byte unchanged.
Edit docs/ci-cd-setup.md: in §4 update the job count sentence to five jobs, add the list item starting with 5. **security** -- (after publish, main and v* only, nur Bericht, never blocks, no secrets), extend the „Ablauf:“ sentence, add a subsection ### Job security: Sicherheitspruefung (nur Bericht) (what it scans, D-15 export, pinned tools and how to update a version: new version plus the SHA256 from the official checksum file, where the summary lines and the artifact appear, roughly five to eight minutes after publish, Trivy cache in the toolcache); add a §6 entry for a red or yellow security job (never affects images or releases; read the skipped reasons). Keep that document's ae/oe/ue spelling.
Commit these files (ci(quick-261009-p0m): Sicherheitspruefung als reiner Bericht in der Pipeline). Then validate against the committed state and add fix-up commits if something fails:
(1) Full run in the real runner image with a canary: git clone -q --no-local . {tmp}/src; inside that clone only, add canary/notiz.txt with a fake GitHub token generated at run time (ghp_ followed by 36 random letters and digits from /dev/urandom) and commit it there; create a throwaway docker volume and run gitea/runner-images:ubuntu-latest with --network gitea, -v /var/run/docker.sock:/var/run/docker.sock, the throwaway volume at /opt/hostedtoolcache, the clone at /w as working directory and GITHUB_REF=refs/heads/main, executing sh .gitea/scripts/security-scan.sh all; echo rc=$?. Write only the SECURITY-SUMMARY lines and the rc= line to checks/task1-runner-full.txt (the token never goes into any file outside the throwaway clone). Expected: rc=0, gitleaks findings=1 and trivy-fs secrets=1 (only the canary), counted lines for every other tool including both images (this proves research assumption A1: the job container scans images of the host daemon). Remove the root-owned report directory inside the clone with a short container run, delete the clone and the throwaway volume.
(2) Host install: sh .gitea/scripts/security-scan.sh install on the dev host, so gitleaks, trivy and osv-scanner exist in ${XDG_CACHE_HOME:-$HOME/.cache}/tessera-security for later tasks (semgrep is skipped there).
(3) Append to baseline/SUMMARY.txt a block „nach Ausnahmelisten (2026-10-09, Task 1)“ with the numbers of the canary run minus the canary hits (inspect the canary run's JSON to subtract exactly the canary findings of gitleaks, Trivy and Semgrep) — gitleaks 0, trivy-fs secrets 0, Semgrep after .semgrepignore, the image counts — and the remark that raw JSON stays out of git.
Commit the evidence and summary files with the task's files (git add -f is not needed: checks/ is not ignored).
Q=.planning/quick/261009-p0m-sicherheitsprotokoll-und-ci-sicherheitsp && TD="${XDG_CACHE_HOME:-$HOME/.cache}/tessera-security" && node -e 'const fs=require("fs"),p=require("path");const d=fs.readdirSync("node_modules/.pnpm").find(x=>/^js-yaml@4./.test(x));if(!d)throw new Error("js-yaml fehlt");const y=require(p.resolve("node_modules/.pnpm",d,"node_modules/js-yaml"));const j=y.load(fs.readFileSync(".gitea/workflows/ci.yml","utf8")).jobs;if(Object.keys(j).join()!=="quality,test,desktop,publish,security")throw new Error("Jobs: "+Object.keys(j));const s=j.security;if(s.needs!=="publish"||s["continue-on-error"]!==true)throw new Error("needs/continue-on-error");if(!String(s.if).includes("refs/heads/main")||!String(s.if).includes("refs/tags/v"))throw new Error("if");if(/secrets./.test(JSON.stringify(s)))throw new Error("secrets im Job");for(const k of ["quality","test","desktop","publish"])if([].concat(j[k].needs||[]).includes("security"))throw new Error("haengt an security: "+k);const runs=s.steps.filter(x=>x.run);if(!runs.length||runs.some(x=>!/|| true$/.test(x.run.trim())))throw new Error("run ohne || true");if(!runs.some(x=>x.run.includes("security-scan.sh all")))throw new Error("Skript fehlt");const co=s.steps.find(x=>String(x.uses||"").startsWith("actions/checkout@"));if(!co||!co.with||co.with["fetch-depth"]!==0)throw new Error("fetch-depth");const up=s.steps.find(x=>String(x.uses||"").startsWith("actions/upload-artifact@v3"));if(!up||up["continue-on-error"]!==true)throw new Error("Artefakt");console.log("ci.yml ok")' && sh -n .gitea/scripts/security-scan.sh && GITHUB_REF=refs/heads/main sh .gitea/scripts/security-scan.sh --print-plan | grep -qx "scan-image localhost:3002/schalli/tessera-ctl/api:beta" && GITHUB_REF=refs/heads/main sh .gitea/scripts/security-scan.sh --print-plan | grep -qx "scan-image localhost:3002/schalli/tessera-ctl/web:beta" && GITHUB_REF=refs/tags/v9.9.9 sh .gitea/scripts/security-scan.sh --print-plan | grep -qx "scan-image localhost:3002/schalli/tessera-ctl/api:live" && GITHUB_REF=refs/heads/live sh .gitea/scripts/security-scan.sh --print-plan | grep -q "^scan-image keine" && python3 -I -c 'import tomllib,re,sys;c=tomllib.load(open(".gitleaks.toml","rb"));al=c.get("allowlists",[]);fx="apps/api/src/cert-manager/fixtures/";bad=[p for a in al for p in a.get("paths",[]) if fx not in p and not (a.get("targetRules") and re.search(r"\.[A-Za-z0-9]+$$",p))];sys.exit(0 if c.get("extend",{}).get("useDefault") is True and al and all(a.get("description") and a.get("paths") for a in al) and not bad else 1)' && "$TD/gitleaks-8.30.1/gitleaks" git --config .gitleaks.toml --redact --no-banner --exit-code 1 . && "$TD/trivy-0.75.0/trivy" --version | grep -q "0.75.0" && "$TD/osv-scanner-2.6.0/osv-scanner" --version | grep -q "2.6.0" && git check-ignore -q "$Q/baseline/osv-scanner.json" && git check-ignore -q security-reports/probe.json && grep -q "^security-reports" .dockerignore && test -f .semgrepignore && O=$(mktemp -d) && git clone -q --no-local . "$O/src" && docker run --rm --network none -v "$O/src:/w:ro" -w /w -e GITHUB_REF=refs/heads/main gitea/runner-images:ubuntu-latest sh -c "SECURITY_REPORT_DIR=/tmp/r SECURITY_TOOL_DIR=/tmp/t sh .gitea/scripts/security-scan.sh all; echo rc=$?" > "$O/offline.txt" 2>&1; grep -qx "rc=0" "$O/offline.txt" && test "$(grep -c "^SECURITY-SUMMARY .* skipped reason=" "$O/offline.txt")" -ge 7 && C="$Q/checks/task1-runner-full.txt" && grep -qx "rc=0" "$C" && grep -qx "SECURITY-SUMMARY gitleaks findings=1" "C" && grep -Eq "^SECURITY-SUMMARY trivy-fs .*secrets=1( |)" "$C" && for t in pnpm-audit-prod osv-scanner semgrep trivy-image-api trivy-image-web; do grep -Eq "^SECURITY-SUMMARY $t [a-z_]+=[0-9]+" "$C" || exit 1; done && grep -qF '5. security' docs/ci-cd-setup.md && grep -q "nach Ausnahmelisten" "$Q/baseline/SUMMARY.txt" && rm -rf "$O" && echo "task1 ok"
<fails_when>ci.yml does not parse or the security job is not the fifth job, lacks needs publish, the main/tag condition, job-level continue-on-error, fetch-depth 0, the run step ending in || true, or the best-effort artifact step, references a secret, or another job depends on it; the script has a syntax error or the ref plan lists wrong images (main → beta, tag → live, live branch → none); .gitleaks.toml lacks useDefault, a description, or allowlists a non-fixture path without a rule or not as a single anchored file; gitleaks over the full history still finds something; the host tool binaries are missing; raw baseline JSON or security-reports are not ignored; the offline run in the runner image exits non-zero or reports fewer than seven skipped tools; the canary run did not exit 0, did not find exactly the canary in gitleaks and Trivy, or lacks a counted line for any other tool or image; ci-cd-setup.md lacks job 5; SUMMARY.txt lacks the after-allowlist block</fails_when>
The job security exists after publish (main and v* only), never gates anything and has no secrets; one script installs pinned, checksum-verified tools, scans only the committed state and the history, prints the D-17 lines and always exits 0; narrow allowlists bring gitleaks to 0 on the real history while a planted fake token is found; proven in gitea/runner-images:ubuntu-latest including the image scans through the host daemon and an offline run; tools installed on the host; docs/ci-cd-setup.md describes the job; baseline JSON ignored; committed on main, not pushed.
Create .planning/quick/261009-p0m-sicherheitsprotokoll-und-ci-sicherheitsp/checks/link-check.py (stdlib only, run as python3 -I {path} from the repository root): for docs/sicherheitsprotokoll.md, docs/README.md, docs/anleitung-betrieb.md, docs/anleitung-entwicklung.md and docs/ci-cd-setup.md it checks every relative Markdown link that lives in the protocol, or points to sicherheitsprotokoll.md, or is the anchor #sicherheitsprüfungen: the target file exists and an anchor matches a heading of the target under GitHub's slug rule (lower case, characters other than word characters, hyphen and space removed, spaces to hyphens); prints the broken ones and exits 1 if any.
Links (D-02): docs/README.md gets a paragraph „Ebenfalls dabei: Sicherheitsprotokoll …“ (for owner and customers, what it contains, kept current). docs/anleitung-betrieb.md: one sentence with the link in the intro and a short paragraph in Kapitel 8 about the automatic security check (after publish, report only, never blocks, numbers in the run log, details in the CI runbook and the protocol).
Maintenance rule (D-03): docs/anleitung-entwicklung.md gets a new section ## Sicherheitsprüfungen between „## Tests“ and „## Konventionen und Fallstricke“ (TOC entry 9 with the anchor #sicherheitsprüfungen, Konventionen becomes 10) with ### So pflegen Sie dieses Protokoll (every security review, every scan worth recording — first scan, after fixing findings, when the pipeline's numbers change noticeably — and every ZAP run before a release adds a „Verlauf“ entry with date, what was checked, numbers, what was fixed or accepted; „Auf einen Blick“ and „Einordnung der Befunde“ are updated in the same change; every „offen“ or „bewusst akzeptiert“ needs a reason; the protocol never contains secret values or attack details), ### Die Prüfung in der Pipeline (job security, D-15 to D-18 in plain words, how to update a tool version with its SHA256), ### Prüfungen von Hand wiederholen (sh .gitea/scripts/security-scan.sh all from the repository root; it scans only the committed state; SCAN_IMAGES for local images; reports in security-reports/), ### Ausnahmelisten (the D-19 rules: only verified false positives, narrowest form, German description, never whole directories outside the fixture folders).
CHANGELOG (D-13, D-21): under „## Unveröffentlicht“ → „### Neu“ one bullet starting with „Sicherheit: “ that names the Sicherheitsprotokoll (all checks so far plus the first full check, kept current) and the new automatic check on every build that only reports and never stops a build — no file names, no tool jargon without explanation.
Run python3 -I on the link checker, gitleaks dir on the protocol, and commit (docs(quick-261009-p0m): Sicherheitsprotokoll mit Bestandsaufnahme und erster Vollpruefung).
P=docs/sicherheitsprotokoll.md && TD="${XDG_CACHE_HOME:-$HOME/.cache}/tessera-security" && test -f "$P" && for h in "## Auf einen Blick" "## So lesen Sie dieses Protokoll" "## Wie die Prüfungen arbeiten" "## Bisherige Sicherheitsprüfungen" "## Erste vollständige Prüfung am 9. Oktober 2026" "## Einordnung der Befunde" "## Verlauf"; do grep -qxF "$h" "$P" || { echo "fehlt: $h"; exit 1; }; done && for k in "Phase 07" "Phase 08" "Phase 16" "Phase 18" "261008-dts" "261008-mzu" "261009-dkv" "261009-ikt" "WINDOWS" "gitleaks" "pnpm audit" "osv-scanner" "Semgrep" "Trivy" "xlsx" "node-forge" "bewusst akzeptiert" "offen" "behoben" "Fehlalarm"; do grep -qF "$k" "$P" || { echo "fehlt: $k"; exit 1; }; done && python3 -I -c 'import re,sys;t=open("docs/sicherheitsprotokoll.md",encoding="utf-8").read();low=t.lower();du=re.search(r"\b(du|dein|deine|deinen|deinem|deiner|dich)\b",t,re.I);ten=[l for l in low.splitlines() if "mandant" in l and re.search(r"offen|geplant|später|etappe",l)];sys.exit(1 if du or ten or "lizenz" in low or " Sie " not in t else 0)' && "$TD/gitleaks-8.30.1/gitleaks" dir "$P" --no-banner --redact --exit-code 1 && grep -qF "](sicherheitsprotokoll.md)" docs/README.md && grep -qF "sicherheitsprotokoll.md" docs/anleitung-betrieb.md && grep -qF "sicherheitsprotokoll.md" docs/anleitung-entwicklung.md && grep -qxF "## Sicherheitsprüfungen" docs/anleitung-entwicklung.md && grep -qxF "### So pflegen Sie dieses Protokoll" docs/anleitung-entwicklung.md && grep -qF "(#sicherheitsprüfungen)" docs/anleitung-entwicklung.md && python3 -I -c 'import sys;t=open("CHANGELOG.md",encoding="utf-8").read().split("\n");i=t.index("## Unveröffentlicht");j=next(k for k in range(i+1,len(t)) if t[k].startswith("## "));sec=t[i+1:j];hs={l[4:].strip() for l in sec if l.startswith("### ")};sys.exit(0 if hs.issubset({"Neu","Geändert","Behoben"}) and any(l.startswith("- Sicherheit: ") and "Sicherheitsprotokoll" in l for l in sec) else 1)' && python3 -I .planning/quick/261009-p0m-sicherheitsprotokoll-und-ci-sicherheitsp/checks/link-check.py && echo "task2 ok"
<fails_when>the protocol is missing, lacks one of the seven headings, one of the eight review references, the ledger, a scanner name, the xlsx or node-forge rows or the status words; it addresses the reader informally, mentions licensing, or frames multi-organisation operation as open or planned; gitleaks finds a secret pattern in it; README, Betriebs- or Entwicklungsanleitung do not link it; the Entwicklungsanleitung lacks the section, the maintenance subsection or the TOC anchor; the CHANGELOG lacks the „Sicherheit:“ bullet naming the protocol or gains a fourth group heading; the link checker reports a broken link or anchor</fails_when>
docs/sicherheitsprotokoll.md exists with inventory, baseline, explanations and a complete triage table; README, Betriebs- and Entwicklungsanleitung link it; the Entwicklungsanleitung tells how to keep it current; CHANGELOG carries the security bullet; links checked; committed on main, not pushed.
Local proof before touching alpha: write checks/zap-testserver.py (stdlib http.server, run with python3 -I; options port, log file, require-auth): / links to /login and /info, /login contains a POST form with user, password and a submit button, every request is logged as method, path and whether an Authorization header was present; with require-auth every request without the header gets 401. Run it on the host (bind 0.0.0.0) and the script against it with ZAP_SPIDER_MINUTES=1 (target via the docker bridge gateway, or ZAP_DOCKER_NETWORK=host with 127.0.0.1 if the bridge cannot reach the host): (1) without auth → write lauf-ohne-anmeldung POST={n} GET={n}; (2) with require-auth and a scratch credentials file containing a dummy test value → write lauf-mit-anmeldung POST={n} auth_ja={n} auth_nein={n} (the single unauthenticated request is the script's own first preflight). Both lines go to checks/task3-zap-local.txt; expected POST=0 in both, at least two GETs, at least two authorised requests and at most one without the header. Stop the server, delete the scratch files.
First run against alpha (D-06): note alpha's version first (curl -s https://alpha.tessera.ctl.de/api-proxy/health/version; if that path does not answer, write „Version nicht abgefragt“), then sh .gitea/scripts/zap-baseline.sh. Write checks/zap-alpha-2026-10-09.txt with date and time, the version line, the ZAP-SUMMARY line and the alert lines only (raw reports stay in the gitignored report directory). Append a ZAP line to baseline/SUMMARY.txt.
Protocol: add the subsection ### Prüfung von außen vor einer Freigabe (OWASP ZAP) under „Wie die Prüfungen arbeiten“ (it looks at alpha like a visitor without an account: start and login page, headers, cookies, delivered files; passive only, submits no forms, attacks nothing; run before every release; Basic Auth in front of alpha stays as it is and the check runs from the internal dev host); add the section ## Prüfung von außen gegen alpha before „## Verlauf“ with a table of runs (Datum | Version | Hoch | Mittel | Niedrig | Info) and a plain explanation of each alert type of the first run; add a row per alert type of risk Niedrig or higher (informational ones in one row) to „Einordnung der Befunde“ with status „offen“ plus assessment, or „bewusst akzeptiert“ plus reason — this task changes no Tessera code and no proxy configuration; add a ZAP row to the baseline table, update „Auf einen Blick“, add a „Verlauf“ entry for the first ZAP run.
Betriebsanleitung (D-05): in Kapitel 9 „### Eine Version freigeben“ add the step „Prüfung von außen“ before merging and tagging: Claude runs sh .gitea/scripts/zap-baseline.sh from the dev host once alpha runs the beta state that is to be released (check the version line), records the result in the protocol („Verlauf“ entry, new findings into „Einordnung der Befunde“), and a new finding of risk „Hoch“ is fixed before the release or justified in the protocol; Basic Auth stays.
Entwicklungsanleitung: add ### Prüfung von außen (ZAP) to „## Sicherheitsprüfungen“ (command, environment variables, outputs, why no AJAX spider and no form submission, the Basic-Auth file route for the case that alpha ever asks this host for a login).
CHANGELOG: extend the Task-2 „Sicherheit:“ bullet (or add one under „Neu“) so it says that alpha is checked from the outside before every release, passively and without attacks („von außen“ in the text).
Run the link checker, commit (ci(quick-261009-p0m): ZAP-Grundpruefung gegen alpha vor Freigaben, erster Lauf).
Q=.planning/quick/261009-p0m-sicherheitsprotokoll-und-ci-sicherheitsp && sh -n .gitea/scripts/zap-baseline.sh && python3 -I -c 'import ast;ast.parse(open(".gitea/scripts/zap-hooks.py").read())' && PL="$(sh .gitea/scripts/zap-baseline.sh --print-plan)" && printf "%s\n" "$PL" | grep -qF "ghcr.io/zaproxy/zaproxy@sha256:7aaa659b0d43078febd82e29bad112285c370727e86ab8340444220e17d9f0d2" && printf "%s\n" "$PL" | grep -qF "https://alpha.tessera.ctl.de" && printf "%s\n" "$PL" | grep -qF "zap-baseline.py" && printf "%s\n" "$PL" | grep -qF -- "--autooff" && printf "%s\n" "$PL" | grep -qF -- "--hook=" && ! printf "%s\n" "PL" | grep -Eq "(^| )-j( |)" && grep -qF "set_option_process_form" .gitea/scripts/zap-hooks.py && grep -qF "set_option_post_form" .gitea/scripts/zap-hooks.py && L="$Q/checks/task3-zap-local.txt" && grep -Eq "^lauf-ohne-anmeldung POST=0 GET=([2-9]|[1-9][0-9]+)$" "$L" && grep -Eq "^lauf-mit-anmeldung POST=0 auth_ja=([2-9]|[1-9][0-9]+) auth_nein=[01]$" "$L" && A="$Q/checks/zap-alpha-2026-10-09.txt" && grep -Eq "^ZAP-SUMMARY ziel=alpha.tessera.ctl.de hoch=[0-9]+ mittel=[0-9]+ niedrig=[0-9]+ info=[0-9]+" "$A" && grep -qxF "## Prüfung von außen gegen alpha" docs/sicherheitsprotokoll.md && grep -qxF "### Prüfung von außen vor einer Freigabe (OWASP ZAP)" docs/sicherheitsprotokoll.md && awk "/^### Eine Version freigeben/{f=1;next}/^### /{f=0}f" docs/anleitung-betrieb.md | grep -qF "zap-baseline.sh" && grep -qxF "### Prüfung von außen (ZAP)" docs/anleitung-entwicklung.md && test ! -e "$Q/baseline/zap-hook-basic-auth.py" && grep -qi "zap" "$Q/baseline/SUMMARY.txt" && git check-ignore -q security-reports/zap-probe/zap.json && python3 -I -c 'import re,sys;fs=[".gitea/scripts/zap-baseline.sh","docs/anleitung-betrieb.md","docs/sicherheitsprotokoll.md","docs/anleitung-entwicklung.md"];bad=[f for f in fs if re.search(r"basic.?auth[^\n]*(entfern|abschalt|deaktivier|ausschalt)",open(f,encoding="utf-8").read(),re.I)];print(bad);sys.exit(1 if bad else 0)' && python3 -I -c 'import sys;t=open("CHANGELOG.md",encoding="utf-8").read().split("\n");i=t.index("## Unveröffentlicht");j=next(k for k in range(i+1,len(t)) if t[k].startswith("## "));sec=t[i+1:j];sys.exit(0 if any(l.startswith("- Sicherheit: ") and "von außen" in l for l in sec) else 1)' && python3 -I "$Q/checks/link-check.py" && echo "task3 ok"
<fails_when>a script does not parse; the plan output lacks the pinned digest, the alpha default, zap-baseline.py, --autooff or the hook, or contains the AJAX spider option; the hook does not switch off form processing and POSTs; the local proof shows a POST, fewer than two GETs, fewer than two authorised requests or more than one request without the header; the alpha evidence lacks the ZAP-SUMMARY line; the protocol lacks the ZAP subsection or the run section; Kapitel 9 „Eine Version freigeben“ does not contain the step; the Entwicklungsanleitung lacks the ZAP subsection; the old research hook still exists; SUMMARY.txt has no ZAP line; ZAP reports are not ignored; any of the four files words Basic Auth as something to take away; the CHANGELOG bullet lacks the outside check; a link or anchor is broken</fails_when>
Optional for the user: open the HTML report of the first run in security-reports/zap-…/zap.html on the dev host and compare it with the protocol's plain explanation.
A passive ZAP baseline script with a hook that never submits forms exists, proven locally (0 POST, Basic-Auth fallback through a file); the first run against alpha is recorded with counts, explanation and triage in the protocol; Kapitel 9 lists it as a release step and Basic Auth stays untouched; Entwicklungsanleitung and CHANGELOG updated; committed on main, not pushed.
<threat_model>
Trust Boundaries
| Boundary | Description |
|---|---|
| CI job / dev host → GitHub releases, PyPI, ghcr.io (Trivy DB), osv.dev, npm registry, semgrep.dev | downloaded tools, databases and rules are untrusted until verified |
| security job container → host docker daemon | the job sees every image and could start containers (pre-existing for every job via the socket mount) |
| scan reports / logs / artifact → Gitea readers | reports may echo matched secret material or internal paths |
| developer working tree → scanners | the working tree holds untracked private material that must never be scanned or reported |
| dev host (ZAP container) → alpha (internet-facing via NPM, Basic Auth for outside sources) | outbound HTTP against a real server with real data |
| protocol (docs) → owner and later customers | published text about weaknesses |
| dependency resolution (pnpm overrides, cargo update) → shipped images and desktop packages | changed third-party code enters production |
| database → CryptoService.decrypt | stored ciphertext is only as trustworthy as database write access |
STRIDE Threat Register
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|---|---|---|---|---|---|
| T-p0m-01 | Tampering | scanner downloads (security-scan.sh install) | high | mitigate | exact versions, SHA256 literals in the script compared before every use including cached archives (D-16); mismatch → tool skipped; no marketplace actions for scanners; Task 1 proves the offline/skip path |
| T-p0m-02 | Elevation of Privilege | security job with the host docker socket | medium | accept | the job gets no secret (gate parses the job for any secret expression), runs after publish so it cannot alter published images, runs only pinned binaries; the socket exposure itself pre-exists for every job and is documented in docs/ci-cd-setup.md §5 |
| T-p0m-03 | Information Disclosure | reports, log lines, artifact, evidence files | medium | mitigate | gitleaks --redact; log lines and evidence carry counts and status words only (D-17, D-25); raw JSON only in gitignored security-reports/ and the 30-day artifact; baseline JSON ignored (D-12); the canary token exists only inside a throwaway clone |
| T-p0m-04 | Information Disclosure | private untracked material in the working tree | high | mitigate | scanners read a git archive HEAD export and the git history only (D-15); validation runs on fresh clones; the material is never named |
| T-p0m-05 | Repudiation / Tampering | allowlists masking real secrets | high | mitigate | narrowest allowlists (D-19) with a structural tomllib gate (only the fixture directory as a directory, everything else single anchored files plus rules); canary proves gitleaks and Trivy still detect a new token; unknown new findings are triaged, real ones reported, never allowlisted |
| T-p0m-06 | Denial of Service | CI duration and runner disk | medium | mitigate | job only on main and v*, after publish, never in any needs; Trivy layer cache deleted after each run; tool cache reused; research images pruned by name in Task 6, disk level reported |
| T-p0m-07 | Denial of Service / Tampering | ZAP against alpha | medium | mitigate | passive baseline only, no AJAX spider, spider without form processing and POST (proven locally: POST=0), spider minutes capped, -T 15, default target alpha only; the run creates no data on alpha |
| T-p0m-08 | Information Disclosure | Basic-Auth credentials for the ZAP fallback | high | mitigate | used only after a 401, read from a file outside the repository, passed through temporary 0600 files removed by trap, never on a command line or in output; Basic Auth in front of alpha stays untouched (D-05) |
| T-p0m-09 | Tampering / Spoofing | CryptoService.decrypt with a truncated GCM tag (forgery with a short tag) | medium | mitigate | tag length exactly 16 bytes checked plus authTagLength: 16 (D-09); specs prove 4-byte, 17-byte and tampered tags are rejected |
| T-p0m-10 | Tampering / Elevation of Privilege | known vulnerabilities in dependencies (Next.js RCE/SSRF, proxy-addr, handlebars, multer, nodemailer, undici …) | high | mitigate | in-major bumps, overrides within majors, removal of the two unused packages, rustls patch; audit before/after with 0 critical in production as gate (Task 4) |
| T-p0m-11 | Denial of Service (availability) | api runtime image change breaks migrate-and-start | high | mitigate | start against a fresh database with all migrations, rebuilt stack, e2e and mail smoke; revert-and-document fallback (D-10, D-23) |
| T-p0m-12 | Information Disclosure | published protocol | medium | mitigate | no secret values, no advisory text or exploit details, no internal IP addresses or credentials; gitleaks dir check on the document in Tasks 2 and 6 |
| T-p0m-13 | Elevation of Privilege | package managers and development tools inside runtime containers | low | mitigate | removed from both runtime stages, gate checks their absence (Task 5) |
| T-p0m-14 | Tampering | ci.yml change breaking every push | medium | mitigate | YAML parsed and structurally checked (job order, needs, condition, no gating), other jobs unchanged; the first real CI run is a checklist item for the orchestrator/user |
| T-p0m-SC | Tampering | npm/pip/cargo installs | high | mitigate | no new direct dependency (only version bumps of packages already in pnpm-lock.yaml and Cargo.lock, overrides on names already present, two removals); every new transitive name must be declared by an updated parent (npm view) and is listed in the SUMMARY, otherwise the bump is reverted; semgrep pinned via pipx in a throwaway container; scanner binaries SHA256-pinned; research Package Legitimacy Audit: no package added |
| </threat_model> |
| Source item | Covered by |
|---|---|
| GOAL: Sicherheitsprotokoll + CI-Sicherheitsprüfungen + Behebung der Baseline-Befunde | Tasks 1–6 |
| D-01 one protocol: inventory, baseline, how checks work, plain German „Sie“ | Task 2 (Task 3 ZAP part, Tasks 4–6 updates) |
| D-02 links from README, Betriebs- and Entwicklungsanleitung | Task 2 |
| D-03 maintenance rule „So pflegen Sie dieses Protokoll“ | Task 2 (applied in Tasks 3–6) |
| D-04 CI job gitleaks history, audit/osv, Semgrep, Trivy fs + images; report only; log lines + artifact; pinned, checksum-verified | Task 1 |
| D-05 ZAP script, release step in Kapitel 9, Basic Auth stays | Task 3 |
| D-06 first ZAP run against alpha, passive, from the dev host, recorded | Task 3 |
| D-07 resting product topics not framed as open | Task 2 (wording gate), Task 6 (re-check) |
| D-08 (a) in-major updates, overrides, no majors, audit before/after, gates green | Task 4 |
| D-09 (b) AES-GCM 16-byte tag with spec | Task 5 Part A |
| D-10 (c) api image without dev dependencies or documented open item | Task 5 Part B |
| D-11 (d) no-fix items and accepted choices documented, .gitleaks.toml for false positives | Task 1 (allowlist), Task 2 (triage), Task 6 (final statuses) |
| D-12 raw JSON out of git, small summary file | Task 1 (baseline/.gitignore, SUMMARY.txt), Tasks 3/6 (appended) |
| D-13 CHANGELOG, Betriebs-, Entwicklungsanleitung, README; no module bumps | Tasks 2–5 |
| D-14 – D-26 planner decisions | Tasks 1 (14–19, 25), 3 (20), 2–5 (21), 4 (22, 24, 26), 5 (23) |
| RESEARCH: pinned stack, direct binaries instead of marketplace actions | Task 1 (D-16) |
| RESEARCH: job after publish, continue-on-error, ` | |
| RESEARCH: bind-mount trap, Trivy cache growth, never gating | Task 1 (binaries in the job container, fanal deleted, needs check) |
| RESEARCH: ignore files (.gitleaks.toml, .semgrepignore) | Task 1 (D-19) |
| RESEARCH: pitfalls rate limits, timing (main/v* only, skip live), noise, raw JSON size, non-technical audience | Tasks 1, 2 |
| RESEARCH: baseline numbers per scanner | Task 2 (table), Task 1 (after-allowlist numbers), Task 6 (after-fix numbers) |
| RESEARCH: dependency triage P1/P2, accept/monitor, dev-only image hygiene | Task 4 (P1/P2), Task 5 (image hygiene), Task 2 (accept/monitor rows) |
| RESEARCH: Semgrep triage (gcm true positive, TLS bypasses, false positives, CI hygiene, workspace hardening) | Task 5 (gcm), Task 2 (all rows) |
| RESEARCH: inventory (8 reviews, threat models, data-separation tests, other quick tasks, WINDOWS ledger, security tests, no SECURITY.md files) | Task 2 |
RESEARCH: ZAP command, options, exit codes, -z ignored, hook fallback, uid 1000 report directory |
Task 3 (D-20) |
| RESEARCH: Dockerfile HEALTHCHECK DS-0026, Cargo.lock findings | Task 2 (rows), Task 4 (rustls) |
| RESEARCH assumptions A1 (image scan via host socket) | Task 1 full run in the runner image |
| RESEARCH assumptions A2, A3, A4 (toolcache persistence, job colour, artifact) | SUMMARY checklist for the first CI run |
| RESEARCH assumption A5 (pnpm version for workspace hardening) | Task 2 (stated only if verified) |
| RESEARCH assumption A6 (reason against marketplace actions) | D-16 (direct binaries route) |
| RESEARCH assumption A7 (15.5.27 highest 15.x) | verified at planning with npm view |
| RESEARCH assumption A8 (spider submits no forms) | Task 3 hook plus local POST=0 proof |
| RESEARCH open question 1 (xlsx / node-forge) | Task 2 (offen with reasons) |
| RESEARCH open question 2 (fix now or record) | decided by the orchestrator (D-08 – D-10), Tasks 4–5 |
| RESEARCH open question 3 (weekly scheduled run) | not planned: the research marks it as not needed now; the SUMMARY names it for the orchestrator |
| Out of scope: licensing, multi-organisation operation as open topic, password-leak or rotation advice, any deploy or docker action on the test server | not planned |
<success_criteria>
- Job security runs after publish on main and v* only, never fails or delays anything, has no secrets, uses pinned and SHA256-verified tools, scans only committed content and history, and reports counts as SECURITY-SUMMARY lines plus a best-effort artifact — proven in the real runner image.
- gitleaks over the full history reports 0 with narrow allowlists, while a planted fake token is still detected.
- docs/sicherheitsprotokoll.md documents all security work so far, the first full scan, the outside check of alpha and the state after the fixes in plain German with „Sie“; every finding has a status with a reason; README, Betriebs- and Entwicklungsanleitung link it and the Entwicklungsanleitung explains how to keep it current.
- The ZAP baseline against alpha is a scripted, passive release step in Kapitel 9 and its first run is recorded; Basic Auth stays.
- Dependencies fixed within their majors (0 critical in production per pnpm audit, fewer high), AES-GCM tag length enforced, runtime images without development tooling (or documented offen); all suites, type check, lint, cargo, rebuilt stack, e2e and mail smoke green.
- CHANGELOG „Unveröffentlicht“ carries the „Sicherheit:“ bullets; no module version changed; commits on main, nothing pushed, nothing deployed. </success_criteria>