diff --git a/.kaizen/agents/custodian-codex/supervision.json b/.kaizen/agents/custodian-codex/supervision.json index 33c566f1..3af230dc 100644 --- a/.kaizen/agents/custodian-codex/supervision.json +++ b/.kaizen/agents/custodian-codex/supervision.json @@ -1215,6 +1215,73 @@ "result": "succeeded after founder explicitly selected attended admin kubeconfig; custom-columns-file equivalent query; original digest not executed" } ] + }, + { + "proposal_id": "SYNC-FORGE-READONLY-PREFLIGHT-20261101", + "original_revision": 1, + "original_digest_sha256": "b6a0abfdc98996d5ac2cc0438a7128866255e50c76b584b452bbae3cb1b86816", + "proposal_ref": "docs/changes/2026-11-01-scanner-json-digest/read-only-preflight-proposal.json", + "disposition": "unscored", + "outcome": "succeeded", + "target": "railiance01 forgejo/forge; existing public scanner artifacts and metadata only", + "expected_result": "selected public artifact/config/guard and node allocation receipt for exact rollout preparation", + "verification_ref": "docs/changes/2026-11-01-scanner-json-digest/read-only-preflight-outcome.json", + "rollback_ref": "not applicable; read-only", + "cost_estimate_eur": 0, + "risk_estimate_eur": 0, + "supervisor": "Bernd Worsch", + "approved_revision": null, + "approved_digest_sha256": null, + "executed_revision": 1, + "executed_digest_sha256": "b6a0abfdc98996d5ac2cc0438a7128866255e50c76b584b452bbae3cb1b86816", + "execution_ref": "Founder-returned selected preflight output and local receipt, 2026-11-02", + "refinement_or_rescue": false, + "promotion_evidence": false, + "verified_outcome": "Expected artifact/template identities and allocation returned; no native mutation; no signed approval evidence backfilled" + }, + { + "proposal_id": "SYNC-SCANNER-JSON-DIGEST-20261101", + "original_revision": 1, + "original_digest_sha256": "75bc6e4bf1a98444cd844306bf8f3f81aca1e31b1609081905fd42ed1b76ae42", + "proposal_ref": "docs/changes/2026-11-01-scanner-json-digest/proposal.json", + "current_revision": 2, + "current_digest_sha256": "d22b1f555af96c203b7d68efd25e80815ce18ca4e3bf0dd3ab96b6f699d50237", + "current_proposal_ref": "docs/changes/2026-11-01-scanner-json-digest/native-r2/PLAN.md", + "disposition": "pending", + "outcome": "not_executed", + "target": "railiance01 forgejo/forge; paired repo-manager/railiance-forge/Apps source", + "expected_result": "Equivalent JSON filename/exact lowercase SHA256 accepted; unchanged full scanner coverage and actual-key refusal", + "verification_ref": "docs/changes/2026-11-01-scanner-json-digest/native-r2/test-validation.json", + "rollback_ref": "docs/changes/2026-11-01-scanner-json-digest/native-r2/PLAN.md", + "cost_estimate_eur": { + "incremental_infrastructure": 0, + "attended_minutes": [ + 15, + 30 + ], + "founder_time": "unpriced" + }, + "risk_estimate_eur": null, + "risk_estimate": "Expected Recreate interruption 1-3 minutes; illustrative one recovery hour EUR100; complete scanner-policy loss unpriced and requires founder judgement. No autopilot grant.", + "supervisor": "Bernd Worsch", + "approved_revision": null, + "approved_digest_sha256": null, + "executed_revision": null, + "executed_digest_sha256": null, + "refinement_or_rescue": true, + "promotion_evidence": false, + "revisions": [ + { + "revision": 1, + "digest_sha256": "75bc6e4bf1a98444cd844306bf8f3f81aca1e31b1609081905fd42ed1b76ae42", + "basis": "paired source candidate only; original preserved" + }, + { + "revision": 2, + "digest_sha256": "d22b1f555af96c203b7d68efd25e80815ce18ca4e3bf0dd3ab96b6f699d50237", + "basis": "founder preflight enables exact native preparation and six source followthrough files; offline fixture refinement retained" + } + ] } ], "trials": [ diff --git a/docs/changes/2026-11-01-scanner-json-digest/README.md b/docs/changes/2026-11-01-scanner-json-digest/README.md index f82dc7aa..4093d1d1 100644 --- a/docs/changes/2026-11-01-scanner-json-digest/README.md +++ b/docs/changes/2026-11-01-scanner-json-digest/README.md @@ -8,6 +8,10 @@ The existing reviewed YAML filename/digest exception rejects the equivalent JSON Validation: 12 local real-Gitleaks tests passed (37 unrelated tests deselected); full receiver `FORGE_TEST_GITLEAKS=... go test -count=1 ./...` passed in 150.087s. A prior Go invocation without that environment passed only scanner-independent tests and was superseded by this real-scanner run. -Native preparation is pending read-only inventory. Current Kubernetes identity `ops-mason-builder` cannot list nodes, deployments or pods in `forgejo`. Do not borrow the founder's admin kubeconfig. The prepared `forge-metadata-proposal.json` uses gated-executor's exact-action format for only names, UIDs and images. Its digest is `sha256:0fceb2dc3c1963a16f40a0221b2eb6cb5384b91e2fe8607bb0121afe47a5d2e1`. The founder was asked to return this metadata or provide the supervised execution result. A native update needs its own exact inspected rollout proposal and supervisor approval under `docs/agent-autonomy-decision.md` and environment-orientation ยง4. +The founder returned the read-only artifact/config/guard/headroom receipt on 2026-11-02. It verifies the expected deployment, pod, node, old adapter, hook and pinned Gitleaks. CPU requests are 68%, memory requests 76%, and Forgejo uses Recreate. The original failed/refined metadata attempts remain recorded; the successful read-only execution grants no mutation authority. + +The exact native preparation is now [revision 2](native-r2/PLAN.md), under the same proposal ID; revision 1 remains unchanged. Approval packet SHA256: `d22b1f555af96c203b7d68efd25e80815ce18ca4e3bf0dd3ab96b6f699d50237`. It includes guarded stage/rollout/rollback, both scanner source changes and Apps source followthrough. Thirteen isolated filesystem/real receive cases and five controller tests passed. The initial missing temporary dispatcher fixture was retained as an unverified preparation attempt and replaced with an explicit offline fixture; native probes separately copy the live dispatcher. Live server dry-run, diff, probes and activation remain mandatory execution checks and have not run yet. + +Native activation awaits exact founder approval under `docs/agent-autonomy-decision.md` ("The supervisor approves that proposal or performs it through the privileged execution path."). The agent must not borrow the founder admin kubeconfig. The prepared founder launcher is `/tmp/forge-scanner-rollout.sh`. Its sealed runner refuses unknown initial identity/guard/hook/source state and any packet drift. The staged scanner in repo-manager and live receiver have not been altered by this candidate. Independent clean-file commits continue under the original policy. diff --git a/docs/changes/2026-11-01-scanner-json-digest/native-approval-pending.json b/docs/changes/2026-11-01-scanner-json-digest/native-approval-pending.json new file mode 100644 index 00000000..3b97c062 --- /dev/null +++ b/docs/changes/2026-11-01-scanner-json-digest/native-approval-pending.json @@ -0,0 +1,8 @@ +{ + "decision_id": "5410707c-ce73-4f80-9c03-805102c31c58", + "status": "open", + "proposal_id": "SYNC-SCANNER-JSON-DIGEST-20261101", + "revision": 2, + "packet_sha256": "d22b1f555af96c203b7d68efd25e80815ce18ca4e3bf0dd3ab96b6f699d50237", + "approval": null +} diff --git a/docs/changes/2026-11-01-scanner-json-digest/native-r2/PLAN.md b/docs/changes/2026-11-01-scanner-json-digest/native-r2/PLAN.md new file mode 100644 index 00000000..c1f188d3 --- /dev/null +++ b/docs/changes/2026-11-01-scanner-json-digest/native-r2/PLAN.md @@ -0,0 +1,37 @@ +# Exact JSON filename/digest correction, revision 2 + +Proposal: `SYNC-SCANNER-JSON-DIGEST-20261101`, revision 2. Revision 1 remains preserved in the parent directory. This revision adds executable native preparation to the already-tested paired profile correction. No native mutation or source-policy activation has occurred. + +The founder's read-only preflight verified deployment `forgejo/forge` UID `1faefb26-cccd-4d0e-b509-70a7eb0b4f4d`, pod UID `2c9f911a-403e-4183-a027-2b8d48742348`, node UID `0659e49e-6539-480a-8b0f-2969dd88cb05`, and the exact previous adapter, Gitleaks and hook hashes. CPU requests were 68% and memory requests 76%. Strategy is Recreate; main container requests remain 200m/1104Mi and memory limit 1536Mi. + +## Action and scope + +The existing YAML filename/exact lowercase 64-hex exception gains its quoted JSON equivalent in both local and receiver adapters. It still applies only to generic-api-key matches whose filename has one of the existing extensions. It does not adjudicate UUID resource identities, public fingerprints, natural-language findings, other digest formats, or every held Custodian artifact. + +1. Recheck six owner-source preimages, deployment/node/pod identities, existing guard and selected ConfigMap, and every current repository's hook inventory. Refuse unknown hook bytes, symlinks, missing scanner fragments or more than 500 repositories. Snapshot protected hook hashes and permissions. The initial pod identity must still match; drift requires new preparation/review. +2. Server dry-run the new immutable ConfigMap and conditional deployment patch, and capture client ConfigMap diff without printing raw objects or errors. The receipt lists the exact two deployment fields being changed. These server checks are prepared; they have **not** been run against the live cluster yet. +3. Stage binary `d7dbd0ef57312ab9507874bb5fd67c49bef637b6d7dee121e526227307af4bf0` in `/data/security/receive-secret-scan/20261102-d7dbd0ef5731`; copy the unchanged pinned Gitleaks from the verified old directory. Check owner, hash, non-symlink and executable modes. Retain every previous directory. +4. Before activation, run a disposable tmpfs receive-pack probe with the staged binary, pinned real Gitleaks and a copy of an actual current dispatcher: accept the JSON digest; reject an intermediate synthetic private key even after removal from the tip; discard quarantine; reject unavailable scanner; accept clean recovery. No hosted repository is used for probe writes. +5. Create immutable `forgejo-secret-template-619d94208139`. Atomically replace only each repository's scanner fragment, keeping permissions/group and protected dispatcher/authorization hooks. Append the new artifact guard to all existing guards and select the new hook ConfigMap with UID/resourceVersion/before-value JSON-patch tests. No image, resource, strategy, credential or Git object changes. +6. Wait up to 180 seconds for the current deployment's Ready pod; reconcile known old hooks on any repositories created during the transition. Verify protected hooks, mounted template hash and full artifact guard. Repeat the isolated receive probe after activation. Retain a sanitized local receipt at `/tmp/forge-scanner-rollout-receipt.json`. +7. After native verification, `source_followthrough.py` applies the six exact reviewed source changes: two repo-manager files, two railiance-forge files, and Apps Helm values/immutable ConfigMap. It checks all preimages before writing and requires this packet's successful native receipt. The local pre-commit hook directly imports the repo-manager source, so this paired source update activates its identical local profile. Commit and publish these sources through normal hooks, then retry eligible held publication and audit remaining failures separately. + +`packet.json` seals executable scripts, selected inputs, source diffs/candidates, tests and this plan. Its SHA256 is the approval digest; the runner rechecks every sealed file. `--approved-digest` is an acknowledgement for founder execution, not a credential boundary or agent-issued approval. The agent must not use the founder kubeconfig. Native execution is attended by the founder or an independently authorized exact privileged execution path. No owner-agent approval has been invented; the founder reviews the paired owner source in this packet. + +## Verification and recovery + +The original paired candidate passed 12 local real-Gitleaks tests and the full receiver suite. This native preparation passed 13 isolated filesystem/real receive cases and 5 controller tests. Tests cover staging/hash failure, symlinks, unchanged protected hooks, complete-scope refusal before writing, partial hook failure restoration, unknown-drift preservation, conditional patch roundtrip and recovery after a lost patch response. The offline dispatcher is an explicit scanner-only fixture; native execution separately copies and tests a real current dispatcher, excluding the authorization engine's native invocation as documented by the probe. + +Any exception after staging starts is recorded as failure and attempts guarded rollback, including an uncertain patch response. Rollback accepts only the exact known old or proposed guard/template pair. It restores the old deployment reference/guard, waits up to 180 seconds, restores only known old/new scanner fragments, verifies all preserved hooks, rechecks the mounted old template and runs the old clean/private-key/quarantine/recovery probe. Unknown concurrent changes are retained and reported as unverified rather than overwritten. Old artifacts are never deleted. New immutable artifacts/ConfigMap are retained for evidence; a retry requires preserving/reviewing the prior receipt. Separate rollback mode is prepared in the same runner. A failed rollback requires founder recovery; automatic recovery is not guaranteed. + +The controller does not coordinate unrelated native administrators or freeze repository creation. It catches known old hooks again after rollout and verifies template inheritance for future repositories. Concurrent destructive repository or protected-hook changes fail verification. All scans retain complete introduced-object/history scope and existing limits; no suppression switch or raw finding values are introduced. + +## Cost, interruption and residual risk + +No new service, image pull, infrastructure commitment or increased resource request is proposed. Estimated attended execution is 15โ€“30 minutes; local probes use existing compute. Monetary infrastructure increment is EUR 0; founder time is unpriced. Recreate causes an expected 1โ€“3 minute Git-hosting interruption, with up to another 3 minutes for readiness rollback, plus bounded probe/command time. This is an estimate, not an availability guarantee. + +Modelled recovery effort is up to one hour (illustratively EUR 100 at EUR 100/hour); there is no defensible complete EUR loss bound for a scanner exception or failed native recovery. The security consequence remains subject to explicit supervisor judgement. A real credential deliberately shaped as an approved filename/digest is indistinguishable by this exception alone; this limitation already exists for the YAML form and extends to its JSON representation. Other rules and provider/private-key negative tests remain active. No autopilot budget, risk grant, promotion or scored approval follows from this preparation. + +## Founder execution after exact approval + +The convenience launcher `/tmp/forge-scanner-rollout.sh` pins this packet's SHA256, explicitly selects the attended admin context, and runs `apply`. Review the plan, six source diffs and runner before execution. Return the sanitized receipt/output. Source followthrough and publication then continue under the same exact approved scope. Do not rerun an uncertain apply; preserve its receipt and inspect or use the reviewed rollback. diff --git a/docs/changes/2026-11-01-scanner-json-digest/native-r2/after-hook b/docs/changes/2026-11-01-scanner-json-digest/native-r2/after-hook new file mode 100644 index 00000000..755dface --- /dev/null +++ b/docs/changes/2026-11-01-scanner-json-digest/native-r2/after-hook @@ -0,0 +1,4 @@ +#!/bin/sh +set +vx +set -eu +exec /data/security/receive-secret-scan/20261102-d7dbd0ef5731/receive-secret-scan --scanner /data/security/receive-secret-scan/20261102-d7dbd0ef5731/gitleaks diff --git a/docs/changes/2026-11-01-scanner-json-digest/native-r2/apps-source-changes.json b/docs/changes/2026-11-01-scanner-json-digest/native-r2/apps-source-changes.json new file mode 100644 index 00000000..d97755e9 --- /dev/null +++ b/docs/changes/2026-11-01-scanner-json-digest/native-r2/apps-source-changes.json @@ -0,0 +1,16 @@ +[ + { + "target": "/home/worsch/railiance-apps/helm/forgejo-values.yaml", + "original_sha256": "910bb0e79a515ed411beded29fbfc8cd0ea26e09d4c3ff5fcaf5bdcd9f3e46b1", + "candidate_sha256": "4bd65a32c97c9aa75f7fb08025b463c3acf060e82ac32f56feab58f1193cbd7d", + "candidate_file": "forgejo-values.yaml", + "patch": "--- /home/worsch/railiance-apps/helm/forgejo-values.yaml\n+++ /home/worsch/railiance-apps/helm/forgejo-values.yaml\n@@ -54,7 +54,7 @@\n extraVolumes:\n - name: secret-scan-template\n configMap:\n- name: forgejo-secret-template-d1df801be39f\n+ name: forgejo-secret-template-619d94208139\n defaultMode: 365\n extraContainerVolumeMounts:\n - &id001\n@@ -74,7 +74,7 @@\n command:\n - sh\n - -c\n- - \"#!/bin/sh\\nset +vx\\nset -eu\\ntest \\\"$(id -u)\\\" = 1000\\ntest -d /data && test ! -L /data\\ntest -d /data/security && test ! -L /data/security\\ntest -d /data/security/receive-secret-scan && test ! -L /data/security/receive-secret-scan\\ntest -d /data/security/receive-secret-scan/20261012-57a0642b6251 && test ! -L /data/security/receive-secret-scan/20261012-57a0642b6251\\ntest -f /data/security/receive-secret-scan/20261012-57a0642b6251/receive-secret-scan && test ! -L /data/security/receive-secret-scan/20261012-57a0642b6251/receive-secret-scan\\ntest \\\"$(stat -c %u /data/security/receive-secret-scan/20261012-57a0642b6251/receive-secret-scan)\\\" = 1000\\ntest \\\"$(sha256sum /data/security/receive-secret-scan/20261012-57a0642b6251/receive-secret-scan | cut -d \\\" \\\" -f 1)\\\" = 57a0642b625105440fe146fceb4d244fd2dd08a75beb316831675643e6cc27ec\\ncase \\\"$(stat -c %a /data/security/receive-secret-scan/20261012-57a0642b6251/receive-secret-scan)\\\" in 755|775) ;; *) exit 21;; esac\\ntest -f /data/security/receive-secret-scan/20261012-57a0642b6251/gitleaks && test ! -L /data/security/receive-secret-scan/20261012-57a0642b6251/gitleaks\\ntest \\\"$(stat -c %u /data/security/receive-secret-scan/20261012-57a0642b6251/gitleaks)\\\" = 1000\\ntest \\\"$(sha256sum /data/security/receive-secret-scan/20261012-57a0642b6251/gitleaks | cut -d \\\" \\\" -f 1)\\\" = 88f91962aa2f93ac6ab281d553b9e125f5197bbbce38f9f2437f7299c32e5509\\ncase \\\"$(stat -c %a /data/security/receive-secret-scan/20261012-57a0642b6251/gitleaks)\\\" in 755|775) ;; *) exit 21;; esac\\nif [ \\\"${1:-check}\\\" = apply ]; then\\n chmod 755 /data/security/receive-secret-scan/20261012-57a0642b6251/receive-secret-scan\\n test \\\"$(stat -c %a /data/security/receive-secret-scan/20261012-57a0642b6251/receive-secret-scan)\\\" = 755\\n chmod 755 /data/security/receive-secret-scan/20261012-57a0642b6251/gitleaks\\n test \\\"$(stat -c %a /data/security/receive-secret-scan/20261012-57a0642b6251/gitleaks)\\\" = 755\\nfi\\n\\n#!/bin/sh\\nset +vx\\nset -eu\\ntest \\\"$(id -u)\\\" = 1000\\ntest -d /data && test ! -L /data\\ntest -d /data/security && test ! -L /data/security\\ntest -d /data/security/receive-secret-scan && test ! -L /data/security/receive-secret-scan\\ntest -d /data/security/receive-secret-scan/20261018-73ee04078786 && test ! -L /data/security/receive-secret-scan/20261018-73ee04078786\\ntest -f /data/security/receive-secret-scan/20261018-73ee04078786/receive-secret-scan && test ! -L /data/security/receive-secret-scan/20261018-73ee04078786/receive-secret-scan\\ntest \\\"$(stat -c %u /data/security/receive-secret-scan/20261018-73ee04078786/receive-secret-scan)\\\" = 1000\\ntest \\\"$(sha256sum /data/security/receive-secret-scan/20261018-73ee04078786/receive-secret-scan | cut -d \\\" \\\" -f 1)\\\" = 73ee04078786801fc74d80d2bef122b7764c93856e403e8de5b078336d7f9e1a\\ncase \\\"$(stat -c %a /data/security/receive-secret-scan/20261018-73ee04078786/receive-secret-scan)\\\" in 755|775) ;; *) exit 21;; esac\\ntest -f /data/security/receive-secret-scan/20261018-73ee04078786/gitleaks && test ! -L /data/security/receive-secret-scan/20261018-73ee04078786/gitleaks\\ntest \\\"$(stat -c %u /data/security/receive-secret-scan/20261018-73ee04078786/gitleaks)\\\" = 1000\\ntest \\\"$(sha256sum /data/security/receive-secret-scan/20261018-73ee04078786/gitleaks | cut -d \\\" \\\" -f 1)\\\" = 88f91962aa2f93ac6ab281d553b9e125f5197bbbce38f9f2437f7299c32e5509\\ncase \\\"$(stat -c %a /data/security/receive-secret-scan/20261018-73ee04078786/gitleaks)\\\" in 755|775) ;; *) exit 21;; esac\\nif [ \\\"${1:-check}\\\" = apply ]; then\\n chmod 755 /data/security/receive-secret-scan/20261018-73ee04078786/receive-secret-scan\\n test \\\"$(stat -c %a /data/security/receive-secret-scan/20261018-73ee04078786/receive-secret-scan)\\\" = 755\\n chmod 755 /data/security/receive-secret-scan/20261018-73ee04078786/gitleaks\\n test \\\"$(stat -c %a /data/security/receive-secret-scan/20261018-73ee04078786/gitleaks)\\\" = 755\\nfi\\n\\n#!/bin/sh\\nset +vx\\nset -eu\\ntest \\\"$(id -u)\\\" = 1000\\ntest -d /data && test ! -L /data\\ntest -d /data/security && test ! -L /data/security\\ntest -d /data/security/receive-secret-scan && test ! -L /data/security/receive-secret-scan\\ntest -d /data/security/receive-secret-scan/20261027-b735d052a49f && test ! -L /data/security/receive-secret-scan/20261027-b735d052a49f\\ntest -f /data/security/receive-secret-scan/20261027-b735d052a49f/receive-secret-scan && test ! -L /data/security/receive-secret-scan/20261027-b735d052a49f/receive-secret-scan\\ntest \\\"$(stat -c %u /data/security/receive-secret-scan/20261027-b735d052a49f/receive-secret-scan)\\\" = 1000\\ntest \\\"$(sha256sum /data/security/receive-secret-scan/20261027-b735d052a49f/receive-secret-scan | cut -d \\\" \\\" -f 1)\\\" = b735d052a49fc8e9cca14cddee074c5429352ad71841eb6548459294f15683a2\\ncase \\\"$(stat -c %a /data/security/receive-secret-scan/20261027-b735d052a49f/receive-secret-scan)\\\" in 755|775) ;; *) exit 21;; esac\\ntest -f /data/security/receive-secret-scan/20261027-b735d052a49f/gitleaks && test ! -L /data/security/receive-secret-scan/20261027-b735d052a49f/gitleaks\\ntest \\\"$(stat -c %u /data/security/receive-secret-scan/20261027-b735d052a49f/gitleaks)\\\" = 1000\\ntest \\\"$(sha256sum /data/security/receive-secret-scan/20261027-b735d052a49f/gitleaks | cut -d \\\" \\\" -f 1)\\\" = 88f91962aa2f93ac6ab281d553b9e125f5197bbbce38f9f2437f7299c32e5509\\ncase \\\"$(stat -c %a /data/security/receive-secret-scan/20261027-b735d052a49f/gitleaks)\\\" in 755|775) ;; *) exit 21;; esac\\nif [ \\\"${1:-check}\\\" = apply ]; then\\n chmod 755 /data/security/receive-secret-scan/20261027-b735d052a49f/receive-secret-scan\\n test \\\"$(stat -c %a /data/security/receive-secret-scan/20261027-b735d052a49f/receive-secret-scan)\\\" = 755\\n chmod 755 /data/security/receive-secret-scan/20261027-b735d052a49f/gitleaks\\n test \\\"$(stat -c %a /data/security/receive-secret-scan/20261027-b735d052a49f/gitleaks)\\\" = 755\\nfi\\n\"\n+ - \"#!/bin/sh\\nset +vx\\nset -eu\\ntest \\\"$(id -u)\\\" = 1000\\ntest -d /data && test ! -L /data\\ntest -d /data/security && test ! -L /data/security\\ntest -d /data/security/receive-secret-scan && test ! -L /data/security/receive-secret-scan\\ntest -d /data/security/receive-secret-scan/20261012-57a0642b6251 && test ! -L /data/security/receive-secret-scan/20261012-57a0642b6251\\ntest -f /data/security/receive-secret-scan/20261012-57a0642b6251/receive-secret-scan && test ! -L /data/security/receive-secret-scan/20261012-57a0642b6251/receive-secret-scan\\ntest \\\"$(stat -c %u /data/security/receive-secret-scan/20261012-57a0642b6251/receive-secret-scan)\\\" = 1000\\ntest \\\"$(sha256sum /data/security/receive-secret-scan/20261012-57a0642b6251/receive-secret-scan | cut -d \\\" \\\" -f 1)\\\" = 57a0642b625105440fe146fceb4d244fd2dd08a75beb316831675643e6cc27ec\\ncase \\\"$(stat -c %a /data/security/receive-secret-scan/20261012-57a0642b6251/receive-secret-scan)\\\" in 755|775) ;; *) exit 21;; esac\\ntest -f /data/security/receive-secret-scan/20261012-57a0642b6251/gitleaks && test ! -L /data/security/receive-secret-scan/20261012-57a0642b6251/gitleaks\\ntest \\\"$(stat -c %u /data/security/receive-secret-scan/20261012-57a0642b6251/gitleaks)\\\" = 1000\\ntest \\\"$(sha256sum /data/security/receive-secret-scan/20261012-57a0642b6251/gitleaks | cut -d \\\" \\\" -f 1)\\\" = 88f91962aa2f93ac6ab281d553b9e125f5197bbbce38f9f2437f7299c32e5509\\ncase \\\"$(stat -c %a /data/security/receive-secret-scan/20261012-57a0642b6251/gitleaks)\\\" in 755|775) ;; *) exit 21;; esac\\nif [ \\\"${1:-check}\\\" = apply ]; then\\n chmod 755 /data/security/receive-secret-scan/20261012-57a0642b6251/receive-secret-scan\\n test \\\"$(stat -c %a /data/security/receive-secret-scan/20261012-57a0642b6251/receive-secret-scan)\\\" = 755\\n chmod 755 /data/security/receive-secret-scan/20261012-57a0642b6251/gitleaks\\n test \\\"$(stat -c %a /data/security/receive-secret-scan/20261012-57a0642b6251/gitleaks)\\\" = 755\\nfi\\n\\n#!/bin/sh\\nset +vx\\nset -eu\\ntest \\\"$(id -u)\\\" = 1000\\ntest -d /data && test ! -L /data\\ntest -d /data/security && test ! -L /data/security\\ntest -d /data/security/receive-secret-scan && test ! -L /data/security/receive-secret-scan\\ntest -d /data/security/receive-secret-scan/20261018-73ee04078786 && test ! -L /data/security/receive-secret-scan/20261018-73ee04078786\\ntest -f /data/security/receive-secret-scan/20261018-73ee04078786/receive-secret-scan && test ! -L /data/security/receive-secret-scan/20261018-73ee04078786/receive-secret-scan\\ntest \\\"$(stat -c %u /data/security/receive-secret-scan/20261018-73ee04078786/receive-secret-scan)\\\" = 1000\\ntest \\\"$(sha256sum /data/security/receive-secret-scan/20261018-73ee04078786/receive-secret-scan | cut -d \\\" \\\" -f 1)\\\" = 73ee04078786801fc74d80d2bef122b7764c93856e403e8de5b078336d7f9e1a\\ncase \\\"$(stat -c %a /data/security/receive-secret-scan/20261018-73ee04078786/receive-secret-scan)\\\" in 755|775) ;; *) exit 21;; esac\\ntest -f /data/security/receive-secret-scan/20261018-73ee04078786/gitleaks && test ! -L /data/security/receive-secret-scan/20261018-73ee04078786/gitleaks\\ntest \\\"$(stat -c %u /data/security/receive-secret-scan/20261018-73ee04078786/gitleaks)\\\" = 1000\\ntest \\\"$(sha256sum /data/security/receive-secret-scan/20261018-73ee04078786/gitleaks | cut -d \\\" \\\" -f 1)\\\" = 88f91962aa2f93ac6ab281d553b9e125f5197bbbce38f9f2437f7299c32e5509\\ncase \\\"$(stat -c %a /data/security/receive-secret-scan/20261018-73ee04078786/gitleaks)\\\" in 755|775) ;; *) exit 21;; esac\\nif [ \\\"${1:-check}\\\" = apply ]; then\\n chmod 755 /data/security/receive-secret-scan/20261018-73ee04078786/receive-secret-scan\\n test \\\"$(stat -c %a /data/security/receive-secret-scan/20261018-73ee04078786/receive-secret-scan)\\\" = 755\\n chmod 755 /data/security/receive-secret-scan/20261018-73ee04078786/gitleaks\\n test \\\"$(stat -c %a /data/security/receive-secret-scan/20261018-73ee04078786/gitleaks)\\\" = 755\\nfi\\n\\n#!/bin/sh\\nset +vx\\nset -eu\\ntest \\\"$(id -u)\\\" = 1000\\ntest -d /data && test ! -L /data\\ntest -d /data/security && test ! -L /data/security\\ntest -d /data/security/receive-secret-scan && test ! -L /data/security/receive-secret-scan\\ntest -d /data/security/receive-secret-scan/20261027-b735d052a49f && test ! -L /data/security/receive-secret-scan/20261027-b735d052a49f\\ntest -f /data/security/receive-secret-scan/20261027-b735d052a49f/receive-secret-scan && test ! -L /data/security/receive-secret-scan/20261027-b735d052a49f/receive-secret-scan\\ntest \\\"$(stat -c %u /data/security/receive-secret-scan/20261027-b735d052a49f/receive-secret-scan)\\\" = 1000\\ntest \\\"$(sha256sum /data/security/receive-secret-scan/20261027-b735d052a49f/receive-secret-scan | cut -d \\\" \\\" -f 1)\\\" = b735d052a49fc8e9cca14cddee074c5429352ad71841eb6548459294f15683a2\\ncase \\\"$(stat -c %a /data/security/receive-secret-scan/20261027-b735d052a49f/receive-secret-scan)\\\" in 755|775) ;; *) exit 21;; esac\\ntest -f /data/security/receive-secret-scan/20261027-b735d052a49f/gitleaks && test ! -L /data/security/receive-secret-scan/20261027-b735d052a49f/gitleaks\\ntest \\\"$(stat -c %u /data/security/receive-secret-scan/20261027-b735d052a49f/gitleaks)\\\" = 1000\\ntest \\\"$(sha256sum /data/security/receive-secret-scan/20261027-b735d052a49f/gitleaks | cut -d \\\" \\\" -f 1)\\\" = 88f91962aa2f93ac6ab281d553b9e125f5197bbbce38f9f2437f7299c32e5509\\ncase \\\"$(stat -c %a /data/security/receive-secret-scan/20261027-b735d052a49f/gitleaks)\\\" in 755|775) ;; *) exit 21;; esac\\nif [ \\\"${1:-check}\\\" = apply ]; then\\n chmod 755 /data/security/receive-secret-scan/20261027-b735d052a49f/receive-secret-scan\\n test \\\"$(stat -c %a /data/security/receive-secret-scan/20261027-b735d052a49f/receive-secret-scan)\\\" = 755\\n chmod 755 /data/security/receive-secret-scan/20261027-b735d052a49f/gitleaks\\n test \\\"$(stat -c %a /data/security/receive-secret-scan/20261027-b735d052a49f/gitleaks)\\\" = 755\\nfi\\n\\n#!/bin/sh\\nset +vx\\nset -eu\\ntest \\\"$(id -u)\\\" = 1000\\ntest -d /data && test ! -L /data\\ntest -d /data/security && test ! -L /data/security\\ntest -d /data/security/receive-secret-scan && test ! -L /data/security/receive-secret-scan\\ntest -d /data/security/receive-secret-scan/20261102-d7dbd0ef5731 && test ! -L /data/security/receive-secret-scan/20261102-d7dbd0ef5731\\ntest -f /data/security/receive-secret-scan/20261102-d7dbd0ef5731/receive-secret-scan && test ! -L /data/security/receive-secret-scan/20261102-d7dbd0ef5731/receive-secret-scan\\ntest \\\"$(stat -c %u /data/security/receive-secret-scan/20261102-d7dbd0ef5731/receive-secret-scan)\\\" = 1000\\ntest \\\"$(sha256sum /data/security/receive-secret-scan/20261102-d7dbd0ef5731/receive-secret-scan | cut -d \\\" \\\" -f 1)\\\" = d7dbd0ef57312ab9507874bb5fd67c49bef637b6d7dee121e526227307af4bf0\\ncase \\\"$(stat -c %a /data/security/receive-secret-scan/20261102-d7dbd0ef5731/receive-secret-scan)\\\" in 755|775) ;; *) exit 21;; esac\\nif [ \\\"${1:-check}\\\" = apply ]; then chmod 755 /data/security/receive-secret-scan/20261102-d7dbd0ef5731/receive-secret-scan; test \\\"$(stat -c %a /data/security/receive-secret-scan/20261102-d7dbd0ef5731/receive-secret-scan)\\\" = 755; fi\\ntest -f /data/security/receive-secret-scan/20261102-d7dbd0ef5731/gitleaks && test ! -L /data/security/receive-secret-scan/20261102-d7dbd0ef5731/gitleaks\\ntest \\\"$(stat -c %u /data/security/receive-secret-scan/20261102-d7dbd0ef5731/gitleaks)\\\" = 1000\\ntest \\\"$(sha256sum /data/security/receive-secret-scan/20261102-d7dbd0ef5731/gitleaks | cut -d \\\" \\\" -f 1)\\\" = 88f91962aa2f93ac6ab281d553b9e125f5197bbbce38f9f2437f7299c32e5509\\ncase \\\"$(stat -c %a /data/security/receive-secret-scan/20261102-d7dbd0ef5731/gitleaks)\\\" in 755|775) ;; *) exit 21;; esac\\nif [ \\\"${1:-check}\\\" = apply ]; then chmod 755 /data/security/receive-secret-scan/20261102-d7dbd0ef5731/gitleaks; test \\\"$(stat -c %a /data/security/receive-secret-scan/20261102-d7dbd0ef5731/gitleaks)\\\" = 755; fi\\n\"\n - sh\n - apply\n securityContext:\n" + }, + { + "target": "/home/worsch/railiance-apps/manifests/forgejo-secret-scan-template.yaml", + "original_sha256": "ca468a0d8847e7a7ed81462e3d9a144c8a370eec0960bf2ba830cafde24509de", + "candidate_sha256": "e91a3405b6524c7dcdfa82d6fd4efc17570e430f833b265892e4f223a19d7689", + "candidate_file": "configmap.json", + "patch": "--- /home/worsch/railiance-apps/manifests/forgejo-secret-scan-template.yaml\n+++ /home/worsch/railiance-apps/manifests/forgejo-secret-scan-template.yaml\n@@ -1,16 +1,16 @@\n-apiVersion: v1\n-kind: ConfigMap\n-metadata:\n- name: forgejo-secret-template-d1df801be39f\n- namespace: forgejo\n- labels:\n- app.kubernetes.io/part-of: forgejo\n- custodian.coulomb.social/workplan: FORGE-WP-ADHOC-2026-10-27\n-immutable: true\n-data:\n- hook: |\n- #!/bin/sh\n- set +vx\n- set -eu\n- exec /data/security/receive-secret-scan/20261027-b735d052a49f/receive-secret-scan --scanner /data/security/receive-secret-scan/20261027-b735d052a49f/gitleaks\n-\n+{\n+ \"apiVersion\": \"v1\",\n+ \"kind\": \"ConfigMap\",\n+ \"metadata\": {\n+ \"name\": \"forgejo-secret-template-619d94208139\",\n+ \"namespace\": \"forgejo\",\n+ \"labels\": {\n+ \"app.kubernetes.io/part-of\": \"forgejo\",\n+ \"custodian.railiance.org/workplan\": \"CUST-WP-0083\"\n+ }\n+ },\n+ \"immutable\": true,\n+ \"data\": {\n+ \"hook\": \"#!/bin/sh\\nset +vx\\nset -eu\\nexec /data/security/receive-secret-scan/20261102-d7dbd0ef5731/receive-secret-scan --scanner /data/security/receive-secret-scan/20261102-d7dbd0ef5731/gitleaks\\n\"\n+ }\n+}\n" + } +] diff --git a/docs/changes/2026-11-01-scanner-json-digest/native-r2/apps-source.patch b/docs/changes/2026-11-01-scanner-json-digest/native-r2/apps-source.patch new file mode 100644 index 00000000..e676ddb2 --- /dev/null +++ b/docs/changes/2026-11-01-scanner-json-digest/native-r2/apps-source.patch @@ -0,0 +1,55 @@ +--- /home/worsch/railiance-apps/helm/forgejo-values.yaml ++++ /home/worsch/railiance-apps/helm/forgejo-values.yaml +@@ -54,7 +54,7 @@ + extraVolumes: + - name: secret-scan-template + configMap: +- name: forgejo-secret-template-d1df801be39f ++ name: forgejo-secret-template-619d94208139 + defaultMode: 365 + extraContainerVolumeMounts: + - &id001 +@@ -74,7 +74,7 @@ + command: + - sh + - -c +- - "#!/bin/sh\nset +vx\nset -eu\ntest \"$(id -u)\" = 1000\ntest -d /data && test ! -L /data\ntest -d /data/security && test ! -L /data/security\ntest -d /data/security/receive-secret-scan && test ! -L /data/security/receive-secret-scan\ntest -d /data/security/receive-secret-scan/20261012-57a0642b6251 && test ! -L /data/security/receive-secret-scan/20261012-57a0642b6251\ntest -f /data/security/receive-secret-scan/20261012-57a0642b6251/receive-secret-scan && test ! -L /data/security/receive-secret-scan/20261012-57a0642b6251/receive-secret-scan\ntest \"$(stat -c %u /data/security/receive-secret-scan/20261012-57a0642b6251/receive-secret-scan)\" = 1000\ntest \"$(sha256sum /data/security/receive-secret-scan/20261012-57a0642b6251/receive-secret-scan | cut -d \" \" -f 1)\" = 57a0642b625105440fe146fceb4d244fd2dd08a75beb316831675643e6cc27ec\ncase \"$(stat -c %a /data/security/receive-secret-scan/20261012-57a0642b6251/receive-secret-scan)\" in 755|775) ;; *) exit 21;; esac\ntest -f /data/security/receive-secret-scan/20261012-57a0642b6251/gitleaks && test ! -L /data/security/receive-secret-scan/20261012-57a0642b6251/gitleaks\ntest \"$(stat -c %u /data/security/receive-secret-scan/20261012-57a0642b6251/gitleaks)\" = 1000\ntest \"$(sha256sum /data/security/receive-secret-scan/20261012-57a0642b6251/gitleaks | cut -d \" \" -f 1)\" = 88f91962aa2f93ac6ab281d553b9e125f5197bbbce38f9f2437f7299c32e5509\ncase \"$(stat -c %a /data/security/receive-secret-scan/20261012-57a0642b6251/gitleaks)\" in 755|775) ;; *) exit 21;; esac\nif [ \"${1:-check}\" = apply ]; then\n chmod 755 /data/security/receive-secret-scan/20261012-57a0642b6251/receive-secret-scan\n test \"$(stat -c %a /data/security/receive-secret-scan/20261012-57a0642b6251/receive-secret-scan)\" = 755\n chmod 755 /data/security/receive-secret-scan/20261012-57a0642b6251/gitleaks\n test \"$(stat -c %a /data/security/receive-secret-scan/20261012-57a0642b6251/gitleaks)\" = 755\nfi\n\n#!/bin/sh\nset +vx\nset -eu\ntest \"$(id -u)\" = 1000\ntest -d /data && test ! -L /data\ntest -d /data/security && test ! -L /data/security\ntest -d /data/security/receive-secret-scan && test ! -L /data/security/receive-secret-scan\ntest -d /data/security/receive-secret-scan/20261018-73ee04078786 && test ! -L /data/security/receive-secret-scan/20261018-73ee04078786\ntest -f /data/security/receive-secret-scan/20261018-73ee04078786/receive-secret-scan && test ! -L /data/security/receive-secret-scan/20261018-73ee04078786/receive-secret-scan\ntest \"$(stat -c %u /data/security/receive-secret-scan/20261018-73ee04078786/receive-secret-scan)\" = 1000\ntest \"$(sha256sum /data/security/receive-secret-scan/20261018-73ee04078786/receive-secret-scan | cut -d \" \" -f 1)\" = 73ee04078786801fc74d80d2bef122b7764c93856e403e8de5b078336d7f9e1a\ncase \"$(stat -c %a /data/security/receive-secret-scan/20261018-73ee04078786/receive-secret-scan)\" in 755|775) ;; *) exit 21;; esac\ntest -f /data/security/receive-secret-scan/20261018-73ee04078786/gitleaks && test ! -L /data/security/receive-secret-scan/20261018-73ee04078786/gitleaks\ntest \"$(stat -c %u /data/security/receive-secret-scan/20261018-73ee04078786/gitleaks)\" = 1000\ntest \"$(sha256sum /data/security/receive-secret-scan/20261018-73ee04078786/gitleaks | cut -d \" \" -f 1)\" = 88f91962aa2f93ac6ab281d553b9e125f5197bbbce38f9f2437f7299c32e5509\ncase \"$(stat -c %a /data/security/receive-secret-scan/20261018-73ee04078786/gitleaks)\" in 755|775) ;; *) exit 21;; esac\nif [ \"${1:-check}\" = apply ]; then\n chmod 755 /data/security/receive-secret-scan/20261018-73ee04078786/receive-secret-scan\n test \"$(stat -c %a /data/security/receive-secret-scan/20261018-73ee04078786/receive-secret-scan)\" = 755\n chmod 755 /data/security/receive-secret-scan/20261018-73ee04078786/gitleaks\n test \"$(stat -c %a /data/security/receive-secret-scan/20261018-73ee04078786/gitleaks)\" = 755\nfi\n\n#!/bin/sh\nset +vx\nset -eu\ntest \"$(id -u)\" = 1000\ntest -d /data && test ! -L /data\ntest -d /data/security && test ! -L /data/security\ntest -d /data/security/receive-secret-scan && test ! -L /data/security/receive-secret-scan\ntest -d /data/security/receive-secret-scan/20261027-b735d052a49f && test ! -L /data/security/receive-secret-scan/20261027-b735d052a49f\ntest -f /data/security/receive-secret-scan/20261027-b735d052a49f/receive-secret-scan && test ! -L /data/security/receive-secret-scan/20261027-b735d052a49f/receive-secret-scan\ntest \"$(stat -c %u /data/security/receive-secret-scan/20261027-b735d052a49f/receive-secret-scan)\" = 1000\ntest \"$(sha256sum /data/security/receive-secret-scan/20261027-b735d052a49f/receive-secret-scan | cut -d \" \" -f 1)\" = b735d052a49fc8e9cca14cddee074c5429352ad71841eb6548459294f15683a2\ncase \"$(stat -c %a /data/security/receive-secret-scan/20261027-b735d052a49f/receive-secret-scan)\" in 755|775) ;; *) exit 21;; esac\ntest -f /data/security/receive-secret-scan/20261027-b735d052a49f/gitleaks && test ! -L /data/security/receive-secret-scan/20261027-b735d052a49f/gitleaks\ntest \"$(stat -c %u /data/security/receive-secret-scan/20261027-b735d052a49f/gitleaks)\" = 1000\ntest \"$(sha256sum /data/security/receive-secret-scan/20261027-b735d052a49f/gitleaks | cut -d \" \" -f 1)\" = 88f91962aa2f93ac6ab281d553b9e125f5197bbbce38f9f2437f7299c32e5509\ncase \"$(stat -c %a /data/security/receive-secret-scan/20261027-b735d052a49f/gitleaks)\" in 755|775) ;; *) exit 21;; esac\nif [ \"${1:-check}\" = apply ]; then\n chmod 755 /data/security/receive-secret-scan/20261027-b735d052a49f/receive-secret-scan\n test \"$(stat -c %a /data/security/receive-secret-scan/20261027-b735d052a49f/receive-secret-scan)\" = 755\n chmod 755 /data/security/receive-secret-scan/20261027-b735d052a49f/gitleaks\n test \"$(stat -c %a /data/security/receive-secret-scan/20261027-b735d052a49f/gitleaks)\" = 755\nfi\n" ++ - "#!/bin/sh\nset +vx\nset -eu\ntest \"$(id -u)\" = 1000\ntest -d /data && test ! -L /data\ntest -d /data/security && test ! -L /data/security\ntest -d /data/security/receive-secret-scan && test ! -L /data/security/receive-secret-scan\ntest -d /data/security/receive-secret-scan/20261012-57a0642b6251 && test ! -L /data/security/receive-secret-scan/20261012-57a0642b6251\ntest -f /data/security/receive-secret-scan/20261012-57a0642b6251/receive-secret-scan && test ! -L /data/security/receive-secret-scan/20261012-57a0642b6251/receive-secret-scan\ntest \"$(stat -c %u /data/security/receive-secret-scan/20261012-57a0642b6251/receive-secret-scan)\" = 1000\ntest \"$(sha256sum /data/security/receive-secret-scan/20261012-57a0642b6251/receive-secret-scan | cut -d \" \" -f 1)\" = 57a0642b625105440fe146fceb4d244fd2dd08a75beb316831675643e6cc27ec\ncase \"$(stat -c %a /data/security/receive-secret-scan/20261012-57a0642b6251/receive-secret-scan)\" in 755|775) ;; *) exit 21;; esac\ntest -f /data/security/receive-secret-scan/20261012-57a0642b6251/gitleaks && test ! -L /data/security/receive-secret-scan/20261012-57a0642b6251/gitleaks\ntest \"$(stat -c %u /data/security/receive-secret-scan/20261012-57a0642b6251/gitleaks)\" = 1000\ntest \"$(sha256sum /data/security/receive-secret-scan/20261012-57a0642b6251/gitleaks | cut -d \" \" -f 1)\" = 88f91962aa2f93ac6ab281d553b9e125f5197bbbce38f9f2437f7299c32e5509\ncase \"$(stat -c %a /data/security/receive-secret-scan/20261012-57a0642b6251/gitleaks)\" in 755|775) ;; *) exit 21;; esac\nif [ \"${1:-check}\" = apply ]; then\n chmod 755 /data/security/receive-secret-scan/20261012-57a0642b6251/receive-secret-scan\n test \"$(stat -c %a /data/security/receive-secret-scan/20261012-57a0642b6251/receive-secret-scan)\" = 755\n chmod 755 /data/security/receive-secret-scan/20261012-57a0642b6251/gitleaks\n test \"$(stat -c %a /data/security/receive-secret-scan/20261012-57a0642b6251/gitleaks)\" = 755\nfi\n\n#!/bin/sh\nset +vx\nset -eu\ntest \"$(id -u)\" = 1000\ntest -d /data && test ! -L /data\ntest -d /data/security && test ! -L /data/security\ntest -d /data/security/receive-secret-scan && test ! -L /data/security/receive-secret-scan\ntest -d /data/security/receive-secret-scan/20261018-73ee04078786 && test ! -L /data/security/receive-secret-scan/20261018-73ee04078786\ntest -f /data/security/receive-secret-scan/20261018-73ee04078786/receive-secret-scan && test ! -L /data/security/receive-secret-scan/20261018-73ee04078786/receive-secret-scan\ntest \"$(stat -c %u /data/security/receive-secret-scan/20261018-73ee04078786/receive-secret-scan)\" = 1000\ntest \"$(sha256sum /data/security/receive-secret-scan/20261018-73ee04078786/receive-secret-scan | cut -d \" \" -f 1)\" = 73ee04078786801fc74d80d2bef122b7764c93856e403e8de5b078336d7f9e1a\ncase \"$(stat -c %a /data/security/receive-secret-scan/20261018-73ee04078786/receive-secret-scan)\" in 755|775) ;; *) exit 21;; esac\ntest -f /data/security/receive-secret-scan/20261018-73ee04078786/gitleaks && test ! -L /data/security/receive-secret-scan/20261018-73ee04078786/gitleaks\ntest \"$(stat -c %u /data/security/receive-secret-scan/20261018-73ee04078786/gitleaks)\" = 1000\ntest \"$(sha256sum /data/security/receive-secret-scan/20261018-73ee04078786/gitleaks | cut -d \" \" -f 1)\" = 88f91962aa2f93ac6ab281d553b9e125f5197bbbce38f9f2437f7299c32e5509\ncase \"$(stat -c %a /data/security/receive-secret-scan/20261018-73ee04078786/gitleaks)\" in 755|775) ;; *) exit 21;; esac\nif [ \"${1:-check}\" = apply ]; then\n chmod 755 /data/security/receive-secret-scan/20261018-73ee04078786/receive-secret-scan\n test \"$(stat -c %a /data/security/receive-secret-scan/20261018-73ee04078786/receive-secret-scan)\" = 755\n chmod 755 /data/security/receive-secret-scan/20261018-73ee04078786/gitleaks\n test \"$(stat -c %a /data/security/receive-secret-scan/20261018-73ee04078786/gitleaks)\" = 755\nfi\n\n#!/bin/sh\nset +vx\nset -eu\ntest \"$(id -u)\" = 1000\ntest -d /data && test ! -L /data\ntest -d /data/security && test ! -L /data/security\ntest -d /data/security/receive-secret-scan && test ! -L /data/security/receive-secret-scan\ntest -d /data/security/receive-secret-scan/20261027-b735d052a49f && test ! -L /data/security/receive-secret-scan/20261027-b735d052a49f\ntest -f /data/security/receive-secret-scan/20261027-b735d052a49f/receive-secret-scan && test ! -L /data/security/receive-secret-scan/20261027-b735d052a49f/receive-secret-scan\ntest \"$(stat -c %u /data/security/receive-secret-scan/20261027-b735d052a49f/receive-secret-scan)\" = 1000\ntest \"$(sha256sum /data/security/receive-secret-scan/20261027-b735d052a49f/receive-secret-scan | cut -d \" \" -f 1)\" = b735d052a49fc8e9cca14cddee074c5429352ad71841eb6548459294f15683a2\ncase \"$(stat -c %a /data/security/receive-secret-scan/20261027-b735d052a49f/receive-secret-scan)\" in 755|775) ;; *) exit 21;; esac\ntest -f /data/security/receive-secret-scan/20261027-b735d052a49f/gitleaks && test ! -L /data/security/receive-secret-scan/20261027-b735d052a49f/gitleaks\ntest \"$(stat -c %u /data/security/receive-secret-scan/20261027-b735d052a49f/gitleaks)\" = 1000\ntest \"$(sha256sum /data/security/receive-secret-scan/20261027-b735d052a49f/gitleaks | cut -d \" \" -f 1)\" = 88f91962aa2f93ac6ab281d553b9e125f5197bbbce38f9f2437f7299c32e5509\ncase \"$(stat -c %a /data/security/receive-secret-scan/20261027-b735d052a49f/gitleaks)\" in 755|775) ;; *) exit 21;; esac\nif [ \"${1:-check}\" = apply ]; then\n chmod 755 /data/security/receive-secret-scan/20261027-b735d052a49f/receive-secret-scan\n test \"$(stat -c %a /data/security/receive-secret-scan/20261027-b735d052a49f/receive-secret-scan)\" = 755\n chmod 755 /data/security/receive-secret-scan/20261027-b735d052a49f/gitleaks\n test \"$(stat -c %a /data/security/receive-secret-scan/20261027-b735d052a49f/gitleaks)\" = 755\nfi\n\n#!/bin/sh\nset +vx\nset -eu\ntest \"$(id -u)\" = 1000\ntest -d /data && test ! -L /data\ntest -d /data/security && test ! -L /data/security\ntest -d /data/security/receive-secret-scan && test ! -L /data/security/receive-secret-scan\ntest -d /data/security/receive-secret-scan/20261102-d7dbd0ef5731 && test ! -L /data/security/receive-secret-scan/20261102-d7dbd0ef5731\ntest -f /data/security/receive-secret-scan/20261102-d7dbd0ef5731/receive-secret-scan && test ! -L /data/security/receive-secret-scan/20261102-d7dbd0ef5731/receive-secret-scan\ntest \"$(stat -c %u /data/security/receive-secret-scan/20261102-d7dbd0ef5731/receive-secret-scan)\" = 1000\ntest \"$(sha256sum /data/security/receive-secret-scan/20261102-d7dbd0ef5731/receive-secret-scan | cut -d \" \" -f 1)\" = d7dbd0ef57312ab9507874bb5fd67c49bef637b6d7dee121e526227307af4bf0\ncase \"$(stat -c %a /data/security/receive-secret-scan/20261102-d7dbd0ef5731/receive-secret-scan)\" in 755|775) ;; *) exit 21;; esac\nif [ \"${1:-check}\" = apply ]; then chmod 755 /data/security/receive-secret-scan/20261102-d7dbd0ef5731/receive-secret-scan; test \"$(stat -c %a /data/security/receive-secret-scan/20261102-d7dbd0ef5731/receive-secret-scan)\" = 755; fi\ntest -f /data/security/receive-secret-scan/20261102-d7dbd0ef5731/gitleaks && test ! -L /data/security/receive-secret-scan/20261102-d7dbd0ef5731/gitleaks\ntest \"$(stat -c %u /data/security/receive-secret-scan/20261102-d7dbd0ef5731/gitleaks)\" = 1000\ntest \"$(sha256sum /data/security/receive-secret-scan/20261102-d7dbd0ef5731/gitleaks | cut -d \" \" -f 1)\" = 88f91962aa2f93ac6ab281d553b9e125f5197bbbce38f9f2437f7299c32e5509\ncase \"$(stat -c %a /data/security/receive-secret-scan/20261102-d7dbd0ef5731/gitleaks)\" in 755|775) ;; *) exit 21;; esac\nif [ \"${1:-check}\" = apply ]; then chmod 755 /data/security/receive-secret-scan/20261102-d7dbd0ef5731/gitleaks; test \"$(stat -c %a /data/security/receive-secret-scan/20261102-d7dbd0ef5731/gitleaks)\" = 755; fi\n" + - sh + - apply + securityContext: +--- /home/worsch/railiance-apps/manifests/forgejo-secret-scan-template.yaml ++++ /home/worsch/railiance-apps/manifests/forgejo-secret-scan-template.yaml +@@ -1,16 +1,16 @@ +-apiVersion: v1 +-kind: ConfigMap +-metadata: +- name: forgejo-secret-template-d1df801be39f +- namespace: forgejo +- labels: +- app.kubernetes.io/part-of: forgejo +- custodian.coulomb.social/workplan: FORGE-WP-ADHOC-2026-10-27 +-immutable: true +-data: +- hook: | +- #!/bin/sh +- set +vx +- set -eu +- exec /data/security/receive-secret-scan/20261027-b735d052a49f/receive-secret-scan --scanner /data/security/receive-secret-scan/20261027-b735d052a49f/gitleaks +- ++{ ++ "apiVersion": "v1", ++ "kind": "ConfigMap", ++ "metadata": { ++ "name": "forgejo-secret-template-619d94208139", ++ "namespace": "forgejo", ++ "labels": { ++ "app.kubernetes.io/part-of": "forgejo", ++ "custodian.railiance.org/workplan": "CUST-WP-0083" ++ } ++ }, ++ "immutable": true, ++ "data": { ++ "hook": "#!/bin/sh\nset +vx\nset -eu\nexec /data/security/receive-secret-scan/20261102-d7dbd0ef5731/receive-secret-scan --scanner /data/security/receive-secret-scan/20261102-d7dbd0ef5731/gitleaks\n" ++ } ++} diff --git a/docs/changes/2026-11-01-scanner-json-digest/native-r2/before-hook b/docs/changes/2026-11-01-scanner-json-digest/native-r2/before-hook new file mode 100644 index 00000000..eb36b0bc --- /dev/null +++ b/docs/changes/2026-11-01-scanner-json-digest/native-r2/before-hook @@ -0,0 +1,4 @@ +#!/bin/sh +set +vx +set -eu +exec /data/security/receive-secret-scan/20261027-b735d052a49f/receive-secret-scan --scanner /data/security/receive-secret-scan/20261027-b735d052a49f/gitleaks diff --git a/docs/changes/2026-11-01-scanner-json-digest/native-r2/configmap.json b/docs/changes/2026-11-01-scanner-json-digest/native-r2/configmap.json new file mode 100644 index 00000000..7a5ed1dd --- /dev/null +++ b/docs/changes/2026-11-01-scanner-json-digest/native-r2/configmap.json @@ -0,0 +1,16 @@ +{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": { + "name": "forgejo-secret-template-619d94208139", + "namespace": "forgejo", + "labels": { + "app.kubernetes.io/part-of": "forgejo", + "custodian.railiance.org/workplan": "CUST-WP-0083" + } + }, + "immutable": true, + "data": { + "hook": "#!/bin/sh\nset +vx\nset -eu\nexec /data/security/receive-secret-scan/20261102-d7dbd0ef5731/receive-secret-scan --scanner /data/security/receive-secret-scan/20261102-d7dbd0ef5731/gitleaks\n" + } +} diff --git a/docs/changes/2026-11-01-scanner-json-digest/native-r2/filesystem-r1.json b/docs/changes/2026-11-01-scanner-json-digest/native-r2/filesystem-r1.json new file mode 100644 index 00000000..fd526fad --- /dev/null +++ b/docs/changes/2026-11-01-scanner-json-digest/native-r2/filesystem-r1.json @@ -0,0 +1 @@ +{"result":"unverified"} diff --git a/docs/changes/2026-11-01-scanner-json-digest/native-r2/filesystem-r2.json b/docs/changes/2026-11-01-scanner-json-digest/native-r2/filesystem-r2.json new file mode 100644 index 00000000..d4651e25 --- /dev/null +++ b/docs/changes/2026-11-01-scanner-json-digest/native-r2/filesystem-r2.json @@ -0,0 +1,73 @@ +{ + "result": "passed", + "cases": [ + { + "case": "guard-new-drift", + "returncode": 0, + "seconds": 0.16 + }, + { + "case": "guard-symlink", + "returncode": 0, + "seconds": 0.13 + }, + { + "case": "guard-unexpected-mode", + "returncode": 0, + "seconds": 0.16 + }, + { + "case": "stage-roundtrip", + "returncode": 0, + "seconds": 0.34 + }, + { + "case": "stage-bad-hash-cleanup", + "returncode": 0, + "seconds": 0.19 + }, + { + "case": "stage-existing-unowned", + "returncode": 0, + "seconds": 0.16 + }, + { + "case": "stage-symlink-refused", + "returncode": 0, + "seconds": 0.14 + }, + { + "case": "hooks-roundtrip", + "returncode": 0, + "seconds": 0.26 + }, + { + "case": "hooks-refuse-complete-scope-before-writing", + "returncode": 0, + "seconds": 0.14 + }, + { + "case": "hooks-partial-failure-recovers", + "returncode": 0, + "seconds": 0.17 + }, + { + "case": "hooks-symlink-refused", + "returncode": 0, + "seconds": 0.12 + }, + { + "case": "hooks-rollback-unknown-drift-retained", + "returncode": 0, + "seconds": 0.21 + }, + { + "case": "real-receive-and-quarantine", + "returncode": 0, + "seconds": 4.57 + } + ], + "native_acceptance": false, + "real_remote_refs_changed": false, + "scope": "Exact new guard, stage/hook scripts, real candidate/Gitleaks receive probes; original guard bytes unchanged." +} diff --git a/docs/changes/2026-11-01-scanner-json-digest/native-r2/fixture-dispatcher.sh b/docs/changes/2026-11-01-scanner-json-digest/native-r2/fixture-dispatcher.sh new file mode 100644 index 00000000..a617788b --- /dev/null +++ b/docs/changes/2026-11-01-scanner-json-digest/native-r2/fixture-dispatcher.sh @@ -0,0 +1,4 @@ +#!/bin/sh +# Offline scanner-only dispatcher; native probe separately copies the real dispatcher. +set -eu +exec "$GIT_DIR/hooks/pre-receive.d/10-custodian-secret-scan" diff --git a/docs/changes/2026-11-01-scanner-json-digest/native-r2/forgejo-values.yaml b/docs/changes/2026-11-01-scanner-json-digest/native-r2/forgejo-values.yaml new file mode 100644 index 00000000..f0904d29 --- /dev/null +++ b/docs/changes/2026-11-01-scanner-json-digest/native-r2/forgejo-values.yaml @@ -0,0 +1,155 @@ +# Product-neutral object names (RAPPS-WP-0019). The upstream chart is named +# 'gitea'; these overrides keep that legacy name out of live objects. The data +# volume keeps its existing claim name; renaming it needs a data copy. +fullnameOverride: forge +nameOverride: forge + +# Non-secret Forgejo Helm values (gitea-charts/gitea chart + Forgejo image). +# Layer helm/forgejo-secrets.sops.yaml for database password and admin credentials. +# Recreate avoids leveldb queue lock fights on the shared RWO PVC during upgrades. +strategy: + type: Recreate + +# Main Forgejo container sizing (FORGE-WP-0011, CUST-WP-0071, RESOURCE-WP-0008). +# Evidence 2026-10-21 (7d): CPU p95 133-160m, peak ~1030m; memory working set +# p95 ~956Mi, per-pod peak 1252Mi, RSS peak 500Mi; no OOM events. +# Request = p95 rule (200m / 1104Mi). Memory limit 1536Mi = 1.23x working-set +# peak and ~3x RSS peak (the rest is reclaimable page cache). No CPU limit: +# bursts to ~1 core are normal for git pack/clone work and must not throttle. +resources: + requests: + cpu: 200m + memory: 1104Mi + limits: + memory: 1536Mi + +service: + http: + type: ClusterIP + +image: + registry: code.forgejo.org + repository: forgejo/forgejo + tag: "11.0.3" + rootless: true + +persistence: + enabled: true + claimName: gitea-shared-storage + size: 10Gi + +postgresql: + enabled: false + +postgresql-ha: + enabled: false + +redis-cluster: + enabled: false + +valkey-cluster: + enabled: false + +# File subPath keeps Git from copying ConfigMap projection symlinks. +extraVolumes: +- name: secret-scan-template + configMap: + name: forgejo-secret-template-619d94208139 + defaultMode: 365 +extraContainerVolumeMounts: +- &id001 + name: secret-scan-template + mountPath: /opt/forge-secret-template/hooks/pre-receive.d/10-custodian-secret-scan + subPath: hook + readOnly: true +extraInitVolumeMounts: +- *id001 + +# Verify admitted scanner custody after Kubernetes fsGroup volume processing. +# Only the existing security subtree is mounted; no credentials are needed. +preExtraInitContainers: +- name: verify-secret-scanner + image: code.forgejo.org/forgejo/forgejo@sha256:691657c7e64af7730a2e885e6d022cf16ae60724931c5392bd61838afadcaeba + imagePullPolicy: IfNotPresent + command: + - sh + - -c + - "#!/bin/sh\nset +vx\nset -eu\ntest \"$(id -u)\" = 1000\ntest -d /data && test ! -L /data\ntest -d /data/security && test ! -L /data/security\ntest -d /data/security/receive-secret-scan && test ! -L /data/security/receive-secret-scan\ntest -d /data/security/receive-secret-scan/20261012-57a0642b6251 && test ! -L /data/security/receive-secret-scan/20261012-57a0642b6251\ntest -f /data/security/receive-secret-scan/20261012-57a0642b6251/receive-secret-scan && test ! -L /data/security/receive-secret-scan/20261012-57a0642b6251/receive-secret-scan\ntest \"$(stat -c %u /data/security/receive-secret-scan/20261012-57a0642b6251/receive-secret-scan)\" = 1000\ntest \"$(sha256sum /data/security/receive-secret-scan/20261012-57a0642b6251/receive-secret-scan | cut -d \" \" -f 1)\" = 57a0642b625105440fe146fceb4d244fd2dd08a75beb316831675643e6cc27ec\ncase \"$(stat -c %a /data/security/receive-secret-scan/20261012-57a0642b6251/receive-secret-scan)\" in 755|775) ;; *) exit 21;; esac\ntest -f /data/security/receive-secret-scan/20261012-57a0642b6251/gitleaks && test ! -L /data/security/receive-secret-scan/20261012-57a0642b6251/gitleaks\ntest \"$(stat -c %u /data/security/receive-secret-scan/20261012-57a0642b6251/gitleaks)\" = 1000\ntest \"$(sha256sum /data/security/receive-secret-scan/20261012-57a0642b6251/gitleaks | cut -d \" \" -f 1)\" = 88f91962aa2f93ac6ab281d553b9e125f5197bbbce38f9f2437f7299c32e5509\ncase \"$(stat -c %a /data/security/receive-secret-scan/20261012-57a0642b6251/gitleaks)\" in 755|775) ;; *) exit 21;; esac\nif [ \"${1:-check}\" = apply ]; then\n chmod 755 /data/security/receive-secret-scan/20261012-57a0642b6251/receive-secret-scan\n test \"$(stat -c %a /data/security/receive-secret-scan/20261012-57a0642b6251/receive-secret-scan)\" = 755\n chmod 755 /data/security/receive-secret-scan/20261012-57a0642b6251/gitleaks\n test \"$(stat -c %a /data/security/receive-secret-scan/20261012-57a0642b6251/gitleaks)\" = 755\nfi\n\n#!/bin/sh\nset +vx\nset -eu\ntest \"$(id -u)\" = 1000\ntest -d /data && test ! -L /data\ntest -d /data/security && test ! -L /data/security\ntest -d /data/security/receive-secret-scan && test ! -L /data/security/receive-secret-scan\ntest -d /data/security/receive-secret-scan/20261018-73ee04078786 && test ! -L /data/security/receive-secret-scan/20261018-73ee04078786\ntest -f /data/security/receive-secret-scan/20261018-73ee04078786/receive-secret-scan && test ! -L /data/security/receive-secret-scan/20261018-73ee04078786/receive-secret-scan\ntest \"$(stat -c %u /data/security/receive-secret-scan/20261018-73ee04078786/receive-secret-scan)\" = 1000\ntest \"$(sha256sum /data/security/receive-secret-scan/20261018-73ee04078786/receive-secret-scan | cut -d \" \" -f 1)\" = 73ee04078786801fc74d80d2bef122b7764c93856e403e8de5b078336d7f9e1a\ncase \"$(stat -c %a /data/security/receive-secret-scan/20261018-73ee04078786/receive-secret-scan)\" in 755|775) ;; *) exit 21;; esac\ntest -f /data/security/receive-secret-scan/20261018-73ee04078786/gitleaks && test ! -L /data/security/receive-secret-scan/20261018-73ee04078786/gitleaks\ntest \"$(stat -c %u /data/security/receive-secret-scan/20261018-73ee04078786/gitleaks)\" = 1000\ntest \"$(sha256sum /data/security/receive-secret-scan/20261018-73ee04078786/gitleaks | cut -d \" \" -f 1)\" = 88f91962aa2f93ac6ab281d553b9e125f5197bbbce38f9f2437f7299c32e5509\ncase \"$(stat -c %a /data/security/receive-secret-scan/20261018-73ee04078786/gitleaks)\" in 755|775) ;; *) exit 21;; esac\nif [ \"${1:-check}\" = apply ]; then\n chmod 755 /data/security/receive-secret-scan/20261018-73ee04078786/receive-secret-scan\n test \"$(stat -c %a /data/security/receive-secret-scan/20261018-73ee04078786/receive-secret-scan)\" = 755\n chmod 755 /data/security/receive-secret-scan/20261018-73ee04078786/gitleaks\n test \"$(stat -c %a /data/security/receive-secret-scan/20261018-73ee04078786/gitleaks)\" = 755\nfi\n\n#!/bin/sh\nset +vx\nset -eu\ntest \"$(id -u)\" = 1000\ntest -d /data && test ! -L /data\ntest -d /data/security && test ! -L /data/security\ntest -d /data/security/receive-secret-scan && test ! -L /data/security/receive-secret-scan\ntest -d /data/security/receive-secret-scan/20261027-b735d052a49f && test ! -L /data/security/receive-secret-scan/20261027-b735d052a49f\ntest -f /data/security/receive-secret-scan/20261027-b735d052a49f/receive-secret-scan && test ! -L /data/security/receive-secret-scan/20261027-b735d052a49f/receive-secret-scan\ntest \"$(stat -c %u /data/security/receive-secret-scan/20261027-b735d052a49f/receive-secret-scan)\" = 1000\ntest \"$(sha256sum /data/security/receive-secret-scan/20261027-b735d052a49f/receive-secret-scan | cut -d \" \" -f 1)\" = b735d052a49fc8e9cca14cddee074c5429352ad71841eb6548459294f15683a2\ncase \"$(stat -c %a /data/security/receive-secret-scan/20261027-b735d052a49f/receive-secret-scan)\" in 755|775) ;; *) exit 21;; esac\ntest -f /data/security/receive-secret-scan/20261027-b735d052a49f/gitleaks && test ! -L /data/security/receive-secret-scan/20261027-b735d052a49f/gitleaks\ntest \"$(stat -c %u /data/security/receive-secret-scan/20261027-b735d052a49f/gitleaks)\" = 1000\ntest \"$(sha256sum /data/security/receive-secret-scan/20261027-b735d052a49f/gitleaks | cut -d \" \" -f 1)\" = 88f91962aa2f93ac6ab281d553b9e125f5197bbbce38f9f2437f7299c32e5509\ncase \"$(stat -c %a /data/security/receive-secret-scan/20261027-b735d052a49f/gitleaks)\" in 755|775) ;; *) exit 21;; esac\nif [ \"${1:-check}\" = apply ]; then\n chmod 755 /data/security/receive-secret-scan/20261027-b735d052a49f/receive-secret-scan\n test \"$(stat -c %a /data/security/receive-secret-scan/20261027-b735d052a49f/receive-secret-scan)\" = 755\n chmod 755 /data/security/receive-secret-scan/20261027-b735d052a49f/gitleaks\n test \"$(stat -c %a /data/security/receive-secret-scan/20261027-b735d052a49f/gitleaks)\" = 755\nfi\n\n#!/bin/sh\nset +vx\nset -eu\ntest \"$(id -u)\" = 1000\ntest -d /data && test ! -L /data\ntest -d /data/security && test ! -L /data/security\ntest -d /data/security/receive-secret-scan && test ! -L /data/security/receive-secret-scan\ntest -d /data/security/receive-secret-scan/20261102-d7dbd0ef5731 && test ! -L /data/security/receive-secret-scan/20261102-d7dbd0ef5731\ntest -f /data/security/receive-secret-scan/20261102-d7dbd0ef5731/receive-secret-scan && test ! -L /data/security/receive-secret-scan/20261102-d7dbd0ef5731/receive-secret-scan\ntest \"$(stat -c %u /data/security/receive-secret-scan/20261102-d7dbd0ef5731/receive-secret-scan)\" = 1000\ntest \"$(sha256sum /data/security/receive-secret-scan/20261102-d7dbd0ef5731/receive-secret-scan | cut -d \" \" -f 1)\" = d7dbd0ef57312ab9507874bb5fd67c49bef637b6d7dee121e526227307af4bf0\ncase \"$(stat -c %a /data/security/receive-secret-scan/20261102-d7dbd0ef5731/receive-secret-scan)\" in 755|775) ;; *) exit 21;; esac\nif [ \"${1:-check}\" = apply ]; then chmod 755 /data/security/receive-secret-scan/20261102-d7dbd0ef5731/receive-secret-scan; test \"$(stat -c %a /data/security/receive-secret-scan/20261102-d7dbd0ef5731/receive-secret-scan)\" = 755; fi\ntest -f /data/security/receive-secret-scan/20261102-d7dbd0ef5731/gitleaks && test ! -L /data/security/receive-secret-scan/20261102-d7dbd0ef5731/gitleaks\ntest \"$(stat -c %u /data/security/receive-secret-scan/20261102-d7dbd0ef5731/gitleaks)\" = 1000\ntest \"$(sha256sum /data/security/receive-secret-scan/20261102-d7dbd0ef5731/gitleaks | cut -d \" \" -f 1)\" = 88f91962aa2f93ac6ab281d553b9e125f5197bbbce38f9f2437f7299c32e5509\ncase \"$(stat -c %a /data/security/receive-secret-scan/20261102-d7dbd0ef5731/gitleaks)\" in 755|775) ;; *) exit 21;; esac\nif [ \"${1:-check}\" = apply ]; then chmod 755 /data/security/receive-secret-scan/20261102-d7dbd0ef5731/gitleaks; test \"$(stat -c %a /data/security/receive-secret-scan/20261102-d7dbd0ef5731/gitleaks)\" = 755; fi\n" + - sh + - apply + securityContext: + runAsUser: 1000 + runAsGroup: 1000 + runAsNonRoot: true + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + resources: + requests: + cpu: 10m + memory: 32Mi + limits: + cpu: 100m + memory: 64Mi + volumeMounts: + - name: data + mountPath: /data/security + subPath: security + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + +gitea: + oauth: + - name: netkingdom + provider: openidConnect + key: forgejo + secret: "" + autoDiscoverUrl: https://kc.coulomb.social/.well-known/openid-configuration + scopes: openid,profile,email,groups + admin: + username: forgejo_admin + email: admin@coulomb.social + passwordMode: initialOnlyRequireReset + # Mailer password from OpenBao via ExternalSecret forgejo-mailer (not Helm/SOPS). + additionalConfigFromEnvs: + - name: GITEA__GIT_0X2E_CONFIG__INIT_0X2E_TEMPLATEDIR + value: /opt/forge-secret-template + - name: GITEA__mailer__PASSWD + valueFrom: + secretKeyRef: + name: forgejo-mailer + key: MAILER_PASSWD + config: + oauth2_client: + ENABLE_AUTO_REGISTRATION: false + ACCOUNT_LINKING: login + OPENID_CONNECT_SCOPES: openid profile email groups + database: + DB_TYPE: postgres + HOST: forgejo-db-rw.databases.svc.cluster.local:5432 + NAME: forgejo + USER: forgejo + server: + DOMAIN: forgejo.coulomb.social + SSH_DOMAIN: forgejo.coulomb.social + SSH_PORT: 22 + SSH_LISTEN_PORT: 2222 + DISABLE_SSH: false + service: + DISABLE_REGISTRATION: true + security: + INSTALL_LOCK: true + session: + COOKIE_SECURE: true + mailer: + ENABLED: true + FROM: forgejo@coulomb.social + # IONOS submission port 587 uses STARTTLS, not implicit TLS (smtps). + PROTOCOL: smtp+starttls + SMTP_ADDR: smtp.ionos.de + SMTP_PORT: 587 + USER: forgejo@coulomb.social + actions: + ENABLED: true \ No newline at end of file diff --git a/docs/changes/2026-11-01-scanner-json-digest/native-r2/hooks.sh b/docs/changes/2026-11-01-scanner-json-digest/native-r2/hooks.sh new file mode 100644 index 00000000..215b8e7a --- /dev/null +++ b/docs/changes/2026-11-01-scanner-json-digest/native-r2/hooks.sh @@ -0,0 +1,66 @@ +#!/bin/sh +# Caller supplies a sealed, exact path list and known public before/after bytes. +set +vx +set -eu +PATH=/usr/bin:/bin +export PATH +area=$1 direction=$2 before_sum=$3 after_sum=$4 +test "$(id -u)" = 1000 +for file in before after paths; do test -f "$area/$file" && test ! -L "$area/$file"; done +test "$(sha256sum "$area/before" | cut -d ' ' -f 1)" = "$before_sum" +test "$(sha256sum "$area/after" | cut -d ' ' -f 1)" = "$after_sum" +case "$direction" in apply) ;; rollback) ;; *) exit 35;; esac +validate_path() { + path=$1 + case "$path" in /data/git/gitea-repositories/*/*.git/hooks/pre-receive.d/10-custodian-secret-scan) ;; *) exit 36;; esac + parent=${path%/*} + while test "$parent" != /data; do + test -d "$parent" && test ! -L "$parent" + parent=${parent%/*} + done + test -f "$path" && test ! -L "$path" + test "$(stat -c %u "$path")" = 1000 + case "$(stat -c %a "$path")" in 755|775) ;; *) exit 37;; esac + current=$(sha256sum "$path" | cut -d ' ' -f 1) + if test "$direction" = apply; then test "$current" = "$before_sum" + else test "$current" = "$before_sum" || test "$current" = "$after_sum"; fi +} +# Validate the complete scope before the first write. +count=0 +while IFS= read -r path; do validate_path "$path"; count=$((count+1)); done < "$area/paths" +test "$count" -gt 0 +temporary= +restore_partial() { + code=$? + trap - EXIT HUP INT TERM + if test -n "$temporary"; then rm -f -- "$temporary" || code=38; fi + # Only our exact replacement may be restored. Unknown drift is retained. + if test "$direction" = apply && test "$code" != 0; then + while IFS= read -r path; do + if test -f "$path" && test ! -L "$path" && + test "$(sha256sum "$path" | cut -d ' ' -f 1)" = "$after_sum"; then + staged=$(mktemp "${path%/*}/.custodian-restore-XXXXXX") || { code=38; continue; } + if cp "$area/before" "$staged" && chmod --reference="$path" "$staged" && mv -T -- "$staged" "$path"; then : + else rm -f -- "$staged"; code=38; fi + fi + done < "$area/paths" + fi + exit "$code" +} +trap restore_partial EXIT +trap 'exit 39' HUP INT TERM +while IFS= read -r path; do + validate_path "$path" + if test "$direction" = rollback; then source="$area/before"; sum=$before_sum; mode=775 + else source="$area/after"; sum=$after_sum; mode=755; fi + if test "$(sha256sum "$path" | cut -d ' ' -f 1)" != "$sum"; then + temporary=$(mktemp "${path%/*}/.custodian-replace-XXXXXX") + cp "$source" "$temporary" + chmod --reference="$path" "$temporary" + test "$(sha256sum "$temporary" | cut -d ' ' -f 1)" = "$sum" + mv -T -- "$temporary" "$path" + temporary= + fi + test "$(sha256sum "$path" | cut -d ' ' -f 1)" = "$sum" +done < "$area/paths" +printf '{"hook_files_verified":%s,"direction":"%s"}\n' "$count" "$direction" diff --git a/docs/changes/2026-11-01-scanner-json-digest/native-r2/inputs.json b/docs/changes/2026-11-01-scanner-json-digest/native-r2/inputs.json new file mode 100644 index 00000000..f19e5362 --- /dev/null +++ b/docs/changes/2026-11-01-scanner-json-digest/native-r2/inputs.json @@ -0,0 +1,61 @@ +{ + "read_only": true, + "deployment_uid": "1faefb26-cccd-4d0e-b509-70a7eb0b4f4d", + "pod": "forge-79c7fb84db-k5wtm", + "pod_uid": "2c9f911a-403e-4183-a027-2b8d48742348", + "deployment_name": "forge", + "container": "gitea", + "node_uid": "0659e49e-6539-480a-8b0f-2969dd88cb05", + "node_name": "239.62.205.92.host.secureserver.net", + "guard_index": 0, + "guard_command": [ + "sh", + "-c", + "#!/bin/sh\nset +vx\nset -eu\ntest \"$(id -u)\" = 1000\ntest -d /data && test ! -L /data\ntest -d /data/security && test ! -L /data/security\ntest -d /data/security/receive-secret-scan && test ! -L /data/security/receive-secret-scan\ntest -d /data/security/receive-secret-scan/20261012-57a0642b6251 && test ! -L /data/security/receive-secret-scan/20261012-57a0642b6251\ntest -f /data/security/receive-secret-scan/20261012-57a0642b6251/receive-secret-scan && test ! -L /data/security/receive-secret-scan/20261012-57a0642b6251/receive-secret-scan\ntest \"$(stat -c %u /data/security/receive-secret-scan/20261012-57a0642b6251/receive-secret-scan)\" = 1000\ntest \"$(sha256sum /data/security/receive-secret-scan/20261012-57a0642b6251/receive-secret-scan | cut -d \" \" -f 1)\" = 57a0642b625105440fe146fceb4d244fd2dd08a75beb316831675643e6cc27ec\ncase \"$(stat -c %a /data/security/receive-secret-scan/20261012-57a0642b6251/receive-secret-scan)\" in 755|775) ;; *) exit 21;; esac\ntest -f /data/security/receive-secret-scan/20261012-57a0642b6251/gitleaks && test ! -L /data/security/receive-secret-scan/20261012-57a0642b6251/gitleaks\ntest \"$(stat -c %u /data/security/receive-secret-scan/20261012-57a0642b6251/gitleaks)\" = 1000\ntest \"$(sha256sum /data/security/receive-secret-scan/20261012-57a0642b6251/gitleaks | cut -d \" \" -f 1)\" = 88f91962aa2f93ac6ab281d553b9e125f5197bbbce38f9f2437f7299c32e5509\ncase \"$(stat -c %a /data/security/receive-secret-scan/20261012-57a0642b6251/gitleaks)\" in 755|775) ;; *) exit 21;; esac\nif [ \"${1:-check}\" = apply ]; then\n chmod 755 /data/security/receive-secret-scan/20261012-57a0642b6251/receive-secret-scan\n test \"$(stat -c %a /data/security/receive-secret-scan/20261012-57a0642b6251/receive-secret-scan)\" = 755\n chmod 755 /data/security/receive-secret-scan/20261012-57a0642b6251/gitleaks\n test \"$(stat -c %a /data/security/receive-secret-scan/20261012-57a0642b6251/gitleaks)\" = 755\nfi\n\n#!/bin/sh\nset +vx\nset -eu\ntest \"$(id -u)\" = 1000\ntest -d /data && test ! -L /data\ntest -d /data/security && test ! -L /data/security\ntest -d /data/security/receive-secret-scan && test ! -L /data/security/receive-secret-scan\ntest -d /data/security/receive-secret-scan/20261018-73ee04078786 && test ! -L /data/security/receive-secret-scan/20261018-73ee04078786\ntest -f /data/security/receive-secret-scan/20261018-73ee04078786/receive-secret-scan && test ! -L /data/security/receive-secret-scan/20261018-73ee04078786/receive-secret-scan\ntest \"$(stat -c %u /data/security/receive-secret-scan/20261018-73ee04078786/receive-secret-scan)\" = 1000\ntest \"$(sha256sum /data/security/receive-secret-scan/20261018-73ee04078786/receive-secret-scan | cut -d \" \" -f 1)\" = 73ee04078786801fc74d80d2bef122b7764c93856e403e8de5b078336d7f9e1a\ncase \"$(stat -c %a /data/security/receive-secret-scan/20261018-73ee04078786/receive-secret-scan)\" in 755|775) ;; *) exit 21;; esac\ntest -f /data/security/receive-secret-scan/20261018-73ee04078786/gitleaks && test ! -L /data/security/receive-secret-scan/20261018-73ee04078786/gitleaks\ntest \"$(stat -c %u /data/security/receive-secret-scan/20261018-73ee04078786/gitleaks)\" = 1000\ntest \"$(sha256sum /data/security/receive-secret-scan/20261018-73ee04078786/gitleaks | cut -d \" \" -f 1)\" = 88f91962aa2f93ac6ab281d553b9e125f5197bbbce38f9f2437f7299c32e5509\ncase \"$(stat -c %a /data/security/receive-secret-scan/20261018-73ee04078786/gitleaks)\" in 755|775) ;; *) exit 21;; esac\nif [ \"${1:-check}\" = apply ]; then\n chmod 755 /data/security/receive-secret-scan/20261018-73ee04078786/receive-secret-scan\n test \"$(stat -c %a /data/security/receive-secret-scan/20261018-73ee04078786/receive-secret-scan)\" = 755\n chmod 755 /data/security/receive-secret-scan/20261018-73ee04078786/gitleaks\n test \"$(stat -c %a /data/security/receive-secret-scan/20261018-73ee04078786/gitleaks)\" = 755\nfi\n\n#!/bin/sh\nset +vx\nset -eu\ntest \"$(id -u)\" = 1000\ntest -d /data && test ! -L /data\ntest -d /data/security && test ! -L /data/security\ntest -d /data/security/receive-secret-scan && test ! -L /data/security/receive-secret-scan\ntest -d /data/security/receive-secret-scan/20261027-b735d052a49f && test ! -L /data/security/receive-secret-scan/20261027-b735d052a49f\ntest -f /data/security/receive-secret-scan/20261027-b735d052a49f/receive-secret-scan && test ! -L /data/security/receive-secret-scan/20261027-b735d052a49f/receive-secret-scan\ntest \"$(stat -c %u /data/security/receive-secret-scan/20261027-b735d052a49f/receive-secret-scan)\" = 1000\ntest \"$(sha256sum /data/security/receive-secret-scan/20261027-b735d052a49f/receive-secret-scan | cut -d \" \" -f 1)\" = b735d052a49fc8e9cca14cddee074c5429352ad71841eb6548459294f15683a2\ncase \"$(stat -c %a /data/security/receive-secret-scan/20261027-b735d052a49f/receive-secret-scan)\" in 755|775) ;; *) exit 21;; esac\ntest -f /data/security/receive-secret-scan/20261027-b735d052a49f/gitleaks && test ! -L /data/security/receive-secret-scan/20261027-b735d052a49f/gitleaks\ntest \"$(stat -c %u /data/security/receive-secret-scan/20261027-b735d052a49f/gitleaks)\" = 1000\ntest \"$(sha256sum /data/security/receive-secret-scan/20261027-b735d052a49f/gitleaks | cut -d \" \" -f 1)\" = 88f91962aa2f93ac6ab281d553b9e125f5197bbbce38f9f2437f7299c32e5509\ncase \"$(stat -c %a /data/security/receive-secret-scan/20261027-b735d052a49f/gitleaks)\" in 755|775) ;; *) exit 21;; esac\nif [ \"${1:-check}\" = apply ]; then\n chmod 755 /data/security/receive-secret-scan/20261027-b735d052a49f/receive-secret-scan\n test \"$(stat -c %a /data/security/receive-secret-scan/20261027-b735d052a49f/receive-secret-scan)\" = 755\n chmod 755 /data/security/receive-secret-scan/20261027-b735d052a49f/gitleaks\n test \"$(stat -c %a /data/security/receive-secret-scan/20261027-b735d052a49f/gitleaks)\" = 755\nfi\n", + "sh", + "apply" + ], + "guard_args": null, + "volume_index": 2, + "volume": { + "configMap": { + "defaultMode": 365, + "name": "forgejo-secret-template-d1df801be39f" + }, + "name": "secret-scan-template" + }, + "strategy": { + "type": "Recreate" + }, + "resources": [ + { + "name": "gitea", + "resources": { + "limits": { + "memory": "1536Mi" + }, + "requests": { + "cpu": "200m", + "memory": "1104Mi" + } + } + } + ], + "old_configmap": "forgejo-secret-template-d1df801be39f", + "old_hook_sha256": "d1df801be39f2cd4ac2a9e71bbd1341ccd2723d236c9c265853b289cc6df221f", + "old_adapter_sha256": "b735d052a49fc8e9cca14cddee074c5429352ad71841eb6548459294f15683a2", + "scanner_sha256": "88f91962aa2f93ac6ab281d553b9e125f5197bbbce38f9f2437f7299c32e5509", + "allocated_resources": "(Total limits may be over 100 percent, i.e., overcommitted.)\n Resource Requests Limits\n -------- -------- ------\n cpu 2720m (68%) 40600m (1015%)\n memory 12236Mi (76%) 41026Mi (256%)\n ephemeral-storage 256Mi (0%) 1Gi (0%)\n hugepages-1Gi 0 (0%) 0 (0%)\n hugepages-2Mi 0 (0%) 0 (0%)", + "old_dir": "/data/security/receive-secret-scan/20261027-b735d052a49f", + "new_dir": "/data/security/receive-secret-scan/20261102-d7dbd0ef5731", + "new_configmap": "forgejo-secret-template-619d94208139", + "new_guard_command": [ + "sh", + "-c", + "#!/bin/sh\nset +vx\nset -eu\ntest \"$(id -u)\" = 1000\ntest -d /data && test ! -L /data\ntest -d /data/security && test ! -L /data/security\ntest -d /data/security/receive-secret-scan && test ! -L /data/security/receive-secret-scan\ntest -d /data/security/receive-secret-scan/20261012-57a0642b6251 && test ! -L /data/security/receive-secret-scan/20261012-57a0642b6251\ntest -f /data/security/receive-secret-scan/20261012-57a0642b6251/receive-secret-scan && test ! -L /data/security/receive-secret-scan/20261012-57a0642b6251/receive-secret-scan\ntest \"$(stat -c %u /data/security/receive-secret-scan/20261012-57a0642b6251/receive-secret-scan)\" = 1000\ntest \"$(sha256sum /data/security/receive-secret-scan/20261012-57a0642b6251/receive-secret-scan | cut -d \" \" -f 1)\" = 57a0642b625105440fe146fceb4d244fd2dd08a75beb316831675643e6cc27ec\ncase \"$(stat -c %a /data/security/receive-secret-scan/20261012-57a0642b6251/receive-secret-scan)\" in 755|775) ;; *) exit 21;; esac\ntest -f /data/security/receive-secret-scan/20261012-57a0642b6251/gitleaks && test ! -L /data/security/receive-secret-scan/20261012-57a0642b6251/gitleaks\ntest \"$(stat -c %u /data/security/receive-secret-scan/20261012-57a0642b6251/gitleaks)\" = 1000\ntest \"$(sha256sum /data/security/receive-secret-scan/20261012-57a0642b6251/gitleaks | cut -d \" \" -f 1)\" = 88f91962aa2f93ac6ab281d553b9e125f5197bbbce38f9f2437f7299c32e5509\ncase \"$(stat -c %a /data/security/receive-secret-scan/20261012-57a0642b6251/gitleaks)\" in 755|775) ;; *) exit 21;; esac\nif [ \"${1:-check}\" = apply ]; then\n chmod 755 /data/security/receive-secret-scan/20261012-57a0642b6251/receive-secret-scan\n test \"$(stat -c %a /data/security/receive-secret-scan/20261012-57a0642b6251/receive-secret-scan)\" = 755\n chmod 755 /data/security/receive-secret-scan/20261012-57a0642b6251/gitleaks\n test \"$(stat -c %a /data/security/receive-secret-scan/20261012-57a0642b6251/gitleaks)\" = 755\nfi\n\n#!/bin/sh\nset +vx\nset -eu\ntest \"$(id -u)\" = 1000\ntest -d /data && test ! -L /data\ntest -d /data/security && test ! -L /data/security\ntest -d /data/security/receive-secret-scan && test ! -L /data/security/receive-secret-scan\ntest -d /data/security/receive-secret-scan/20261018-73ee04078786 && test ! -L /data/security/receive-secret-scan/20261018-73ee04078786\ntest -f /data/security/receive-secret-scan/20261018-73ee04078786/receive-secret-scan && test ! -L /data/security/receive-secret-scan/20261018-73ee04078786/receive-secret-scan\ntest \"$(stat -c %u /data/security/receive-secret-scan/20261018-73ee04078786/receive-secret-scan)\" = 1000\ntest \"$(sha256sum /data/security/receive-secret-scan/20261018-73ee04078786/receive-secret-scan | cut -d \" \" -f 1)\" = 73ee04078786801fc74d80d2bef122b7764c93856e403e8de5b078336d7f9e1a\ncase \"$(stat -c %a /data/security/receive-secret-scan/20261018-73ee04078786/receive-secret-scan)\" in 755|775) ;; *) exit 21;; esac\ntest -f /data/security/receive-secret-scan/20261018-73ee04078786/gitleaks && test ! -L /data/security/receive-secret-scan/20261018-73ee04078786/gitleaks\ntest \"$(stat -c %u /data/security/receive-secret-scan/20261018-73ee04078786/gitleaks)\" = 1000\ntest \"$(sha256sum /data/security/receive-secret-scan/20261018-73ee04078786/gitleaks | cut -d \" \" -f 1)\" = 88f91962aa2f93ac6ab281d553b9e125f5197bbbce38f9f2437f7299c32e5509\ncase \"$(stat -c %a /data/security/receive-secret-scan/20261018-73ee04078786/gitleaks)\" in 755|775) ;; *) exit 21;; esac\nif [ \"${1:-check}\" = apply ]; then\n chmod 755 /data/security/receive-secret-scan/20261018-73ee04078786/receive-secret-scan\n test \"$(stat -c %a /data/security/receive-secret-scan/20261018-73ee04078786/receive-secret-scan)\" = 755\n chmod 755 /data/security/receive-secret-scan/20261018-73ee04078786/gitleaks\n test \"$(stat -c %a /data/security/receive-secret-scan/20261018-73ee04078786/gitleaks)\" = 755\nfi\n\n#!/bin/sh\nset +vx\nset -eu\ntest \"$(id -u)\" = 1000\ntest -d /data && test ! -L /data\ntest -d /data/security && test ! -L /data/security\ntest -d /data/security/receive-secret-scan && test ! -L /data/security/receive-secret-scan\ntest -d /data/security/receive-secret-scan/20261027-b735d052a49f && test ! -L /data/security/receive-secret-scan/20261027-b735d052a49f\ntest -f /data/security/receive-secret-scan/20261027-b735d052a49f/receive-secret-scan && test ! -L /data/security/receive-secret-scan/20261027-b735d052a49f/receive-secret-scan\ntest \"$(stat -c %u /data/security/receive-secret-scan/20261027-b735d052a49f/receive-secret-scan)\" = 1000\ntest \"$(sha256sum /data/security/receive-secret-scan/20261027-b735d052a49f/receive-secret-scan | cut -d \" \" -f 1)\" = b735d052a49fc8e9cca14cddee074c5429352ad71841eb6548459294f15683a2\ncase \"$(stat -c %a /data/security/receive-secret-scan/20261027-b735d052a49f/receive-secret-scan)\" in 755|775) ;; *) exit 21;; esac\ntest -f /data/security/receive-secret-scan/20261027-b735d052a49f/gitleaks && test ! -L /data/security/receive-secret-scan/20261027-b735d052a49f/gitleaks\ntest \"$(stat -c %u /data/security/receive-secret-scan/20261027-b735d052a49f/gitleaks)\" = 1000\ntest \"$(sha256sum /data/security/receive-secret-scan/20261027-b735d052a49f/gitleaks | cut -d \" \" -f 1)\" = 88f91962aa2f93ac6ab281d553b9e125f5197bbbce38f9f2437f7299c32e5509\ncase \"$(stat -c %a /data/security/receive-secret-scan/20261027-b735d052a49f/gitleaks)\" in 755|775) ;; *) exit 21;; esac\nif [ \"${1:-check}\" = apply ]; then\n chmod 755 /data/security/receive-secret-scan/20261027-b735d052a49f/receive-secret-scan\n test \"$(stat -c %a /data/security/receive-secret-scan/20261027-b735d052a49f/receive-secret-scan)\" = 755\n chmod 755 /data/security/receive-secret-scan/20261027-b735d052a49f/gitleaks\n test \"$(stat -c %a /data/security/receive-secret-scan/20261027-b735d052a49f/gitleaks)\" = 755\nfi\n\n#!/bin/sh\nset +vx\nset -eu\ntest \"$(id -u)\" = 1000\ntest -d /data && test ! -L /data\ntest -d /data/security && test ! -L /data/security\ntest -d /data/security/receive-secret-scan && test ! -L /data/security/receive-secret-scan\ntest -d /data/security/receive-secret-scan/20261102-d7dbd0ef5731 && test ! -L /data/security/receive-secret-scan/20261102-d7dbd0ef5731\ntest -f /data/security/receive-secret-scan/20261102-d7dbd0ef5731/receive-secret-scan && test ! -L /data/security/receive-secret-scan/20261102-d7dbd0ef5731/receive-secret-scan\ntest \"$(stat -c %u /data/security/receive-secret-scan/20261102-d7dbd0ef5731/receive-secret-scan)\" = 1000\ntest \"$(sha256sum /data/security/receive-secret-scan/20261102-d7dbd0ef5731/receive-secret-scan | cut -d \" \" -f 1)\" = d7dbd0ef57312ab9507874bb5fd67c49bef637b6d7dee121e526227307af4bf0\ncase \"$(stat -c %a /data/security/receive-secret-scan/20261102-d7dbd0ef5731/receive-secret-scan)\" in 755|775) ;; *) exit 21;; esac\nif [ \"${1:-check}\" = apply ]; then chmod 755 /data/security/receive-secret-scan/20261102-d7dbd0ef5731/receive-secret-scan; test \"$(stat -c %a /data/security/receive-secret-scan/20261102-d7dbd0ef5731/receive-secret-scan)\" = 755; fi\ntest -f /data/security/receive-secret-scan/20261102-d7dbd0ef5731/gitleaks && test ! -L /data/security/receive-secret-scan/20261102-d7dbd0ef5731/gitleaks\ntest \"$(stat -c %u /data/security/receive-secret-scan/20261102-d7dbd0ef5731/gitleaks)\" = 1000\ntest \"$(sha256sum /data/security/receive-secret-scan/20261102-d7dbd0ef5731/gitleaks | cut -d \" \" -f 1)\" = 88f91962aa2f93ac6ab281d553b9e125f5197bbbce38f9f2437f7299c32e5509\ncase \"$(stat -c %a /data/security/receive-secret-scan/20261102-d7dbd0ef5731/gitleaks)\" in 755|775) ;; *) exit 21;; esac\nif [ \"${1:-check}\" = apply ]; then chmod 755 /data/security/receive-secret-scan/20261102-d7dbd0ef5731/gitleaks; test \"$(stat -c %a /data/security/receive-secret-scan/20261102-d7dbd0ef5731/gitleaks)\" = 755; fi\n", + "sh", + "apply" + ], + "new_hook_sha256": "619d9420813905f34413c1d7766d6da79ac2713cb9b02d1eacd61ad610dafd23", + "new_adapter_sha256": "d7dbd0ef57312ab9507874bb5fd67c49bef637b6d7dee121e526227307af4bf0" +} diff --git a/docs/changes/2026-11-01-scanner-json-digest/native-r2/packet.json b/docs/changes/2026-11-01-scanner-json-digest/native-r2/packet.json new file mode 100644 index 00000000..ccb21968 --- /dev/null +++ b/docs/changes/2026-11-01-scanner-json-digest/native-r2/packet.json @@ -0,0 +1,29 @@ +{ + "proposal_id": "SYNC-SCANNER-JSON-DIGEST-20261101", + "revision": 2, + "files": { + "PLAN.md": "093a18d5e66316af7f5dc91d466467000e5b31e7b064eeca6cfd979583d65424", + "after-hook": "619d9420813905f34413c1d7766d6da79ac2713cb9b02d1eacd61ad610dafd23", + "apps-source-changes.json": "311970f9155f704713145dfd864b3d73f42518cd86e4ffae12604b4f04cb4713", + "apps-source.patch": "cecc1f0aa2e1751c61b9c9453849ae29d09cdd5116802a084b01f0677496addb", + "before-hook": "d1df801be39f2cd4ac2a9e71bbd1341ccd2723d236c9c265853b289cc6df221f", + "configmap.json": "e91a3405b6524c7dcdfa82d6fd4efc17570e430f833b265892e4f223a19d7689", + "filesystem-r1.json": "0f3f9ddc7a52e7ed583536f5804d737b55ce4075ced77fba35c0bc7f81f70072", + "filesystem-r2.json": "0a17b575761aeb3deee8b4b54d8440b74e1d3688128d5ab96d43af801ee4d832", + "fixture-dispatcher.sh": "38a6d661aee7dd3c64e15b0baefec4c2410f75de4543d9465d69c1d26a80d2b8", + "forgejo-values.yaml": "4bd65a32c97c9aa75f7fb08025b463c3acf060e82ac32f56feab58f1193cbd7d", + "hooks.sh": "d6d6903983d673381afd37f14004069b47cc1e3a28410ccfed93720945924b34", + "inputs.json": "a2c28e5fd3a572d2b57a839a36c1f9bbf47c369b6a46748da7212ce82595fa86", + "preflight-receipt.json": "4050e9a2d17e1cfc69621e2c48d3339d46fbde62be2a2ce4cc85e8970647f82e", + "probe.sh": "b65f4bc001e779888c35ca857485195da16df199a5ab1ec4c0f60fc369e6c5b8", + "proposal.json": "8c1e23c3b179f2a316042c43a4847f212a0c03466845fa92e00c2fa5c6c4e375", + "rollback-probe.sh": "503760ec33d429cf4d02cacc3437088e40b5cbe07957a279652f605a1bf00476", + "rollout.py": "bffa9d66ce9ae471a0abe7179c30a6005ef8719b526cfdfd9e3a34f1dae4b1fb", + "scanner-source-changes.json": "20e7ded830d575006969db85bda144b179bb886e44ba92947d4790d110131cf0", + "source_followthrough.py": "3fb99358829f0891d01e056ebf8b65f9dfca96aafa211a2674065fd20cfd811b", + "stage.sh": "10cfdbeb308036ba153842943e8b6fa600cac94be842081ea7ce88245f787eeb", + "test-validation.json": "386b9558ed7af725e93ea2315a0ec489b4ff2071eed92c87a43bbf36163099c7", + "test_filesystem.py": "f02df3c07e222fa833ad1b7322c01d56f626606dfe64b87c59656c6edf916ace", + "test_rollout.py": "42bf0d411e39befbfe39ef4f18857e3b5c2f548f38ff52f94c8186418820b0b2" + } +} diff --git a/docs/changes/2026-11-01-scanner-json-digest/native-r2/preflight-receipt.json b/docs/changes/2026-11-01-scanner-json-digest/native-r2/preflight-receipt.json new file mode 100644 index 00000000..318f8725 --- /dev/null +++ b/docs/changes/2026-11-01-scanner-json-digest/native-r2/preflight-receipt.json @@ -0,0 +1,49 @@ +{ + "read_only": true, + "deployment_uid": "1faefb26-cccd-4d0e-b509-70a7eb0b4f4d", + "pod": "forge-79c7fb84db-k5wtm", + "pod_uid": "2c9f911a-403e-4183-a027-2b8d48742348", + "deployment_name": "forge", + "container": "gitea", + "node_uid": "0659e49e-6539-480a-8b0f-2969dd88cb05", + "node_name": "239.62.205.92.host.secureserver.net", + "guard_index": 0, + "guard_command": [ + "sh", + "-c", + "#!/bin/sh\nset +vx\nset -eu\ntest \"$(id -u)\" = 1000\ntest -d /data && test ! -L /data\ntest -d /data/security && test ! -L /data/security\ntest -d /data/security/receive-secret-scan && test ! -L /data/security/receive-secret-scan\ntest -d /data/security/receive-secret-scan/20261012-57a0642b6251 && test ! -L /data/security/receive-secret-scan/20261012-57a0642b6251\ntest -f /data/security/receive-secret-scan/20261012-57a0642b6251/receive-secret-scan && test ! -L /data/security/receive-secret-scan/20261012-57a0642b6251/receive-secret-scan\ntest \"$(stat -c %u /data/security/receive-secret-scan/20261012-57a0642b6251/receive-secret-scan)\" = 1000\ntest \"$(sha256sum /data/security/receive-secret-scan/20261012-57a0642b6251/receive-secret-scan | cut -d \" \" -f 1)\" = 57a0642b625105440fe146fceb4d244fd2dd08a75beb316831675643e6cc27ec\ncase \"$(stat -c %a /data/security/receive-secret-scan/20261012-57a0642b6251/receive-secret-scan)\" in 755|775) ;; *) exit 21;; esac\ntest -f /data/security/receive-secret-scan/20261012-57a0642b6251/gitleaks && test ! -L /data/security/receive-secret-scan/20261012-57a0642b6251/gitleaks\ntest \"$(stat -c %u /data/security/receive-secret-scan/20261012-57a0642b6251/gitleaks)\" = 1000\ntest \"$(sha256sum /data/security/receive-secret-scan/20261012-57a0642b6251/gitleaks | cut -d \" \" -f 1)\" = 88f91962aa2f93ac6ab281d553b9e125f5197bbbce38f9f2437f7299c32e5509\ncase \"$(stat -c %a /data/security/receive-secret-scan/20261012-57a0642b6251/gitleaks)\" in 755|775) ;; *) exit 21;; esac\nif [ \"${1:-check}\" = apply ]; then\n chmod 755 /data/security/receive-secret-scan/20261012-57a0642b6251/receive-secret-scan\n test \"$(stat -c %a /data/security/receive-secret-scan/20261012-57a0642b6251/receive-secret-scan)\" = 755\n chmod 755 /data/security/receive-secret-scan/20261012-57a0642b6251/gitleaks\n test \"$(stat -c %a /data/security/receive-secret-scan/20261012-57a0642b6251/gitleaks)\" = 755\nfi\n\n#!/bin/sh\nset +vx\nset -eu\ntest \"$(id -u)\" = 1000\ntest -d /data && test ! -L /data\ntest -d /data/security && test ! -L /data/security\ntest -d /data/security/receive-secret-scan && test ! -L /data/security/receive-secret-scan\ntest -d /data/security/receive-secret-scan/20261018-73ee04078786 && test ! -L /data/security/receive-secret-scan/20261018-73ee04078786\ntest -f /data/security/receive-secret-scan/20261018-73ee04078786/receive-secret-scan && test ! -L /data/security/receive-secret-scan/20261018-73ee04078786/receive-secret-scan\ntest \"$(stat -c %u /data/security/receive-secret-scan/20261018-73ee04078786/receive-secret-scan)\" = 1000\ntest \"$(sha256sum /data/security/receive-secret-scan/20261018-73ee04078786/receive-secret-scan | cut -d \" \" -f 1)\" = 73ee04078786801fc74d80d2bef122b7764c93856e403e8de5b078336d7f9e1a\ncase \"$(stat -c %a /data/security/receive-secret-scan/20261018-73ee04078786/receive-secret-scan)\" in 755|775) ;; *) exit 21;; esac\ntest -f /data/security/receive-secret-scan/20261018-73ee04078786/gitleaks && test ! -L /data/security/receive-secret-scan/20261018-73ee04078786/gitleaks\ntest \"$(stat -c %u /data/security/receive-secret-scan/20261018-73ee04078786/gitleaks)\" = 1000\ntest \"$(sha256sum /data/security/receive-secret-scan/20261018-73ee04078786/gitleaks | cut -d \" \" -f 1)\" = 88f91962aa2f93ac6ab281d553b9e125f5197bbbce38f9f2437f7299c32e5509\ncase \"$(stat -c %a /data/security/receive-secret-scan/20261018-73ee04078786/gitleaks)\" in 755|775) ;; *) exit 21;; esac\nif [ \"${1:-check}\" = apply ]; then\n chmod 755 /data/security/receive-secret-scan/20261018-73ee04078786/receive-secret-scan\n test \"$(stat -c %a /data/security/receive-secret-scan/20261018-73ee04078786/receive-secret-scan)\" = 755\n chmod 755 /data/security/receive-secret-scan/20261018-73ee04078786/gitleaks\n test \"$(stat -c %a /data/security/receive-secret-scan/20261018-73ee04078786/gitleaks)\" = 755\nfi\n\n#!/bin/sh\nset +vx\nset -eu\ntest \"$(id -u)\" = 1000\ntest -d /data && test ! -L /data\ntest -d /data/security && test ! -L /data/security\ntest -d /data/security/receive-secret-scan && test ! -L /data/security/receive-secret-scan\ntest -d /data/security/receive-secret-scan/20261027-b735d052a49f && test ! -L /data/security/receive-secret-scan/20261027-b735d052a49f\ntest -f /data/security/receive-secret-scan/20261027-b735d052a49f/receive-secret-scan && test ! -L /data/security/receive-secret-scan/20261027-b735d052a49f/receive-secret-scan\ntest \"$(stat -c %u /data/security/receive-secret-scan/20261027-b735d052a49f/receive-secret-scan)\" = 1000\ntest \"$(sha256sum /data/security/receive-secret-scan/20261027-b735d052a49f/receive-secret-scan | cut -d \" \" -f 1)\" = b735d052a49fc8e9cca14cddee074c5429352ad71841eb6548459294f15683a2\ncase \"$(stat -c %a /data/security/receive-secret-scan/20261027-b735d052a49f/receive-secret-scan)\" in 755|775) ;; *) exit 21;; esac\ntest -f /data/security/receive-secret-scan/20261027-b735d052a49f/gitleaks && test ! -L /data/security/receive-secret-scan/20261027-b735d052a49f/gitleaks\ntest \"$(stat -c %u /data/security/receive-secret-scan/20261027-b735d052a49f/gitleaks)\" = 1000\ntest \"$(sha256sum /data/security/receive-secret-scan/20261027-b735d052a49f/gitleaks | cut -d \" \" -f 1)\" = 88f91962aa2f93ac6ab281d553b9e125f5197bbbce38f9f2437f7299c32e5509\ncase \"$(stat -c %a /data/security/receive-secret-scan/20261027-b735d052a49f/gitleaks)\" in 755|775) ;; *) exit 21;; esac\nif [ \"${1:-check}\" = apply ]; then\n chmod 755 /data/security/receive-secret-scan/20261027-b735d052a49f/receive-secret-scan\n test \"$(stat -c %a /data/security/receive-secret-scan/20261027-b735d052a49f/receive-secret-scan)\" = 755\n chmod 755 /data/security/receive-secret-scan/20261027-b735d052a49f/gitleaks\n test \"$(stat -c %a /data/security/receive-secret-scan/20261027-b735d052a49f/gitleaks)\" = 755\nfi\n", + "sh", + "apply" + ], + "guard_args": null, + "volume_index": 2, + "volume": { + "configMap": { + "defaultMode": 365, + "name": "forgejo-secret-template-d1df801be39f" + }, + "name": "secret-scan-template" + }, + "strategy": { + "type": "Recreate" + }, + "resources": [ + { + "name": "gitea", + "resources": { + "limits": { + "memory": "1536Mi" + }, + "requests": { + "cpu": "200m", + "memory": "1104Mi" + } + } + } + ], + "old_configmap": "forgejo-secret-template-d1df801be39f", + "old_hook_sha256": "d1df801be39f2cd4ac2a9e71bbd1341ccd2723d236c9c265853b289cc6df221f", + "old_adapter_sha256": "b735d052a49fc8e9cca14cddee074c5429352ad71841eb6548459294f15683a2", + "scanner_sha256": "88f91962aa2f93ac6ab281d553b9e125f5197bbbce38f9f2437f7299c32e5509", + "allocated_resources": "(Total limits may be over 100 percent, i.e., overcommitted.)\n Resource Requests Limits\n -------- -------- ------\n cpu 2720m (68%) 40600m (1015%)\n memory 12236Mi (76%) 41026Mi (256%)\n ephemeral-storage 256Mi (0%) 1Gi (0%)\n hugepages-1Gi 0 (0%) 0 (0%)\n hugepages-2Mi 0 (0%) 0 (0%)" +} diff --git a/docs/changes/2026-11-01-scanner-json-digest/native-r2/probe.sh b/docs/changes/2026-11-01-scanner-json-digest/native-r2/probe.sh new file mode 100644 index 00000000..79ec5dd0 --- /dev/null +++ b/docs/changes/2026-11-01-scanner-json-digest/native-r2/probe.sh @@ -0,0 +1,80 @@ +#!/bin/sh +# Exact staged executable, real receive-pack, private synthetic tmpfs inputs. +set +vx +set -eu +PATH=/usr/bin:/bin +export PATH +version=$1 dispatcher=$2 +test "$(id -u)" = 1000 +test "$(stat -f -c %t /dev/shm)" = 1021994 +umask 077 +base=$(mktemp -d /dev/shm/forge-receive-activation-XXXXXXXX) +printf '%s' receive-batch-probe > "$base/.owner" +phase=setup +cleanup() { + code=$? + trap - EXIT HUP INT TERM + if test "$code" != 0; then printf '{"probe_phase":"%s","exit_code":%s,"result":"unverified"}\n' "$phase" "$code" >&2; fi + if test -d "$base" && test ! -L "$base" && test "$(cat "$base/.owner")" = receive-batch-probe; then + rm -rf -- "$base" || code=41 + else code=41; fi + exit "$code" +} +trap cleanup EXIT +trap 'exit 42' HUP INT TERM +export HOME="$base" TMPDIR="$base" GIT_CONFIG_NOSYSTEM=1 GIT_CONFIG_GLOBAL=/dev/null GIT_TERMINAL_PROMPT=0 +mkdir -p "$base/template/hooks/pre-receive.d" +hook="$base/template/hooks/pre-receive.d/10-custodian-secret-scan" +printf '#!/bin/sh\nset +vx\nset -eu\nexec %s/receive-secret-scan --scanner %s/gitleaks\n' "$version" "$version" > "$hook" +chmod 755 "$hook" +git -c init.templateDir="$base/template" init -q --bare "$base/server.git" +cp "$dispatcher" "$base/server.git/hooks/pre-receive" +chmod 755 "$base/server.git/hooks/pre-receive" +git init -q -b main "$base/client" +git -C "$base/client" config core.hooksPath "$base/no-hooks" +git -C "$base/client" config user.name 'Custodian isolated receive probe' +git -C "$base/client" config user.email 'probe@example.test' +printf 'clean baseline\n' > "$base/client/source.txt" +printf '{"values/secrets-engine.yaml": "%s"}\n' "$(printf synthetic-public-digest | sha256sum | cut -d ' ' -f 1)" > "$base/client/source-pin.json" +git -C "$base/client" add source.txt source-pin.json +git -C "$base/client" commit -qm 'isolated clean baseline' +phase=clean_push +git -C "$base/client" push "$base/server.git" HEAD:refs/heads/main > "$base/clean.out" 2>&1 +grep -q '"status":"clean"' "$base/clean.out" +initial=$(git -C "$base/client" rev-parse HEAD) +phase=synthetic_key +ssh-keygen -q -t ed25519 -N '' -f "$base/synthetic-key" >/dev/null 2>&1 +cp "$base/synthetic-key" "$base/client/fixture.txt" +git -C "$base/client" add fixture.txt +git -C "$base/client" commit -qm 'isolated synthetic fixture' +rejected=$(git -C "$base/client" rev-parse HEAD) +git -C "$base/client" rm -q fixture.txt +git -C "$base/client" commit -qm 'remove fixture from tip' +phase=blocked_history +if git -C "$base/client" push "$base/server.git" HEAD:refs/heads/main > "$base/blocked.out" 2>&1; then exit 43; fi +grep -q '"status":"blocked"' "$base/blocked.out" +if grep -q 'BEGIN OPENSSH PRIVATE KEY' "$base/blocked.out"; then exit 44; fi +if git -C "$base/server.git" cat-file -e "$rejected" >/dev/null 2>&1; then exit 45; fi +test "$(git -C "$base/server.git" rev-parse refs/heads/main)" = "$initial" +git -C "$base/client" checkout -qb recovery "$initial" +printf 'clean recovery\n' > "$base/client/source.txt" +git -C "$base/client" add source.txt +git -C "$base/client" commit -qm 'isolated clean recovery' +phase=unavailable_scanner +recovery=$(git -C "$base/client" rev-parse HEAD) +cp "$hook" "$base/saved-hook" +printf '#!/bin/sh\nexec %s/receive-secret-scan --scanner /nonexistent\n' "$version" > "$base/server.git/hooks/pre-receive.d/10-custodian-secret-scan" +if git -C "$base/client" push "$base/server.git" HEAD:refs/heads/main > "$base/unavailable.out" 2>&1; then exit 46; fi +grep -q '"status":"unverified"' "$base/unavailable.out" +test "$(git -C "$base/server.git" rev-parse refs/heads/main)" = "$initial" +if git -C "$base/server.git" cat-file -e "$recovery" >/dev/null 2>&1; then exit 47; fi +cp "$base/saved-hook" "$base/server.git/hooks/pre-receive.d/10-custodian-secret-scan" +phase=clean_recovery +git -C "$base/client" push "$base/server.git" HEAD:refs/heads/main > "$base/recovered.out" 2>&1 +grep -q '"status":"clean"' "$base/recovered.out" +test "$(git -C "$base/server.git" rev-parse refs/heads/main)" = "$recovery" +trap - EXIT HUP INT TERM +test "$(cat "$base/.owner")" = receive-batch-probe +rm -rf -- "$base" +test ! -e "$base" +printf '{"json_digest_clean_push":true,"intermediate_secret_refused":true,"quarantine_discarded":true,"scanner_unavailable_refused":true,"clean_recovery":true,"temporary_probe_removed":true,"builtin_authorization_excluded":true}\n' diff --git a/docs/changes/2026-11-01-scanner-json-digest/native-r2/proposal.json b/docs/changes/2026-11-01-scanner-json-digest/native-r2/proposal.json new file mode 100644 index 00000000..9abcaa99 --- /dev/null +++ b/docs/changes/2026-11-01-scanner-json-digest/native-r2/proposal.json @@ -0,0 +1,31 @@ +{ + "proposal_id": "SYNC-SCANNER-JSON-DIGEST-20261101", + "revision": 2, + "original_revision": 1, + "original_proposal_sha256": "75bc6e4bf1a98444cd844306bf8f3f81aca1e31b1609081905fd42ed1b76ae42", + "target": "railiance01 forgejo/forge and all existing known scanner fragments (maximum 500 repositories); paired repo-manager, railiance-forge and Apps source", + "expected_result": "Equivalent JSON filename/exact lowercase SHA256 metadata accepted; full scan scope and secret refusal preserved", + "verification_ref": "PLAN.md; test-validation.json; native guarded dry-run and probes are execution prerequisites", + "rollback_ref": "rollout.py rollback; PLAN.md; old artifacts retained", + "cost_estimate_eur": { + "additional_infrastructure": 0, + "attended_minutes": [ + 15, + 30 + ], + "founder_time": "unpriced" + }, + "risk_estimate_eur": { + "complete_credible_loss_bound": null, + "illustrative_recovery": 100, + "basis": "one recovery hour at illustrative EUR100/hour; scanner-policy loss unpriced; supervised judgement required" + }, + "estimated_git_hosting_interruption_minutes": [ + 1, + 3 + ], + "rollback_readiness_timeout_seconds": 180, + "authority": "pending exact founder approval; not executed; no authority or promotion inferred", + "source_preimages": "scanner-source-changes.json and apps-source-changes.json", + "scope_limits": "PLAN.md; no UUID, fingerprint or prose exceptions; preserve all history/object scan coverage" +} diff --git a/docs/changes/2026-11-01-scanner-json-digest/native-r2/rollback-probe.sh b/docs/changes/2026-11-01-scanner-json-digest/native-r2/rollback-probe.sh new file mode 100644 index 00000000..86a01d27 --- /dev/null +++ b/docs/changes/2026-11-01-scanner-json-digest/native-r2/rollback-probe.sh @@ -0,0 +1,79 @@ +#!/bin/sh +# Exact staged executable, real receive-pack, private synthetic tmpfs inputs. +set +vx +set -eu +PATH=/usr/bin:/bin +export PATH +version=$1 dispatcher=$2 +test "$(id -u)" = 1000 +test "$(stat -f -c %t /dev/shm)" = 1021994 +umask 077 +base=$(mktemp -d /dev/shm/forge-receive-activation-XXXXXXXX) +printf '%s' receive-batch-probe > "$base/.owner" +phase=setup +cleanup() { + code=$? + trap - EXIT HUP INT TERM + if test "$code" != 0; then printf '{"probe_phase":"%s","exit_code":%s,"result":"unverified"}\n' "$phase" "$code" >&2; fi + if test -d "$base" && test ! -L "$base" && test "$(cat "$base/.owner")" = receive-batch-probe; then + rm -rf -- "$base" || code=41 + else code=41; fi + exit "$code" +} +trap cleanup EXIT +trap 'exit 42' HUP INT TERM +export HOME="$base" TMPDIR="$base" GIT_CONFIG_NOSYSTEM=1 GIT_CONFIG_GLOBAL=/dev/null GIT_TERMINAL_PROMPT=0 +mkdir -p "$base/template/hooks/pre-receive.d" +hook="$base/template/hooks/pre-receive.d/10-custodian-secret-scan" +printf '#!/bin/sh\nset +vx\nset -eu\nexec %s/receive-secret-scan --scanner %s/gitleaks\n' "$version" "$version" > "$hook" +chmod 755 "$hook" +git -c init.templateDir="$base/template" init -q --bare "$base/server.git" +cp "$dispatcher" "$base/server.git/hooks/pre-receive" +chmod 755 "$base/server.git/hooks/pre-receive" +git init -q -b main "$base/client" +git -C "$base/client" config core.hooksPath "$base/no-hooks" +git -C "$base/client" config user.name 'Custodian isolated receive probe' +git -C "$base/client" config user.email 'probe@example.test' +printf 'clean baseline\n' > "$base/client/source.txt" +git -C "$base/client" add source.txt +git -C "$base/client" commit -qm 'isolated clean baseline' +phase=clean_push +git -C "$base/client" push "$base/server.git" HEAD:refs/heads/main > "$base/clean.out" 2>&1 +grep -q '"status":"clean"' "$base/clean.out" +initial=$(git -C "$base/client" rev-parse HEAD) +phase=synthetic_key +ssh-keygen -q -t ed25519 -N '' -f "$base/synthetic-key" >/dev/null 2>&1 +cp "$base/synthetic-key" "$base/client/fixture.txt" +git -C "$base/client" add fixture.txt +git -C "$base/client" commit -qm 'isolated synthetic fixture' +rejected=$(git -C "$base/client" rev-parse HEAD) +git -C "$base/client" rm -q fixture.txt +git -C "$base/client" commit -qm 'remove fixture from tip' +phase=blocked_history +if git -C "$base/client" push "$base/server.git" HEAD:refs/heads/main > "$base/blocked.out" 2>&1; then exit 43; fi +grep -q '"status":"blocked"' "$base/blocked.out" +if grep -q 'BEGIN OPENSSH PRIVATE KEY' "$base/blocked.out"; then exit 44; fi +if git -C "$base/server.git" cat-file -e "$rejected" >/dev/null 2>&1; then exit 45; fi +test "$(git -C "$base/server.git" rev-parse refs/heads/main)" = "$initial" +git -C "$base/client" checkout -qb recovery "$initial" +printf 'clean recovery\n' > "$base/client/source.txt" +git -C "$base/client" add source.txt +git -C "$base/client" commit -qm 'isolated clean recovery' +phase=unavailable_scanner +recovery=$(git -C "$base/client" rev-parse HEAD) +cp "$hook" "$base/saved-hook" +printf '#!/bin/sh\nexec %s/receive-secret-scan --scanner /nonexistent\n' "$version" > "$base/server.git/hooks/pre-receive.d/10-custodian-secret-scan" +if git -C "$base/client" push "$base/server.git" HEAD:refs/heads/main > "$base/unavailable.out" 2>&1; then exit 46; fi +grep -q '"status":"unverified"' "$base/unavailable.out" +test "$(git -C "$base/server.git" rev-parse refs/heads/main)" = "$initial" +if git -C "$base/server.git" cat-file -e "$recovery" >/dev/null 2>&1; then exit 47; fi +cp "$base/saved-hook" "$base/server.git/hooks/pre-receive.d/10-custodian-secret-scan" +phase=clean_recovery +git -C "$base/client" push "$base/server.git" HEAD:refs/heads/main > "$base/recovered.out" 2>&1 +grep -q '"status":"clean"' "$base/recovered.out" +test "$(git -C "$base/server.git" rev-parse refs/heads/main)" = "$recovery" +trap - EXIT HUP INT TERM +test "$(cat "$base/.owner")" = receive-batch-probe +rm -rf -- "$base" +test ! -e "$base" +printf '{"clean_push":true,"intermediate_secret_refused":true,"quarantine_discarded":true,"scanner_unavailable_refused":true,"clean_recovery":true,"temporary_probe_removed":true,"builtin_authorization_excluded":true}\n' diff --git a/docs/changes/2026-11-01-scanner-json-digest/native-r2/rollout.py b/docs/changes/2026-11-01-scanner-json-digest/native-r2/rollout.py new file mode 100644 index 00000000..49a71d45 --- /dev/null +++ b/docs/changes/2026-11-01-scanner-json-digest/native-r2/rollout.py @@ -0,0 +1,330 @@ +"""Founder-run exact scanner rollout. Default: read-only inventory and server dry-run. + +This acknowledgement check binds reviewed bytes; it is not an approval service or +credential boundary. Run only in the founder's attended execution environment. +Raw Kubernetes errors/objects, Git contents and scanner findings are never printed. +""" +from pathlib import Path +import argparse +import base64 +import copy +import hashlib +import json +import re +import shlex +import subprocess +import time + +HERE = Path(__file__).resolve().parent +HOOK = 'pre-receive.d/10-custodian-secret-scan' +REPO = r'/data/git/gitea-repositories/[a-z0-9_.-]+/[a-z0-9_.-]+\.git' +RECEIPT = Path('/tmp/forge-scanner-rollout-receipt.json') + + +def sha(raw): + return hashlib.sha256(raw).hexdigest() + + +def encode(value): + return json.dumps(value, sort_keys=True, separators=(',', ':')).encode() + + +def ensure(condition, reason): + if not condition: + raise RuntimeError(reason) + + +def verify_packet(): + manifest = json.loads((HERE / 'packet.json').read_bytes()) + for name, digest in manifest['files'].items(): + ensure(Path(name).name == name, 'unsafe-packet-path') + ensure(sha((HERE / name).read_bytes()) == digest, 'packet-file-drift:' + name) + return sha((HERE / 'packet.json').read_bytes()) + + +def patch_for(d, cfg, reverse=False): + spec = d['spec']['template']['spec'] + gi, vi = cfg['guard_index'], cfg['volume_index'] + guard = cfg['new_guard_command'] if reverse else cfg['guard_command'] + replacement = cfg['guard_command'] if reverse else cfg['new_guard_command'] + cm = cfg['new_configmap'] if reverse else cfg['old_configmap'] + dest = cfg['old_configmap'] if reverse else cfg['new_configmap'] + ensure(d['metadata']['uid'] == cfg['deployment_uid'], 'deployment-identity-drift') + ensure(d['spec'].get('strategy') == cfg['strategy'], 'strategy-drift') + ensure(spec['initContainers'][gi]['name'] == 'verify-secret-scanner', 'guard-index-drift') + ensure(spec['initContainers'][gi].get('args') == cfg['guard_args'], 'guard-args-drift') + ensure(spec['initContainers'][gi]['command'] == guard, 'guard-command-drift') + vol = copy.deepcopy(cfg['volume']); vol['configMap']['name'] = cm + ensure(spec['volumes'][vi] == vol, 'volume-drift') + a = f'/spec/template/spec/initContainers/{gi}/command' + b = f'/spec/template/spec/volumes/{vi}' + return [ + {'op': 'test', 'path': '/metadata/uid', 'value': cfg['deployment_uid']}, + {'op': 'test', 'path': '/metadata/resourceVersion', 'value': d['metadata']['resourceVersion']}, + {'op': 'test', 'path': a, 'value': guard}, + {'op': 'test', 'path': b, 'value': vol}, + {'op': 'replace', 'path': a, 'value': replacement}, + {'op': 'replace', 'path': b + '/configMap/name', 'value': dest}, + ] + + +def inventory_script(): + return r'''set +vx +set -eu +export PATH=/usr/bin:/bin +test "$(id -u)" = 1000 +for parent in /data /data/git /data/git/gitea-repositories; do test -d "$parent" && test ! -L "$parent"; done +count=0 +for repo in /data/git/gitea-repositories/*/*.git; do + test -d "$repo" || continue + count=$((count+1)); test "$count" -le 500 + for parent in "${repo%/*}" "$repo" "$repo/hooks" "$repo/hooks/pre-receive.d"; do test -d "$parent" && test ! -L "$parent"; done + test -z "$(find "$repo/hooks" -type l -print -quit)" + test -f "$repo/hooks/pre-receive" && test -x "$repo/hooks/pre-receive" + test -f "$repo/hooks/pre-receive.d/10-custodian-secret-scan" + find "$repo/hooks" -type f ! -name '*.sample' -exec sh -c ' + for file do printf "%s %s %s " "$(stat -c %a "$file")" "$(stat -c %u "$file")" "$(stat -c %g "$file")"; sha256sum "$file"; done + ' sh {} + +done +test "$count" -gt 0 +''' + + +def parse_inventory(raw, cfg, allowed): + result = {} + for line in raw.decode().splitlines(): + parts = line.split(' ', 4) + ensure(len(parts) == 5, 'malformed-inventory') + mode, uid, gid, digest, path = parts + path = path.lstrip(' ') + match = re.fullmatch('(' + REPO + r')/hooks/([a-zA-Z0-9_.-]+(?:/[a-zA-Z0-9_.-]+)*)', path) + ensure(match is not None and re.fullmatch('[0-9a-f]{64}', digest), 'unsafe-inventory-entry') + repo, name = match.groups() + entries = result.setdefault(repo, {}) + ensure(name not in entries, 'duplicate-hook') + entries[name] = dict(mode=mode, uid=int(uid), gid=int(gid), sha256=digest) + ensure(0 < len(result) <= 500, 'empty-or-oversized-inventory') + for files in result.values(): + ensure(HOOK in files and 'pre-receive' in files, 'uncovered-repository') + f = files[HOOK] + ensure(f['uid'] == 1000 and f['gid'] == 1000 and f['mode'] in ('755', '775'), 'scanner-hook-custody-drift') + ensure(f['sha256'] in allowed, 'unknown-scanner-hook') + return result + + +def protected_equal(before, after): + ensure(set(before) <= set(after), 'repository-disappeared') + for repo, files in before.items(): + ensure({k: v for k, v in files.items() if k != HOOK} == + {k: v for k, v in after[repo].items() if k != HOOK}, 'protected-hook-drift') + for k in ('mode', 'uid', 'gid'): + ensure(files[HOOK][k] == after[repo][HOOK][k], 'scanner-hook-metadata-drift') + + +class Rollout: + def __init__(self, cfg): + self.c = cfg + self.receipt = {'proposal_id': 'SYNC-SCANNER-JSON-DIGEST-20261101', 'revision': 2, + 'events': [], 'native_mutation': False} + + def k(self, *args, data=None, timeout=60): + q = subprocess.run(['kubectl', *args], input=data, capture_output=True, timeout=timeout) + ensure(q.returncode == 0, 'kubectl-command-failed; raw diagnostics withheld') + return q.stdout + + def event(self, name, **details): + self.receipt['events'].append({'event': name, **details}) + RECEIPT.write_text(json.dumps(self.receipt, indent=2) + '\n') + print(json.dumps({'event': name, **details}), flush=True) + + def deployment(self): + d = json.loads(self.k('-n', 'forgejo', 'get', 'deployment', 'forge', '-o', 'json')) + ensure(d['metadata']['uid'] == self.c['deployment_uid'], 'deployment-identity-drift') + return d + + def target(self): + nodes = json.loads(self.k('get', 'nodes', '-o', 'json'))['items'] + ensure(len(nodes) == 1 and nodes[0]['metadata']['uid'] == self.c['node_uid'], 'node-identity-drift') + ensure(any(a['address'] == '92.205.62.239' for a in nodes[0]['status']['addresses']), 'node-address-drift') + + def ready_pod(self, d, seconds=180): + selector = ','.join(k + '=' + v for k, v in sorted(d['spec']['selector']['matchLabels'].items())) + until = time.monotonic() + seconds + while True: + rs = json.loads(self.k('-n', 'forgejo', 'get', 'replicasets', '-l', selector, '-o', 'json'))['items'] + owners = {x['metadata']['uid'] for x in rs if x['spec']['template']['spec'] == d['spec']['template']['spec'] and any(o['uid'] == self.c['deployment_uid'] for o in x['metadata'].get('ownerReferences', []))} + pods = json.loads(self.k('-n', 'forgejo', 'get', 'pods', '-l', selector, '-o', 'json'))['items'] + ready = [p for p in pods if not p['metadata'].get('deletionTimestamp') and + any(o['uid'] in owners for o in p['metadata'].get('ownerReferences', [])) and + any(c['type'] == 'Ready' and c['status'] == 'True' for c in p.get('status', {}).get('conditions', []))] + if len(ready) == 1: + # Require the current Deployment rollout, not a surviving old pod. + now = self.deployment() + if now.get('status', {}).get('observedGeneration', 0) >= now['metadata']['generation'] and now.get('status', {}).get('updatedReplicas') == now['spec'].get('replicas', 1) and now.get('status', {}).get('readyReplicas') == now['spec'].get('replicas', 1): + return ready[0]['metadata']['name'] + ensure(time.monotonic() < until, 'readiness-timeout') + time.sleep(2) + + def shell(self, pod, script, args=(), data=None, timeout=90): + return self.k('-n', 'forgejo', 'exec', '-i', pod, '-c', self.c['container'], '--', + 'sh', '-c', script, 'sh', *args, data=data, timeout=timeout) + + def inventory(self, pod, allowed): + return parse_inventory(self.shell(pod, inventory_script()), self.c, allowed) + + def dry_run(self, d): + cm = (HERE / 'configmap.json').read_bytes() + self.k('create', '--dry-run=server', '-f', '-', '-o', 'name', data=cm) + patch = patch_for(d, self.c) + self.k('-n', 'forgejo', 'patch', 'deployment', 'forge', '--type=json', '--dry-run=server', + '-p', json.dumps(patch), '-o', 'name') + diff = subprocess.run(['kubectl', 'diff', '-f', '-'], input=cm, capture_output=True, timeout=60) + ensure(diff.returncode in (0, 1), 'client-diff-failed; raw diagnostics withheld') + self.event('server-dry-run-passed', client_configmap_diff_exit=diff.returncode, changes=['append exact artifact guard', 'select immutable hook ConfigMap'], + old_configmap=self.c['old_configmap'], new_configmap=self.c['new_configmap'], + strategy='Recreate', resources_unchanged=True) + + def preflight(self): + for name in ('scanner-source-changes.json', 'apps-source-changes.json'): + for source in json.loads((HERE / name).read_bytes()): + ensure(sha(Path(source['target']).read_bytes()) == source['original_sha256'], 'owner-source-drift') + self.target() + d = self.deployment(); patch_for(d, self.c) + pod = self.ready_pod(d) + ensure(pod == self.c['pod'], 'initial-pod-name-drift') + uid = self.k('-n', 'forgejo', 'get', 'pod', pod, '-o', 'jsonpath={.metadata.uid}').decode() + ensure(uid == self.c['pod_uid'], 'initial-pod-identity-drift') + current = self.k('-n', 'forgejo', 'get', 'configmap', self.c['old_configmap'], '-o', 'jsonpath={.data.hook}') + ensure(sha(current) == self.c['old_hook_sha256'], 'old-template-drift') + self.shell(pod, self.c['guard_command'][2], ('check',)) + before = self.inventory(pod, {self.c['old_hook_sha256']}) + self.receipt['before_hooks'] = before + self.event('preflight-passed', repositories=len(before), deployment_uid=self.c['deployment_uid']) + self.dry_run(d) + return d, pod, before + + def migrate(self, pod, direction): + c = self.c + # Only public hooks/path names in private tmpfs. Child restores partial writes. + wrapper = '''set +vx +set -eu +umask 077 +test "$(stat -f -c %t /dev/shm)" = 1021994 +area=$(mktemp -d /dev/shm/forge-scanner-hooks-XXXXXXXX) +cleanup() { code=$?; trap - EXIT; rm -f -- "$area/before" "$area/after" "$area/paths"; rmdir "$area" || code=50; exit "$code"; } +trap cleanup EXIT +''' + for name in ('before', 'after'): + value = base64.b64encode((HERE / (name + '-hook')).read_bytes()).decode() + wrapper += 'printf %s ' + shlex.quote(value) + ' | base64 -d > "$area/' + name + '"\n' + wrapper += 'cat > "$area/paths"\nsh -c ' + shlex.quote((HERE / 'hooks.sh').read_text()) + ' sh "$area" "$1" "$2" "$3"\n' + before = self.inventory(pod, {c['old_hook_sha256'], c['new_hook_sha256']}) + paths = ''.join(repo + '/hooks/' + HOOK + '\n' for repo, files in sorted(before.items()) + if direction == 'rollback' or files[HOOK]['sha256'] == c['old_hook_sha256']) + if paths: + self.shell(pod, wrapper, (direction, c['old_hook_sha256'], c['new_hook_sha256']), paths.encode()) + expected = c['old_hook_sha256'] if direction == 'rollback' else c['new_hook_sha256'] + after = self.inventory(pod, {expected}) + protected_equal(before, after) + self.event('hooks-' + direction, repositories=len(after)) + return after + + def probe(self, pod, version, inventory): + dispatcher = sorted(inventory)[0] + '/hooks/pre-receive' + result = json.loads(self.shell(pod, (HERE / 'probe.sh').read_text(), (version, dispatcher), timeout=300)) + ensure(all(result[k] for k in ('json_digest_clean_push', 'intermediate_secret_refused', 'scanner_unavailable_refused', 'clean_recovery', 'temporary_probe_removed')), 'probe-incomplete') + self.event('isolated-receiver-probe-passed', version=version, checks=result) + + def rollback(self, before=None): + self.target(); d = self.deployment() + spec = d['spec']['template']['spec']; cmd = spec['initContainers'][self.c['guard_index']]['command'] + is_new = cmd == self.c['new_guard_command'] + ensure(is_new or cmd == self.c['guard_command'], 'rollback-refused-unknown-guard') + # The exact reverse patch tests both guard and volume, and current resourceVersion. + if is_new: + patch = patch_for(d, self.c, reverse=True) + self.k('-n', 'forgejo', 'patch', 'deployment', 'forge', '--type=json', '--dry-run=server', '-p', json.dumps(patch), '-o', 'name') + self.k('-n', 'forgejo', 'patch', 'deployment', 'forge', '--type=json', '-p', json.dumps(patch), '-o', 'name') + else: + patch_for(d, self.c) + pod = self.ready_pod(self.deployment()) + after = self.migrate(pod, 'rollback') + self.shell(pod, self.c['guard_command'][2], ('check',)) + mounted = self.shell(pod, 'cat /opt/forge-secret-template/hooks/pre-receive.d/10-custodian-secret-scan') + ensure(sha(mounted) == self.c['old_hook_sha256'], 'rollback-mounted-template-drift') + if before: + protected_equal(before, after) + result = json.loads(self.shell(pod, (HERE / 'rollback-probe.sh').read_text(), (self.c['old_dir'], sorted(after)[0] + '/hooks/pre-receive'), timeout=300)) + ensure(all(result[k] for k in ('clean_push', 'intermediate_secret_refused', 'scanner_unavailable_refused', 'clean_recovery', 'temporary_probe_removed')), 'rollback-probe-incomplete') + self.event('rollback-probe-passed', checks=result) + self.event('rollback-verified', old_artifacts_retained=True, staged_artifacts_retained=True) + + def apply(self, binary): + d, pod, before = self.preflight() + raw = binary.read_bytes(); ensure(sha(raw) == self.c['new_adapter_sha256'], 'candidate-binary-drift') + self.receipt['native_mutation'] = True + self.event('staging-started') + try: + self.shell(pod, (HERE / 'stage.sh').read_text(), + (self.c['new_dir'], self.c['old_dir'], self.c['new_adapter_sha256'], self.c['scanner_sha256'], + 'SYNC-SCANNER-JSON-DIGEST-20261101-R2'), data=raw) + self.shell(pod, self.c['new_guard_command'][2], ('check',)) + self.probe(pod, self.c['new_dir'], before) + # Append-only immutable ConfigMap: do not overwrite an existing object. + self.k('create', '-f', '-', '-o', 'name', data=(HERE / 'configmap.json').read_bytes()) + self.migrate(pod, 'apply') + # Refresh version immediately before guarded patch; unrelated fields preserved. + patch = patch_for(self.deployment(), self.c) + self.k('-n', 'forgejo', 'patch', 'deployment', 'forge', '--type=json', '-p', json.dumps(patch), '-o', 'name') + self.event('deployment-patched') + pod = self.ready_pod(self.deployment()) + after = self.migrate(pod, 'apply') # Includes repos created during transition. + protected_equal(before, after) + self.shell(pod, self.c['new_guard_command'][2], ('check',)) + mounted = self.shell(pod, 'cat /opt/forge-secret-template/hooks/pre-receive.d/10-custodian-secret-scan') + ensure(sha(mounted) == self.c['new_hook_sha256'], 'mounted-template-drift') + self.probe(pod, self.c['new_dir'], after) + self.event('activation-verified', repositories=len(after), hook_sha256=self.c['new_hook_sha256']) + except Exception as exc: + self.event('activation-failed', reason=str(exc) if isinstance(exc, RuntimeError) else type(exc).__name__) + try: + self.rollback(before) + except Exception as recovery: + self.event('rollback-unverified', reason=str(recovery) if isinstance(recovery, RuntimeError) else type(recovery).__name__) + raise + + +def main(): + global RECEIPT + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('mode', choices=('preflight', 'apply', 'rollback'), nargs='?', default='preflight') + parser.add_argument('--approved-digest') + parser.add_argument('--binary', type=Path, default=Path('/tmp/sync-scanner-candidate/receive-secret-scan-candidate')) + args = parser.parse_args() + if args.mode == 'preflight': + RECEIPT = Path('/tmp/forge-scanner-rollout-preflight-receipt.json') + digest = verify_packet() + cfg = json.loads((HERE / 'inputs.json').read_bytes()) + if args.mode != 'preflight': + ensure(args.approved_digest == digest, 'exact-founder-acknowledgement-required') + runner = Rollout(cfg) + runner.receipt.update(packet_sha256=digest, mode=args.mode) + if args.mode == 'apply': + ensure(not RECEIPT.exists(), 'prior-receipt-present; preserve-and-review-before-retry') + with RECEIPT.open('x') as stream: + stream.write(json.dumps(runner.receipt) + '\n') + runner.apply(args.binary) + elif args.mode == 'rollback': + prior = json.loads(RECEIPT.read_bytes()) if RECEIPT.exists() else {} + runner.receipt['prior_receipt'] = prior + runner.rollback(prior.get('before_hooks')) + else: + runner.preflight() + + +if __name__ == '__main__': + try: + main() + except Exception as error: + print(json.dumps({'result': 'refused-or-unverified', 'reason': str(error) if isinstance(error, RuntimeError) else type(error).__name__}), flush=True) + raise SystemExit(1) diff --git a/docs/changes/2026-11-01-scanner-json-digest/native-r2/scanner-source-changes.json b/docs/changes/2026-11-01-scanner-json-digest/native-r2/scanner-source-changes.json new file mode 100644 index 00000000..6b0b2fb5 --- /dev/null +++ b/docs/changes/2026-11-01-scanner-json-digest/native-r2/scanner-source-changes.json @@ -0,0 +1,26 @@ +[ + { + "target": "/home/worsch/repo-manager/src/repo_manager/secret_scan.py", + "original_sha256": "12d0e1b10c4d613401e33bfdb4012c6cb3e3719b6b1480f8d3ae2f7a27aeb73f", + "candidate_sha256": "740f64dc1e8421669e6f84fccadef50fcd3d9a802df07f62c6718f917ba55fa9", + "patch": "--- /home/worsch/repo-manager/src/repo_manager/secret_scan.py\n+++ /home/worsch/repo-manager/src/repo_manager/secret_scan.py\n@@ -55,7 +55,7 @@\n description = \"Filename key whose value is a lowercase 64-hex content digest\"\n targetRules = [\"generic-api-key\"]\n regexTarget = \"match\"\n-regexes = ['^[\\w.-]+\\.(?:md|markdown|txt|yaml|yml|json|py|toml):[ \\t]+[a-f0-9]{64}$']\n+regexes = ['^[\\w.-]+\\.(?:md|markdown|txt|yaml|yml|json|py|toml)(?::[ \\t]+[a-f0-9]{64}|\":[ \\t]+\"[a-f0-9]{64}\")$']\n \"\"\"\n \n \n" + }, + { + "target": "/home/worsch/repo-manager/tests/test_secret_scan.py", + "original_sha256": "5560f57163dc2189d189cce1c7b772ebf1adc8f8a84d388b2b42b546c10cc1c8", + "candidate_sha256": "2e6d53651ca18e787487cd961efcddc7f483637c63e6e10df399a6d4fcdae962", + "patch": "--- /home/worsch/repo-manager/tests/test_secret_scan.py\n+++ /home/worsch/repo-manager/tests/test_secret_scan.py\n@@ -479,3 +479,21 @@\n if process.poll() is None:\n process.kill(); process.wait(timeout=5)\n process.stdout.close()\n+\n+\n+@pytest.mark.parametrize(\"key\", [\"values/secrets-engine.yaml\", \"docs/runtime-externalsecret.r1.yaml\"])\n+def test_json_filename_digest_is_allowed(binary, repo, key):\n+ digest = os.urandom(32).hex()\n+ stage(repo, json.dumps({key: digest}).encode(), \"source-pin.json\")\n+ assert secret_scan.scan_staged(repo, binary)[\"status\"] == \"clean\"\n+\n+\n+@pytest.mark.parametrize(\"content\", [\n+ lambda digest: json.dumps({\"secret\": digest}),\n+ lambda digest: json.dumps({\"values/secrets-engine.yaml\": \"glpat-\" + digest}),\n+ lambda digest: json.dumps({\"values/secrets-engine.yaml\": digest + \"a\"}),\n+ lambda digest: json.dumps({\"values/secrets-engine.yaml\": digest.upper()}),\n+])\n+def test_json_digest_exception_does_not_allow_secret_shapes(binary, repo, content):\n+ stage(repo, content(os.urandom(32).hex()).encode(), \"manifest.json\")\n+ assert secret_scan.scan_staged(repo, binary)[\"status\"] == \"blocked\"\n" + }, + { + "target": "/home/worsch/railiance-forge/tools/receive-secret-scan/main.go", + "original_sha256": "a9835dfc830bde5091354debf26ae40c37fab5fdc2c1b0e483774dda0b93521a", + "candidate_sha256": "4eb494b30b845e090332d3c20070ff417c2114a479823a6097cfe0292ee18aac", + "patch": "--- /home/worsch/railiance-forge/tools/receive-secret-scan/main.go\n+++ /home/worsch/railiance-forge/tools/receive-secret-scan/main.go\n@@ -36,7 +36,7 @@\n description = \"Filename key whose value is a lowercase 64-hex content digest\"\n targetRules = [\"generic-api-key\"]\n regexTarget = \"match\"\n-regexes = ['^[\\w.-]+\\.(?:md|markdown|txt|yaml|yml|json|py|toml):[ \\t]+[a-f0-9]{64}$']\n+regexes = ['^[\\w.-]+\\.(?:md|markdown|txt|yaml|yml|json|py|toml)(?::[ \\t]+[a-f0-9]{64}|\":[ \\t]+\"[a-f0-9]{64}\")$']\n `\n const maxInput = 512 * 1024\n const maxObject = 8 * 1024 * 1024\n" + }, + { + "target": "/home/worsch/railiance-forge/tools/receive-secret-scan/batch_test.go", + "original_sha256": "4ef5669786c49cadc9e8985d77c215e34effd3e4aabeb5387cafbe51e5e89e17", + "candidate_sha256": "d94d7812a74b1cb7ff53ee9a3e137d8b524d96f4dca813ca52a9f84f800323b9", + "patch": "--- /home/worsch/railiance-forge/tools/receive-secret-scan/batch_test.go\n+++ /home/worsch/railiance-forge/tools/receive-secret-scan/batch_test.go\n@@ -188,6 +188,11 @@\n \t\t{\"inline-suppression\", append([]byte(\"# gitleaks:allow\\n\"), privateKey...), true},\n \t\t{\"filename-digest\", []byte(\" cards/access.md: \" + digest + \"\\n\"), false},\n \t\t{\"bare-keyword-digest\", []byte(\"access: \" + digest + \"\\n\"), true},\n+\t\t{\"json-filename-digest\", []byte(\"{\\\"values/secrets-engine.yaml\\\": \\\"\" + digest + \"\\\"}\\n\"), false},\n+\t\t{\"json-bare-secret-digest\", []byte(\"{\\\"secret\\\": \\\"\" + digest + \"\\\"}\\n\"), true},\n+\t\t{\"json-filename-token\", []byte(\"{\\\"values/secrets-engine.yaml\\\": \\\"\" + string(canary) + \"\\\"}\\n\"), true},\n+\t\t{\"json-filename-long-digest\", []byte(\"{\\\"values/secrets-engine.yaml\\\": \\\"\" + digest + \"a\\\"}\\n\"), true},\n+\t\t{\"json-filename-uppercase-digest\", []byte(\"{\\\"values/secrets-engine.yaml\\\": \\\"\" + strings.ToUpper(digest) + \"\\\"}\\n\"), true},\n \t} {\n \t\tt.Run(tc.name, func(t *testing.T) {\n \t\t\tstdin, e := scan(context.Background(), f, config, tc.input)\n" + } +] \ No newline at end of file diff --git a/docs/changes/2026-11-01-scanner-json-digest/native-r2/source_followthrough.py b/docs/changes/2026-11-01-scanner-json-digest/native-r2/source_followthrough.py new file mode 100644 index 00000000..53d0a397 --- /dev/null +++ b/docs/changes/2026-11-01-scanner-json-digest/native-r2/source_followthrough.py @@ -0,0 +1,86 @@ +"""Apply exactly reviewed paired scanner and Apps source after native verification. + +No Git operations or native actions. Requires founder acknowledgement of packet. +All six original hashes are checked before writing; differing owner work is refused. +""" +from pathlib import Path +import argparse +import hashlib +import importlib.util +import json +import os +import re +import tempfile + +HERE = Path(__file__).resolve().parent + + +def sha(raw): return hashlib.sha256(raw).hexdigest() + + +def patched(original, diff): + before = original.decode().splitlines(keepends=True) + result = []; cursor = 0; lines = diff.splitlines(keepends=True); i = 2 + while i < len(lines): + match = re.fullmatch(r'@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@[^\n]*\n?', lines[i]) + if not match: raise ValueError('malformed-hunk') + start = int(match[1]) - 1 + if start < cursor: raise ValueError('overlapping-hunk') + result.extend(before[cursor:start]); cursor = start; i += 1 + while i < len(lines) and not lines[i].startswith('@@ '): + line = lines[i]; tag, content = line[0], line[1:] + if tag in (' ', '-'): + if cursor >= len(before) or before[cursor] != content: raise ValueError('hunk-preimage-drift') + cursor += 1 + if tag in (' ', '+'): result.append(content) + elif tag != '-': raise ValueError('unsupported-patch-line') + i += 1 + result.extend(before[cursor:]) + return ''.join(result).encode() + + +def candidates(): + changes = json.loads((HERE / 'scanner-source-changes.json').read_bytes()) + changes += json.loads((HERE / 'apps-source-changes.json').read_bytes()) + result = [] + for change in changes: + target = Path(change['target']); raw = target.read_bytes() + if sha(raw) != change['original_sha256']: raise RuntimeError('owner-source-drift:' + str(target)) + candidate = ((HERE / change['candidate_file']).read_bytes() if 'candidate_file' in change else patched(raw, change['patch'])) + if sha(candidate) != change['candidate_sha256']: raise RuntimeError('candidate-hash-mismatch') + result.append((target, raw, candidate)) + return result + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--apply', action='store_true') + parser.add_argument('--approved-digest') + args = parser.parse_args() + spec = importlib.util.spec_from_file_location('rollout', HERE / 'rollout.py') + module = importlib.util.module_from_spec(spec); spec.loader.exec_module(module) + digest = module.verify_packet() + prepared = candidates() + if args.apply: + if args.approved_digest != digest: raise RuntimeError('exact-founder-acknowledgement-required') + receipt = json.loads(Path('/tmp/forge-scanner-rollout-receipt.json').read_bytes()) + if receipt.get('packet_sha256') != digest or receipt['events'][-1]['event'] != 'activation-verified': + raise RuntimeError('native-activation-not-verified') + for target, original, candidate in prepared: + if target.read_bytes() != original: raise RuntimeError('concurrent-owner-source-drift') + mode = target.stat().st_mode & 0o777 + fd, name = tempfile.mkstemp(prefix='.scanner-reviewed-', dir=target.parent) + try: + with os.fdopen(fd, 'wb') as stream: stream.write(candidate); stream.flush(); os.fsync(stream.fileno()) + os.chmod(name, mode) + os.replace(name, target) + finally: + if os.path.exists(name): os.unlink(name) + print(json.dumps({'result': 'applied' if args.apply else 'preimages-and-candidates-verified', 'files': len(prepared), 'packet_sha256': digest})) + + +if __name__ == '__main__': + try: main() + except Exception as error: + print(json.dumps({'result': 'refused-or-unverified', 'reason': str(error) if isinstance(error, RuntimeError) else type(error).__name__})) + raise SystemExit(1) diff --git a/docs/changes/2026-11-01-scanner-json-digest/native-r2/stage.sh b/docs/changes/2026-11-01-scanner-json-digest/native-r2/stage.sh new file mode 100644 index 00000000..b1de273c --- /dev/null +++ b/docs/changes/2026-11-01-scanner-json-digest/native-r2/stage.sh @@ -0,0 +1,58 @@ +#!/bin/sh +# Exact public executable staging. No repository/configuration changes. +set +vx +set -eu +PATH=/usr/bin:/bin +export PATH +new=$1 old=$2 adapter_sum=$3 scanner_sum=$4 marker=$5 +test "$(id -u)" = 1000 +for parent in /data /data/security /data/security/receive-secret-scan "$old"; do + test -d "$parent" && test ! -L "$parent" +done +verify() { + file=$1 sum=$2 + test -f "$file" && test ! -L "$file" + test "$(stat -c %u "$file")" = 1000 + test "$(sha256sum "$file" | cut -d ' ' -f 1)" = "$sum" +} +verify "$old/gitleaks" "$scanner_sum" +if test -e "$new" || test -L "$new"; then + test -d "$new" && test ! -L "$new" + test "$(cat "$new/.migration-owner")" = "$marker" + verify "$new/receive-secret-scan" "$adapter_sum" + verify "$new/gitleaks" "$scanner_sum" + test "$(stat -c %a "$new/receive-secret-scan")" = 755 + test "$(stat -c %a "$new/gitleaks")" = 755 + printf '{"stage_verified":true,"already_staged":true}\n' + exit 0 +fi +temporary="$new.staging-$$" +test ! -e "$temporary" && test ! -L "$temporary" +umask 077 +mkdir "$temporary" +printf '%s' "$marker" > "$temporary/.migration-owner" +cleanup() { + code=$? + trap - EXIT HUP INT TERM + if test -d "$temporary" && test ! -L "$temporary" && + test "$(cat "$temporary/.migration-owner")" = "$marker"; then + rm -f -- "$temporary/receive-secret-scan" "$temporary/gitleaks" "$temporary/.migration-owner" + rmdir "$temporary" || code=33 + fi + exit "$code" +} +trap cleanup EXIT +trap 'exit 34' HUP INT TERM +cat > "$temporary/receive-secret-scan" +cp "$old/gitleaks" "$temporary/gitleaks" +verify "$temporary/receive-secret-scan" "$adapter_sum" +verify "$temporary/gitleaks" "$scanner_sum" +chmod 755 "$temporary/receive-secret-scan" "$temporary/gitleaks" "$temporary" +test ! -e "$new" && test ! -L "$new" +mv -T -- "$temporary" "$new" +test "$(cat "$new/.migration-owner")" = "$marker" +verify "$new/receive-secret-scan" "$adapter_sum" +verify "$new/gitleaks" "$scanner_sum" +test "$(stat -c %a "$new/receive-secret-scan")" = 755 +test "$(stat -c %a "$new/gitleaks")" = 755 +printf '{"stage_verified":true,"already_staged":false}\n' diff --git a/docs/changes/2026-11-01-scanner-json-digest/native-r2/test-validation.json b/docs/changes/2026-11-01-scanner-json-digest/native-r2/test-validation.json new file mode 100644 index 00000000..f8150b4c --- /dev/null +++ b/docs/changes/2026-11-01-scanner-json-digest/native-r2/test-validation.json @@ -0,0 +1,26 @@ +{ + "paired_source_candidate": { + "local_real_gitleaks": "12 passed, 37 deselected", + "receiver_real_gitleaks": "full suite passed in 150.087s" + }, + "native_preparation": { + "controller_tests": 5, + "filesystem_and_real_receive_cases": 13, + "filesystem_receipt": "filesystem-r2.json", + "apps_semantic_diff": "only guard append and template selection; all other values equal", + "six_owner_source_preimages_and_candidates": "verified" + }, + "preparation_attempts": [ + { + "attempt": "filesystem-r1", + "outcome": "unverified; stopped before any case because historical temporary dispatcher fixture was absent", + "refinement": "use explicit committed scanner-only offline dispatcher; native probe still copies real current dispatcher" + }, + { + "attempt": "filesystem-r2", + "outcome": "13/13 passed" + } + ], + "native_server_dry_run": "not executed; mandatory in founder-run apply", + "native_mutation": false +} diff --git a/docs/changes/2026-11-01-scanner-json-digest/native-r2/test_filesystem.py b/docs/changes/2026-11-01-scanner-json-digest/native-r2/test_filesystem.py new file mode 100644 index 00000000..bb622aa3 --- /dev/null +++ b/docs/changes/2026-11-01-scanner-json-digest/native-r2/test_filesystem.py @@ -0,0 +1,164 @@ +"""Actual scripts in private PID/network namespaces and tmpfs; no remote writes.""" +from pathlib import Path +import hashlib +import importlib.util +import json +import os +import shutil +import subprocess +import sys +import time + + +def sha(raw): return hashlib.sha256(raw).hexdigest() + + +def run(argv, data=None, success=True): + result = subprocess.run(argv, input=data, capture_output=True, timeout=180) + if (result.returncode == 0) != success: + # Inputs are public or disposable synthetic material. Do not print raw diagnostics. + phases = [] + for line in result.stderr.splitlines(): + try: + value = json.loads(line) + if isinstance(value, dict) and value.get('result') == 'unverified': phases.append(value.get('probe_phase', 'unknown')) + except (ValueError, UnicodeError): pass + raise AssertionError('unexpected-script-status:' + str(result.returncode) + ';phase=' + ','.join(phases)) + return result + + +def inside(case): + c = json.loads(Path('/fixtures/control.json').read_text()) + old, new = Path(c['old_directory']), Path(c['new_directory']) + old.mkdir(parents=True) + for name, src in [('receive-secret-scan', '/old-adapter'), ('gitleaks', '/provider')]: + shutil.copyfile(src, old / name); (old / name).chmod(0o775) + new.mkdir() + for name, src in [('receive-secret-scan', '/new-adapter'), ('gitleaks', '/provider')]: + shutil.copyfile(src, new / name); (new / name).chmod(0o775) + def guard(command, success=True): return run(command, success=success) + def clean_new(): shutil.rmtree(new) + def stage(success=True, payload=None): + return run(['sh', '/scripts/stage.sh', str(new), str(old), c['candidate_sha256'], c['provider_sha256'], 'offline-fixture'], + data=Path('/new-adapter').read_bytes() if payload is None else payload, success=success) + if case == 'old-single-guard-counterexample': + guard(c['single_command']) + assert all((new / n).stat().st_mode & 0o777 == 0o755 for n in ['receive-secret-scan', 'gitleaks']) + assert all((old / n).stat().st_mode & 0o777 == 0o775 for n in ['receive-secret-scan', 'gitleaks']) + elif case == 'both-version-guard': + guard(c['combined_command']) + assert all((p / n).stat().st_mode & 0o777 == 0o755 for p in [old, new] for n in ['receive-secret-scan', 'gitleaks']) + elif case in ['guard-old-drift', 'guard-new-drift', 'guard-symlink', 'guard-unexpected-mode']: + if case == 'guard-old-drift': (old / 'gitleaks').write_bytes(b'changed') + if case == 'guard-new-drift': (new / 'receive-secret-scan').write_bytes(b'changed') + if case == 'guard-symlink': + (new / 'gitleaks').unlink(); (new / 'gitleaks').symlink_to(old / 'gitleaks') + if case == 'guard-unexpected-mode': (new / 'gitleaks').chmod(0o777) + guard(c['combined_command'], success=False) + elif case == 'stage-roundtrip': + clean_new(); stage(); stage() + assert sha((new / 'receive-secret-scan').read_bytes()) == c['candidate_sha256'] + assert not list(new.parent.glob(new.name + '.staging-*')) + elif case == 'stage-bad-hash-cleanup': + clean_new(); stage(success=False, payload=b'incomplete-transfer') + assert not new.exists() and not list(new.parent.glob(new.name + '.staging-*')) + elif case == 'stage-existing-unowned': + (new / 'unrelated').write_bytes(b'preserve-me') + stage(success=False) + assert (new / 'unrelated').read_bytes() == b'preserve-me' + elif case == 'stage-symlink-refused': + clean_new(); new.symlink_to(old) + stage(success=False) + assert (old / 'receive-secret-scan').stat().st_mode & 0o777 == 0o775 + elif case.startswith('hooks-'): + area = Path('/tmp/control'); area.mkdir() + before, after = b'#!/bin/sh\nexit 0\n', b'#!/bin/sh\nexit 2\n' + (area / 'before').write_bytes(before); (area / 'after').write_bytes(after) + paths = [Path('/data/git/gitea-repositories/test/' + name + '.git/hooks/pre-receive.d/10-custodian-secret-scan') for name in ['first', 'second']] + for path in paths: + path.parent.mkdir(parents=True); path.write_bytes(before); path.chmod(0o775) + (path.parent / 'protected').write_bytes(b'unchanged authorization') + (area / 'paths').write_text(''.join(str(p) + '\n' for p in paths)) + def hooks(direction, success=True): return run(['sh', '/scripts/hooks.sh', str(area), direction, sha(before), sha(after)], success=success) + if case == 'hooks-roundtrip': + hooks('apply') + assert all(p.read_bytes() == after and p.stat().st_mode & 0o777 == 0o775 for p in paths) + hooks('rollback') + assert all(p.read_bytes() == before and p.stat().st_mode & 0o777 == 0o775 for p in paths) + elif case == 'hooks-refuse-complete-scope-before-writing': + paths[1].write_bytes(b'unexpected owner change'); hooks('apply', success=False) + assert paths[0].read_bytes() == before + assert paths[1].read_bytes() == b'unexpected owner change' + elif case == 'hooks-partial-failure-recovers': + paths[1].parent.chmod(0o555); hooks('apply', success=False) + assert all(p.read_bytes() == before for p in paths) + assert all(p.stat().st_mode & 0o777 == 0o775 for p in paths) + elif case == 'hooks-symlink-refused': + paths[1].unlink(); paths[1].symlink_to(paths[0]); hooks('apply', success=False) + assert paths[0].read_bytes() == before + elif case == 'hooks-rollback-unknown-drift-retained': + hooks('apply'); paths[1].write_bytes(b'unknown-drift'); hooks('rollback', success=False) + assert paths[0].read_bytes() == after and paths[1].read_bytes() == b'unknown-drift' + assert all((p.parent / 'protected').read_bytes() == b'unchanged authorization' for p in paths) + assert not list(Path('/data').rglob('.custodian-*')) + elif case == 'real-receive-and-quarantine': + guard(c['combined_command']) + result = json.loads(run(['sh', '/scripts/probe.sh', str(new), '/dispatcher']).stdout) + assert all(value is True for value in result.values()) + assert not list(Path('/dev/shm').iterdir()) + else: raise ValueError('unknown-case') + print(json.dumps({'case': case, 'passed': True, 'native_acceptance': False})) + + +def outside(): + import argparse + parser = argparse.ArgumentParser(); parser.add_argument('--attempt', required=True) + args = parser.parse_args() + if not args.attempt.isalnum(): raise ValueError('unsafe-attempt') + here = Path(__file__).resolve().parent + receipt = here / ('filesystem-' + args.attempt + '.json') + with receipt.open('x') as f: f.write('{"result":"unverified"}\n') + cfg = json.loads((here / 'inputs.json').read_bytes()) + fixtures = Path('/tmp/sync-scanner-native-fixtures'); fixtures.mkdir(exist_ok=True) + guard = cfg['new_guard_command'].copy() + guard[2] = guard[2][len(cfg['guard_command'][2]) + 1:] + control = {'old_directory': cfg['old_dir'], 'new_directory': cfg['new_dir'], + 'candidate_sha256': cfg['new_adapter_sha256'], 'provider_sha256': cfg['scanner_sha256'], + 'combined_command': guard, 'single_command': guard} + (fixtures / 'control.json').write_text(json.dumps(control)) + (fixtures / 'passwd').write_text('gitea:x:1000:1000:Offline fixture:/tmp:/bin/sh\n') + (fixtures / 'group').write_text('gitea:x:1000:\n') + binaries = {'old-adapter': Path('/tmp/sync-scanner-candidate/receive-secret-scan-candidate'), + 'new-adapter': Path('/tmp/sync-scanner-candidate/receive-secret-scan-candidate'), + 'provider': Path('/home/worsch/.local/lib/repo-manager/gitleaks/8.30.1/gitleaks'), + 'dispatcher': here / 'fixture-dispatcher.sh'} + assert sha(binaries['new-adapter'].read_bytes()) == cfg['new_adapter_sha256'] + assert sha(binaries['provider'].read_bytes()) == cfg['scanner_sha256'] + prefix = ['bwrap', '--unshare-net', '--unshare-pid', '--die-with-parent', '--new-session'] + for path in ['/usr', '/lib', '/lib64']: prefix += ['--ro-bind', path, path] + prefix += ['--symlink', 'usr/bin', '/bin', '--proc', '/proc', '--dev', '/dev', '--tmpfs', '/dev/shm', '--tmpfs', '/tmp', '--tmpfs', '/data', + '--ro-bind', str(here), '/scripts', '--ro-bind', str(fixtures), '/fixtures', '--dir', '/etc', + '--ro-bind', str(fixtures / 'passwd'), '/etc/passwd', '--ro-bind', str(fixtures / 'group'), '/etc/group'] + for name, path in binaries.items(): prefix += ['--ro-bind', str(path), '/' + name] + prefix += ['--clearenv', '--setenv', 'PATH', '/usr/bin:/bin', '--setenv', 'HOME', '/tmp', '--chdir', '/', '--', 'python3', '/scripts/test_filesystem.py', '--inside'] + cases = ['guard-new-drift', 'guard-symlink', 'guard-unexpected-mode', + 'stage-roundtrip', 'stage-bad-hash-cleanup', 'stage-existing-unowned', 'stage-symlink-refused', + 'hooks-roundtrip', 'hooks-refuse-complete-scope-before-writing', 'hooks-partial-failure-recovers', + 'hooks-symlink-refused', 'hooks-rollback-unknown-drift-retained', 'real-receive-and-quarantine'] + results = [] + try: + for case in cases: + started = time.monotonic(); completed = subprocess.run(prefix + [case], capture_output=True, timeout=200) + failure = [line for line in completed.stderr.decode(errors='replace').splitlines() if line.startswith('AssertionError: unexpected-script-status:')] + results.append({'case': case, 'returncode': completed.returncode, 'seconds': round(time.monotonic()-started, 2), **({'failure': failure[-1]} if failure else {})}) + receipt.write_text(json.dumps({'result': 'unverified', 'cases': results}, indent=2) + '\n') + if completed.returncode: raise ValueError('offline-case-failed:' + case) + receipt.write_text(json.dumps({'result': 'passed', 'cases': results, 'native_acceptance': False, 'real_remote_refs_changed': False, + 'scope': 'Exact new guard, stage/hook scripts, real candidate/Gitleaks receive probes; original guard bytes unchanged.'}, indent=2) + '\n') + finally: + print(json.dumps({'receipt': str(receipt), 'passed': sum(x['returncode'] == 0 for x in results), 'total': len(cases)})) + + +if __name__ == '__main__': + if len(sys.argv) == 3 and sys.argv[1] == '--inside': inside(sys.argv[2]) + else: outside() diff --git a/docs/changes/2026-11-01-scanner-json-digest/native-r2/test_rollout.py b/docs/changes/2026-11-01-scanner-json-digest/native-r2/test_rollout.py new file mode 100644 index 00000000..aa772ffa --- /dev/null +++ b/docs/changes/2026-11-01-scanner-json-digest/native-r2/test_rollout.py @@ -0,0 +1,97 @@ +"""Guard semantics and recovery tests; no Kubernetes access.""" +from pathlib import Path +import copy +import importlib.util +import json +import unittest +from unittest.mock import patch + +HERE = Path(__file__).resolve().parent +spec = importlib.util.spec_from_file_location('scanner_rollout', HERE / 'rollout.py') +m = importlib.util.module_from_spec(spec); spec.loader.exec_module(m) +C = json.loads((HERE / 'inputs.json').read_bytes()) + + +def deployment(): + init = [{'name': 'unrelated'}] * (C['guard_index'] + 1) + init[C['guard_index']] = {'name': 'verify-secret-scanner', 'command': C['guard_command']} + volumes = [{'name': 'unrelated'}] * (C['volume_index'] + 1) + volumes[C['volume_index']] = C['volume'] + return copy.deepcopy({'metadata': {'uid': C['deployment_uid'], 'resourceVersion': '123'}, + 'spec': {'strategy': C['strategy'], 'template': {'spec': { + 'initContainers': init, 'volumes': volumes, + 'containers': [{'name': 'gitea', 'image': 'existing-image', 'env': [{'name': 'public', 'value': 'preserved'}]}]}}}}) + + +def apply_patch(d, operations): + value = copy.deepcopy(d) + for op in operations: + parts = op['path'].strip('/').split('/'); obj = value + for key in parts[:-1]: obj = obj[int(key)] if isinstance(obj, list) else obj[key] + key = int(parts[-1]) if isinstance(obj, list) else parts[-1] + if op['op'] == 'test': + if obj[key] != op['value']: raise ValueError('test-failed') + else: obj[key] = op['value'] + return value + + +class Tests(unittest.TestCase): + def test_forward_and_reverse_change_only_two_selected_fields(self): + before = deployment(); ops = m.patch_for(before, C) + self.assertEqual(sum(o['op'] == 'replace' for o in ops), 2) + after = apply_patch(before, ops) + self.assertEqual(after['spec']['template']['spec']['containers'], before['spec']['template']['spec']['containers']) + self.assertEqual(apply_patch(after, m.patch_for(after, C, True)), before) + + def test_identity_guard_volume_strategy_and_resource_version_drift_refused(self): + for key in ('uid', 'guard', 'volume', 'strategy'): + d = deployment(); s = d['spec']['template']['spec'] + if key == 'uid': d['metadata']['uid'] = 'other' + elif key == 'guard': s['initContainers'][C['guard_index']]['command'] = ['other'] + elif key == 'volume': s['volumes'][C['volume_index']]['configMap']['name'] = 'other' + else: d['spec']['strategy'] = {'type': 'RollingUpdate'} + with self.subTest(key=key), self.assertRaises(RuntimeError): m.patch_for(d, C) + d = deployment(); ops = m.patch_for(d, C); d['metadata']['resourceVersion'] = '124' + with self.assertRaises(ValueError): apply_patch(d, ops) + + def test_inventory_requires_all_scanner_hooks_and_preserves_protected_set(self): + repo = '/data/git/gitea-repositories/coulomb/example.git' + raw = ('775 1000 1000 ' + C['old_hook_sha256'] + ' ' + repo + '/hooks/' + m.HOOK + '\n' + + '755 1000 1000 ' + 'a'*64 + ' ' + repo + '/hooks/pre-receive\n').encode() + before = m.parse_inventory(raw, C, {C['old_hook_sha256']}) + m.protected_equal(before, copy.deepcopy(before)) + changed = copy.deepcopy(before); changed[repo]['pre-receive']['sha256'] = 'b'*64 + with self.assertRaises(RuntimeError): m.protected_equal(before, changed) + with self.assertRaises(RuntimeError): m.parse_inventory(raw.splitlines(keepends=True)[1], C, {C['old_hook_sha256']}) + with self.assertRaises(RuntimeError): m.parse_inventory(raw + raw, C, {C['old_hook_sha256']}) + + def test_lost_patch_response_attempts_guarded_recovery_and_retains_failure(self): + class Simulated(m.Rollout): + def __init__(self): super().__init__(C); self.recovered = False + def preflight(self): return deployment(), 'pod', {} + def event(self, name, **kw): self.receipt['events'].append({'event': name, **kw}) + def shell(self, *a, **kw): return b'{}' + def probe(self, *a): pass + def migrate(self, *a): return {} + def deployment(self): return deployment() + def k(self, *a, **kw): + if 'patch' in a: raise RuntimeError('lost-response-after-patch') + return b'' + def rollback(self, before=None): self.recovered = True + x = Simulated() + with self.assertRaisesRegex(RuntimeError, 'lost-response'): x.apply(Path('/tmp/sync-scanner-candidate/receive-secret-scan-candidate')) + self.assertTrue(x.recovered) + self.assertIn('activation-failed', [e['event'] for e in x.receipt['events']]) + self.assertNotIn('activation-verified', [e['event'] for e in x.receipt['events']]) + + def test_rollback_refuses_unknown_deployment_instead_of_overwriting(self): + class Simulated(m.Rollout): + def target(self): pass + def deployment(self): + d = deployment(); d['spec']['template']['spec']['initContainers'][C['guard_index']]['command'] = ['unknown'] + return d + def k(self, *a, **kw): raise AssertionError('must not mutate') + with self.assertRaisesRegex(RuntimeError, 'unknown-guard'): Simulated(C).rollback() + + +if __name__ == '__main__': unittest.main() diff --git a/docs/changes/2026-11-01-scanner-json-digest/read-only-preflight-outcome.json b/docs/changes/2026-11-01-scanner-json-digest/read-only-preflight-outcome.json new file mode 100644 index 00000000..68cf7ea5 --- /dev/null +++ b/docs/changes/2026-11-01-scanner-json-digest/read-only-preflight-outcome.json @@ -0,0 +1,13 @@ +{ + "proposal_id": "SYNC-FORGE-READONLY-PREFLIGHT-20261101", + "revision": 1, + "proposal_sha256": "b6a0abfdc98996d5ac2cc0438a7128866255e50c76b584b452bbae3cb1b86816", + "script_sha256": "e41c1183dae2499e62d6762b0714cc60356152494b593a45a868938f439a5f51", + "executed_by": "founder in attended terminal", + "authorization_evidence": "User returned bash /tmp/forge-scanner-preflight.sh output and local receipt in active conversation, 2026-11-02", + "outcome": "succeeded; selected exact identities/artifact/template hashes and allocation returned", + "receipt_ref": "native-r2/preflight-receipt.json", + "native_mutation": false, + "promotion_evidence": false, + "score": "unscored; no signed approval or scored success backfilled" +} diff --git a/docs/changes/2026-11-01-scanner-json-digest/read-only-preflight-proposal.json b/docs/changes/2026-11-01-scanner-json-digest/read-only-preflight-proposal.json new file mode 100644 index 00000000..04cbc7aa --- /dev/null +++ b/docs/changes/2026-11-01-scanner-json-digest/read-only-preflight-proposal.json @@ -0,0 +1,14 @@ +{ + "proposal_id": "SYNC-FORGE-READONLY-PREFLIGHT-20261101", + "revision": 1, + "target": "railiance01 forgejo/forge; existing public scanner artifacts and metadata only", + "original_script_sha256": "e41c1183dae2499e62d6762b0714cc60356152494b593a45a868938f439a5f51", + "invocation_script_sha256": "1217346155fa2ea730153c6fbf94f7ec548840cb07fbfa3cfaa61db59fe02d3a", + "expected_result": "selected public artifact/config/guard and node allocation receipt for exact rollout preparation", + "verification": "checks target deployment/pod UID, node address, artifact and hook identities; returns selected metadata only", + "rollback": "not applicable; read-only inspection, no state change", + "cost_estimate_eur": 0, + "risk_estimate_eur": 0, + "estimate_basis": "bounded reads of existing metadata and public scanner binaries only; no new spend or mutation", + "disposition": "requested founder execution; pending result; no exact approval token and no scored success" +} diff --git a/docs/changes/2026-11-01-scanner-json-digest/read-only-preflight.py b/docs/changes/2026-11-01-scanner-json-digest/read-only-preflight.py new file mode 100644 index 00000000..608e21bd --- /dev/null +++ b/docs/changes/2026-11-01-scanner-json-digest/read-only-preflight.py @@ -0,0 +1,57 @@ +"""Founder-run read-only scanner preflight; prints only selected public evidence.""" +import subprocess,json,hashlib,pathlib,sys +NS='forgejo';NAME='forge';POD='forge-79c7fb84db-k5wtm' +EXPECTED_UID='1faefb26-cccd-4d0e-b509-70a7eb0b4f4d' +POD_UID='2c9f911a-403e-4183-a027-2b8d48742348' +BASE='/data/security/receive-secret-scan/20261027-b735d052a49f' +ADAPTER='b735d052a49fc8e9cca14cddee074c5429352ad71841eb6548459294f15683a2' +SCANNER='88f91962aa2f93ac6ab281d553b9e125f5197bbbce38f9f2437f7299c32e5509' +def k(*args): + q=subprocess.run(['kubectl',*args],capture_output=True,timeout=60) + if q.returncode:raise RuntimeError('read-only-command-failed; raw diagnostics withheld') + return q.stdout + +def main(): + d=json.loads(k('-n',NS,'get','deployment',NAME,'-o','json')) + if d['metadata']['uid']!=EXPECTED_UID:raise RuntimeError('deployment-identity-drift') + uid=k('-n',NS,'get','pod',POD,'-o','jsonpath={.metadata.uid}').decode() + if uid!=POD_UID:raise RuntimeError('pod-identity-drift') + spec=d['spec']['template']['spec'];inits=spec.get('initContainers',[]) + candidates=[c['name'] for c in spec['containers'] if c.get('image')=='code.forgejo.org/forgejo/forgejo:11.0.3-rootless'] + if len(candidates)!=1:raise RuntimeError('forge-container-ambiguous') + container=candidates[0] + guards=[(i,x) for i,x in enumerate(inits) if x['name']=='verify-secret-scanner'] + if len(guards)!=1:raise RuntimeError('scanner-guard-ambiguous') + index,guard=guards[0] + volumes=spec.get('volumes',[]);selected=[(i,x) for i,x in enumerate(volumes) if x['name']=='secret-scan-template'] + if len(selected)!=1:raise RuntimeError('scanner-volume-ambiguous') + vindex,volume=selected[0];cmname=volume['configMap']['name'] + if cmname!='forgejo-secret-template-d1df801be39f':raise RuntimeError('template-configmap-drift') + # This exact public ConfigMap contains only the receiver hook, per owner source. + hook=k('-n',NS,'get','configmap',cmname,'-o','jsonpath={.data.hook}') + expected=('#!/bin/sh\nset +vx\nset -eu\nexec '+BASE+'/receive-secret-scan --scanner '+BASE+'/gitleaks\n').encode() + if hook!=expected:raise RuntimeError('mounted-hook-source-drift') + metadata=k('-n',NS,'exec',POD,'-c',container,'--','sh','-c', + 'set -eu; test -f "$1" && test ! -L "$1"; test -f "$2" && test ! -L "$2"; sha256sum "$1" "$2"', + 'sh',BASE+'/receive-secret-scan',BASE+'/gitleaks').decode().splitlines() + hashes=[s.split()[0] for s in metadata] + if hashes!=[ADAPTER,SCANNER]:raise RuntimeError('installed-scanner-artifact-drift') + nodes=json.loads(k('get','nodes','-o','json'))['items'] + if len(nodes)!=1 or not any(a['address']=='92.205.62.239' for a in nodes[0]['status']['addresses']):raise RuntimeError('node-target-drift') + description=k('describe','node',nodes[0]['metadata']['name']).decode() + allocation=description.split('Allocated resources:',1)[1].split('Events:',1)[0].strip() if 'Allocated resources:' in description else None + if allocation is None:raise RuntimeError('node-headroom-unavailable') + receipt=dict(read_only=True,deployment_uid=EXPECTED_UID,pod=POD,pod_uid=POD_UID, + deployment_name=NAME,container=container,node_uid=nodes[0]['metadata']['uid'],node_name=nodes[0]['metadata']['name'], + guard_index=index,guard_command=guard.get('command'),guard_args=guard.get('args'),volume_index=vindex, + volume=volume,strategy=d['spec'].get('strategy'),resources=[dict(name=c['name'],resources=c.get('resources')) for c in spec['containers']], + old_configmap=cmname,old_hook_sha256=hashlib.sha256(hook).hexdigest(),old_adapter_sha256=ADAPTER,scanner_sha256=SCANNER, + allocated_resources=allocation) + pathlib.Path('/tmp/forge-scanner-preflight-receipt.json').write_text(json.dumps(receipt,indent=2)+'\n') + # Keep long public shell commands in the local receipt; return concise checks. + print(json.dumps({k:receipt[k] for k in ['read_only','deployment_name','deployment_uid','pod_uid','node_uid','old_configmap','old_adapter_sha256','strategy','allocated_resources']},indent=2)) + print('receipt: /tmp/forge-scanner-preflight-receipt.json') +try:main() +except Exception as exc: + print('preflight refused:',str(exc) if isinstance(exc,RuntimeError) else type(exc).__name__) + sys.exit(1) diff --git a/docs/repository-sync-2026-11-01.md b/docs/repository-sync-2026-11-01.md index a3cf66d7..3b77e229 100644 --- a/docs/repository-sync-2026-11-01.md +++ b/docs/repository-sync-2026-11-01.md @@ -71,3 +71,11 @@ Concurrent Social work was checkpointed at c0e8dbb after all **81** focused mock The final full tip sweep proves **167 of 169 primary checkout tips** equal to their queried remotes. Social and rapp-flex-auth-secrets-engine are the two held main histories. Including the ignored upstream mirror gives 168 equal checkout tips overall; linked worktrees/caches are accounted for separately. Current unresolved historical branches remain documented above. The 139 held Custodian paths exactly equal the original diagnostic set; the index is clear. State-hub's ten generated weekly records and the four generated cache INTENT overlays remain local under their documented dispositions. All jobs launched for this sync are terminal. No required ordinary Git/check/consistency work remains executable past these controls. The same missing founder-run native inspection and supervised scanner disposition has persisted through multiple goal continuations. The goal must remain unachieved; its live handoff is CUST-WP-0083 and human task 589e1a54-9a4e-5f90-bd29-5c61a6cf8081. First unblock step remains the already-requested `bash /tmp/forge-scanner-preflight.sh` in the founder terminal, followed by exact guarded rollout/adjudication review. No control bypass or unrestricted credential access is authorized by this report. + +### Native scanner preparation, 2026-11-02 + +Founder read-only preflight succeeded: expected forge/node/pod and old adapter/Gitleaks/template hashes match; Recreate strategy, CPU requests 68%, memory requests 76%. Durable selected receipt: `docs/changes/2026-11-01-scanner-json-digest/native-r2/preflight-receipt.json`. This resolves the previous missing-preflight request, without authorizing native mutation. + +Revision 2 of SYNC-SCANNER-JSON-DIGEST-20261101 is concretely prepared and sealed by packet SHA256 `d22b1f555af96c203b7d68efd25e80815ce18ca4e3bf0dd3ab96b6f699d50237`. Its reviewed scope is the paired JSON filename/exact lowercase SHA256 correction, versioned native artifact, guarded hook migration, two-field deployment patch and six owner source files. Native server checks remain pending execution. Thirteen isolated filesystem/real receiver tests and five controller tests passed, including rollback, partial write recovery, unknown drift refusal and intermediate secret/quarantine coverage. The first offline attempt stopped because an old temporary fixture was missing; the replacement explicit scanner-only dispatcher and native real-dispatcher distinction are recorded. Original revision 1, failures and unscored receipts remain intact. + +The founder reviews `native-r2/PLAN.md` and the sealed source diffs, then may approve this exact revision and perform `/tmp/forge-scanner-rollout.sh` through the attended admin lane. Expected Recreate interruption is 1โ€“3 minutes, with guarded recovery; no new resource commitment. Risk and founder time are explicitly unpriced beyond an illustrative EUR100 recovery hour, requiring supervised judgement. Remaining UUID/fingerprint/prose findings and all other held publication remain open in CUST-WP-0083. No native policy or owner scanner source was changed during preparation. diff --git a/workplans/CUST-WP-0083-fleet-sync-and-scanner-remediation.md b/workplans/CUST-WP-0083-fleet-sync-and-scanner-remediation.md index cd706ad4..8559a64f 100644 --- a/workplans/CUST-WP-0083-fleet-sync-and-scanner-remediation.md +++ b/workplans/CUST-WP-0083-fleet-sync-and-scanner-remediation.md @@ -7,7 +7,7 @@ repo: the-custodian status: blocked owner: codex created: "2026-11-01" -updated: "2026-11-01" +updated: "2026-11-02" flavor: implementation depends_on: [] related_repos: [repo-manager, railiance-forge, railiance-apps, rapp-flex-auth-secrets-engine, intelligence-radar] @@ -32,6 +32,7 @@ Discovered 210 checkouts. Reviewed primary changes, managed clones, linked workt ```task id: CUST-WP-0083-T02 status: wait +depends_on: [CUST-WP-0083-T03, CUST-WP-0083-T04] priority: high state_hub_task_id: "c7bf93ed-4d01-5128-85c5-dcba10054274" ``` @@ -43,6 +44,7 @@ Seven repositories received source/workplan/session/capture commits, and 16 affe ```task id: CUST-WP-0083-T03 status: wait +depends_on: [CUST-WP-0083-T04] priority: high state_hub_task_id: "a09478f1-59c3-589d-ac7a-5439cc79af09" ``` @@ -54,19 +56,21 @@ Generated-only gates, local/remote scanner deadlines and false positives are ind ```task id: CUST-WP-0083-T04 status: wait +decision_id: "5410707c-ce73-4f80-9c03-805102c31c58" priority: high needs_human: true -intervention_note: "Run bash /tmp/forge-scanner-preflight.sh in the founder terminal and return the selected receipt; exact native correction and rollback will then be prepared for approval." +intervention_note: "Read-only preflight received. Review/approve SYNC-SCANNER-JSON-DIGEST-20261101 revision 2, packet SHA256 d22b1f555af96c203b7d68efd25e80815ce18ca4e3bf0dd3ab96b6f699d50237; exact rollout, rollback and six owner source changes are in docs/changes/2026-11-01-scanner-json-digest/native-r2/PLAN.md. Native action remains unexecuted." state_hub_task_id: "589e1a54-9a4e-5f90-bd29-5c61a6cf8081" ``` -Human-lane checkpoint: the current ops-mason-builder identity cannot inspect the forge or perform native administration. The founder returned current deployment/pod identity using their attended admin context. Read-only artifact/config/guard/headroom inspection is prepared as `/tmp/forge-scanner-preflight.sh` and awaits its result. Prepare the exact guarded rollout, verification, rollback and cost/risk assessment from this evidence before requesting approval. The agent must not borrow the founder admin kubeconfig. No local runtime scanner or receiver policy is changed by the existing candidate. Preserve original revisions, failures and any refinements in supervised receipts; no autopilot or scored approval is inferred. +Human-lane checkpoint: the founder successfully returned `/tmp/forge-scanner-preflight-receipt.json` on 2026-11-02. It verifies exact node/deployment/pod identities and old artifacts/template, Recreate strategy and 68% CPU/76% memory requests. The exact revision-2 packet is prepared at `docs/changes/2026-11-01-scanner-json-digest/native-r2/PLAN.md`, SHA256 `d22b1f555af96c203b7d68efd25e80815ce18ca4e3bf0dd3ab96b6f699d50237`. Its 13 isolated filesystem/real receiver cases and five controller tests passed; all six owner preimages/candidate digests and the two-field Apps semantic change were verified. Initial missing offline dispatcher fixture failure is retained. Server dry-run/diff, live probes and activation are prepared execution prerequisites, not completed native evidence. Await exact founder approval and attended native execution; preserve all old artifacts and guarded rollback, source followthrough and normal publication. Neither local runtime scanner nor live receiver has changed. No admin credential borrowing, invented owner approval, promotion or scored approval. ## Verify complete remote publication and report exceptions ```task id: CUST-WP-0083-T05 status: wait +depends_on: [CUST-WP-0083-T02, CUST-WP-0083-T08] priority: high state_hub_task_id: "af09a25a-7aa4-5d40-ac8f-62b23aeea75d" ``` @@ -100,6 +104,7 @@ Completed 2026-11-01. Approval-engine, informed-decision and state-hub adopted t ```task id: CUST-WP-0083-T08 status: wait +depends_on: [CUST-WP-0083-T03, CUST-WP-0083-T04] priority: medium state_hub_task_id: "39ef8dbd-09fb-5b25-a2f4-e035c6fa4484" ```