From fdc6d4a0a54d9072cf7535f577efc7b71df1d2a0 Mon Sep 17 00:00:00 2001 From: Public Source Publisher Date: Wed, 14 Oct 2026 13:18:45 +0000 Subject: [PATCH] Publish curated ITWorx_MCP_Hub source from 3237e4d8aec312d63d13b4e80664422a7520b5f9 --- PUBLIC_SOURCE_EXPORT.md | 2 +- PUBLIC_SOURCE_MANIFEST.json | 51 +++-- .../images/contextforge/candidate/locks.json | 9 +- .../candidate/production-requirements.lock | 86 +++---- .../security-bases/ingestion-clamd/Dockerfile | 9 + .../ingestion-clamd/verify-pcre2.sh | 31 +++ .../images/security-bases/postgres/Dockerfile | 42 ++++ scripts/contextforge_candidate_recipe.py | 38 +++- scripts/verify.py | 2 + tests/test_contextforge_candidate_recipe.py | 62 ++++- tests/test_release_images_workflow.py | 11 +- tests/test_security_base_preparation.py | 214 ++++++++++++++++++ tests/test_security_pcre_inventory.py | 47 ++++ 13 files changed, 536 insertions(+), 68 deletions(-) create mode 100644 infrastructure/images/security-bases/ingestion-clamd/Dockerfile create mode 100644 infrastructure/images/security-bases/ingestion-clamd/verify-pcre2.sh create mode 100644 infrastructure/images/security-bases/postgres/Dockerfile create mode 100644 tests/test_security_base_preparation.py create mode 100644 tests/test_security_pcre_inventory.py diff --git a/PUBLIC_SOURCE_EXPORT.md b/PUBLIC_SOURCE_EXPORT.md index d28e78a..23dd6a7 100644 --- a/PUBLIC_SOURCE_EXPORT.md +++ b/PUBLIC_SOURCE_EXPORT.md @@ -1,3 +1,3 @@ # Public source export -This source snapshot was generated from the private canonical repository at revision `fb03947ffb946bfeaca89437fbb5f90751a14993`. It contains no private Git history or operational evidence. Changes are published from the canonical repository. +This source snapshot was generated from the private canonical repository at revision `3237e4d8aec312d63d13b4e80664422a7520b5f9`. It contains no private Git history or operational evidence. Changes are published from the canonical repository. diff --git a/PUBLIC_SOURCE_MANIFEST.json b/PUBLIC_SOURCE_MANIFEST.json index a2c0cad..9893933 100644 --- a/PUBLIC_SOURCE_MANIFEST.json +++ b/PUBLIC_SOURCE_MANIFEST.json @@ -1,6 +1,6 @@ { "schemaVersion": 1, - "sourceRevision": "fb03947ffb946bfeaca89437fbb5f90751a14993", + "sourceRevision": "3237e4d8aec312d63d13b4e80664422a7520b5f9", "files": [ { "path": ".dockerignore", @@ -2044,13 +2044,13 @@ }, { "path": "infrastructure/images/contextforge/candidate/locks.json", - "sha256": "75b7f31fdf84cf390095c4c13123b64a5270b25a5d7eb6018077544101e0b697", - "bytes": 3132 + "sha256": "2d9ae18e423ba1eb0f91d07580a999dad56e22d0f76fc795a1bbcf63c804acd7", + "bytes": 3291 }, { "path": "infrastructure/images/contextforge/candidate/production-requirements.lock", - "sha256": "3a1b599d2487bd9f1aaf6f2be0f65b6db17da5847a123e905c5e5337a3e87f76", - "bytes": 131299 + "sha256": "61c96efcbf1efcc4a8a42832d315f10f1e9c6afe9dc39523048c7c58d4dc4af0", + "bytes": 131298 }, { "path": "infrastructure/images/contextforge/compat-image.env", @@ -2132,6 +2132,21 @@ "sha256": "783655674084dd10d269f124113adf675a873328e05de273bd9dc06daf37563d", "bytes": 317 }, + { + "path": "infrastructure/images/security-bases/ingestion-clamd/Dockerfile", + "sha256": "0b806f1077af26b85a059eb2a40eeff27b661ef997d57fc82bb648fc311043d5", + "bytes": 508 + }, + { + "path": "infrastructure/images/security-bases/ingestion-clamd/verify-pcre2.sh", + "sha256": "1ec848d1f52b37d3abfb56e1308383d4d2907810e65bad95368b5ecee60a5514", + "bytes": 909 + }, + { + "path": "infrastructure/images/security-bases/postgres/Dockerfile", + "sha256": "ba80a2d4c49ae50b0e57d58cd31736997bdb023972ac7951c05f50b15e7b68da", + "bytes": 3461 + }, { "path": "infrastructure/postgres/init/001-create-databases.sh", "sha256": "ed6274fa3e5d1e70f41caa639a1c9a8bae10844cc9e1520279212621b0b16fb1", @@ -2444,8 +2459,8 @@ }, { "path": "scripts/contextforge_candidate_recipe.py", - "sha256": "4faf2c30223e8e1b5015474da46044e72d6edeb3653c57679d75aa504b2e2a2d", - "bytes": 22518 + "sha256": "136330e402a9d737cf6560edf27ea91688e50a4e2317a2de6db63e65ff17fb43", + "bytes": 23617 }, { "path": "scripts/contextforge_runtime_contract.py", @@ -2574,8 +2589,8 @@ }, { "path": "scripts/verify.py", - "sha256": "2219c1db92ddb2d02d0c352ed12be7b43acc6c275dad9b9454206bab515b2a07", - "bytes": 14593 + "sha256": "99c3e96e0d5f53d501225022b112bf82e6d3382524e1908e9e6465e8ea96d5b8", + "bytes": 14707 }, { "path": "scripts/verify_demo_token_flow.py", @@ -3144,8 +3159,8 @@ }, { "path": "tests/test_contextforge_candidate_recipe.py", - "sha256": "f96a0b665c2ffb507da80f26d44b04f5fa6e7384dfe9639b8549f8a866e3da1b", - "bytes": 19894 + "sha256": "5ca7b0abd29f553368c34e1bdc6592d754d11dfec50fb1c1aedb678c065f7646", + "bytes": 22577 }, { "path": "tests/test_contextforge_compat_image_contract.py", @@ -3454,8 +3469,8 @@ }, { "path": "tests/test_release_images_workflow.py", - "sha256": "055e9adb93816464381e4de32b0c11e86d54555606a17041ba1ee52f36d73487", - "bytes": 3063 + "sha256": "64a7398d772caa7ffe7590fb473efa3fdeaa244dae62b52d4b9024ab3503d87a", + "bytes": 3130 }, { "path": "tests/test_release_scanner_asset.py", @@ -3472,6 +3487,16 @@ "sha256": "772b87fafb5a877a32234ab10b6ed418445575ac9dc0e13380491d294c55330c", "bytes": 5048 }, + { + "path": "tests/test_security_base_preparation.py", + "sha256": "817a39215877dd9125cb51edeaf08ec95be453abff2341ca52264e29d7061a36", + "bytes": 8209 + }, + { + "path": "tests/test_security_pcre_inventory.py", + "sha256": "820c9ec6637119310e8627e30661a4b1ee2053057bc0f949db47914629512218", + "bytes": 1863 + }, { "path": "tests/test_stitch_connector.py", "sha256": "c34cd2dc20721052dc63a15dc763ecc9b4f1adaef1d6ec2d5189adff9161c269", diff --git a/infrastructure/images/contextforge/candidate/locks.json b/infrastructure/images/contextforge/candidate/locks.json index ccb4d1e..a236199 100644 --- a/infrastructure/images/contextforge/candidate/locks.json +++ b/infrastructure/images/contextforge/candidate/locks.json @@ -36,7 +36,11 @@ "constraints": [ "mcp==1.30.0", "PyJWT==2.15.0", - "urllib3==2.8.0" + "urllib3==2.8.0", + "anyio==4.14.2", + "cryptography==50.0.0", + "langgraph-sdk==0.4.4", + "langgraph==1.2.3" ], "extras": [ "redis", @@ -52,9 +56,10 @@ "python": "3.12", "requirements": { "build-tools.lock": "4ba5f91f6473fc4fc9971b2353629203099d9409f537895972e60a65890af02f", - "production-requirements.lock": "3a1b599d2487bd9f1aaf6f2be0f65b6db17da5847a123e905c5e5337a3e87f76" + "production-requirements.lock": "61c96efcbf1efcc4a8a42832d315f10f1e9c6afe9dc39523048c7c58d4dc4af0" }, "schema": "itworx.contextforge-candidate-locks/v1", + "securityFreshnessOverrideUtc": "2026-10-14T00:00:00Z", "upstreamCommit": "1e58a0434a78bbdbe1e975abeb0bed1043ff14c1", "upstreamFiles": { "Containerfile": "2ab0187f3a63eced093a1dec3141ec3e30ef879eb7de0dfd9e4bc5fce33f64f2", diff --git a/infrastructure/images/contextforge/candidate/production-requirements.lock b/infrastructure/images/contextforge/candidate/production-requirements.lock index a9fdd62..d0ffb99 100644 --- a/infrastructure/images/contextforge/candidate/production-requirements.lock +++ b/infrastructure/images/contextforge/candidate/production-requirements.lock @@ -10,9 +10,9 @@ annotated-types==0.7.0 \ ansicon==1.89.0 ; sys_platform == 'win32' \ --hash=sha256:e4d039def5768a47e4afec8e89e83ec3ae5a26bf00ad851f914d1240b444d2b1 \ --hash=sha256:f1def52d17f65c2c9682cf8370c03f541f410c1752d6a14029f97318e4b9dfec -anyio==4.14.1 \ - --hash=sha256:4e5533c5b8ff0a24f5d7a176cbe6877129cd183893f66b537f8f227d10527d72 \ - --hash=sha256:8d648a3544c1a700e3ff78615cd679e4c5c3f149904287e73687b2596963629e +anyio==4.14.2 \ + --hash=sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494 \ + --hash=sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f argon2-cffi==25.1.0 \ --hash=sha256:694ae5cc8a42f4c4e2bf2ca0e64e51e23a040c6a517a85074683d3959e1346c1 \ --hash=sha256:fdc8b074db390fccb6eb4a3604ae7231f219aa669a2652e0f20e16ba513d5741 @@ -233,40 +233,40 @@ cpex-url-reputation==0.3.4 \ --hash=sha256:b91db67405dc00e93d147cc4fb6688ac8983c6935d8b41709db33f3c0c249313 \ --hash=sha256:d059fbd921ddd20f941a06e18ddad8aa49bc8a0b79d19f340e65706847ea084f \ --hash=sha256:e97bf605d931d8a478236cbc34e6170a07189fd3670fda9bb2cc3c03ae054ea7 -cryptography==49.0.0 \ - --hash=sha256:026ac7423e6fa66872d3bf889be5974507da3944f866f704fa200eadacd00001 \ - --hash=sha256:084ef1af862eb07ec46d25f68689f2102a9fc0e05ce7b80f14f5fe51e4eef0f6 \ - --hash=sha256:0b82e28ee398a386f0807bba7884d30f25218855690f45115831bcce5d90822c \ - --hash=sha256:0e959b578856a3924bc0cbb710fc12c387b9412a951389f3ca61704a9e25f325 \ - --hash=sha256:0f21641cf4b30fca7aee061ced0ec7ad7b073518088b7c9969a297c0ae796c69 \ - --hash=sha256:2400ef9c9e2299a25614eb1dea3db54a69b1349efd043bfac9c67630d136df36 \ - --hash=sha256:28d8b15e6275f12c8a207dc309dfa957903c927d08d0cc937ee3f63f200693cc \ - --hash=sha256:2afe9051da7ae7bd5905da5a949280c7d2bb75682e188f650a9d0f2756b834c6 \ - --hash=sha256:2eda353d8a27bcbcaa4cbed18994a74ab4d19a2ca897db188ea269ab9b71419b \ - --hash=sha256:35b151772baff2c74cba7fa290ceaff4c3b11c0c881eb93eb5dbc05a7cfbba18 \ - --hash=sha256:36d1709f992593689b45bda411498d62c6e365f2ca00b84657d4dadd24de16db \ - --hash=sha256:42b0684e0e40cf26122427802486f6d93aea593612603a94fbf260c7eb1e9c1b \ - --hash=sha256:53ecee2e23f7169b6117e99fc8a944e5e50f79e69758a83b52a00cb98ab2b2d2 \ - --hash=sha256:66ec79c3904820572d7e987abdf304281f141d37ad9a489b8e97066e7b9b6459 \ - --hash=sha256:67e1d20ad9ef3a563c59ef22e7a8a0b8210bd26604369ea4a30a7c66aefe504e \ - --hash=sha256:6f2debedf9ca60cf1d5bd466475638af5130f89965605cd818484d19987d3a21 \ - --hash=sha256:6fc361c34fb6aac015ce19435876635e5c6d21db31998b0920f675f131e043b8 \ - --hash=sha256:8c25ceb16df5b9435f3f6a9829204985b0e0cbee3b48aacd432c7d2c850b44d9 \ - --hash=sha256:966fe0e9c67490071f14c0d2b1cb2dfb3023c5ce39457343931415f08382f2db \ - --hash=sha256:9e82dcc8e56052715fb18b2429e3bca4823b1629136a2084fc45a9a5cecb9b64 \ - --hash=sha256:b39efa323140595abd3ecca8529d321ae50f55f3aa3ba9cc81ea56a6011953d5 \ - --hash=sha256:b47db11c2c3525083296069b98ac5221907455e989ae0c2e3008bde851921615 \ - --hash=sha256:b87e65d263b3e5d3bb92a57e2a6638e2f31110fa7aa890c7b2dbba42248d0a3f \ - --hash=sha256:be9fcb48a55f023493482827d4f459bd263cc20efde64f204b97c123201850c6 \ - --hash=sha256:c83782480a4a9da4d0feb51950131ba32e12e70813848b3343f6e18c28a66838 \ - --hash=sha256:cbc77da8c523d5abd028635ba850a6966fcee2c82e2bf65a41d1d8afe0f98be9 \ - --hash=sha256:ccac2bfebc306b862133e3bb71f3f6ee8bb525240089b2d952e4144b3a6d5da7 \ - --hash=sha256:d0527ce944105f257f605a827d6ebead966c752038b6e8656abb9c5edee6fc68 \ - --hash=sha256:e5dfc1e64de5677cec922ffa8da89c546d0415bf6efdf081842e5d44c84e1f0e \ - --hash=sha256:ec5e529fb80935c94fe7b729f9972b50e351a0e6b50aa294fd5cabb109fcc29a \ - --hash=sha256:f78ff2c9ed8dc2d036b0f4d640e22522213d047c1b14e61205a7e55c80a494d4 \ - --hash=sha256:f89660a348f4f78a92366240a61404e337586ef7f5909a2fef59ca88ef505493 \ - --hash=sha256:fc1e275c2f1d97b1a6450b8b0ea3ebfa6e087a611c2b26cb2404d48588abab7b +cryptography==50.0.0 \ + --hash=sha256:031e2d5dd4bb9caa3ca9c82e5a197fd8ae680232cee62603d1a813f3f07e3d03 \ + --hash=sha256:06a32a980526a6ab9a4b9bf8f7385800791e2bb960903cb6b530e4817509a3b7 \ + --hash=sha256:07479a1cb08219ab719147e742e76090c9c773321959bb94946fffdd397a6437 \ + --hash=sha256:07949c449a1abcf60d1ee6e88956d89404c7df3c8258f46589e912988e551987 \ + --hash=sha256:105110f43a471dbd0060b9c9516cb8a6a79233631a04cc2ba16f28323ac6e025 \ + --hash=sha256:1489e263a8048bb8b6a8bac662eb2d402ea5d2b7b4699b72f385f1e2772db105 \ + --hash=sha256:2a8183b489dc1f7f80f135780fadc1108f14b31b8a40411c7a5b17425f65f28b \ + --hash=sha256:37fdb0d0111f1e2ff07139dfb79f1b49531f8e213c46f1163dd7642979b58c47 \ + --hash=sha256:5e34edd123674534acd70147f0ca331eaa2c74e6325fb2028c886aa26ba0b68c \ + --hash=sha256:62598a8a57f815db4c6259a4e97d857dab56697e7de8e8ab02352ab74da1995d \ + --hash=sha256:65c2c3add92b45fd0709db8594536aea39c2a67af0e27ffcf049c498501140b7 \ + --hash=sha256:6e7d61120573a7f2cd94cc095f9e81f6967c61ccdf194285aa143ecec8e0b708 \ + --hash=sha256:7cec5b856506da6defb290f30c9ee687d5f5e8cb0bd3f6459dde43b0b4fa40ef \ + --hash=sha256:82148ec5bddac30b51a5b3c1945075f896fa022cb93f8e4a01e9f6ee95292c5f \ + --hash=sha256:828743d939e9629bc267b8e2d08d8bb67cd4319c771a33d4b18b22dd8fb7440a \ + --hash=sha256:8d89f3976b10b4ce31118de72329025f70d2c6ead14a8217c5514dd2c6d5a78f \ + --hash=sha256:8eb5e1172eb569ea8a872796576e6a67c276351728b6455d5beb01242b027c6a \ + --hash=sha256:900131fafd8aead39ac7dd3a7e833be754c17a95cfd91221636949fe4eb0aa8a \ + --hash=sha256:910d11e1a385c654bf738bf3e6b8e6ed5de0f5610fcae2be9e5b398d8081d20e \ + --hash=sha256:910e1d2668e7de9648f2bcee30e180db2a6b15c30f887d7c4c93ddf96e3992e3 \ + --hash=sha256:a1b30560f2acc95aa8b2e06e716a13dbfc97314747b80d9707e307f77b40d6b3 \ + --hash=sha256:a91296cb61e8df6f86d0c19cc4068228da256bf59bf86049fbd821084565327f \ + --hash=sha256:b42a28c1844fd9de8f3f7d540e36b66f3a9c83fceac7170ebc7a6a19edd9dcae \ + --hash=sha256:bd1c592e4d5974f0d08d4888e432157adba757c66da0246918e43677fafa2d30 \ + --hash=sha256:c87f62a3d3b9888ed0fdde100ec06aa61ca9cd44bad9057d1dff9a516b5f5bb9 \ + --hash=sha256:c99c003e088647b8a5b7c145d6f78c335f6348332b62e142d411c4b63d1460b9 \ + --hash=sha256:ccdc4a71a4dabae05de219404f9f4abc38e3b58422177ff93d0da05967dafa07 \ + --hash=sha256:d24fead1d4d076e1bfb006dcec392074a3cd8d7b4fc8a595aa64073b2b7a96ba \ + --hash=sha256:d764dcf130c428ef66786f866dd750f53182bc608813489915e9fc106bb0c82f \ + --hash=sha256:e722f16708d854fe924790e051061f6704a472c3bac347b6fd88033ea8dd0dc5 \ + --hash=sha256:eeac2acb5a20ed25e0ad6d1df9891a520b78b404266b6d11778f25d5d691a6c9 \ + --hash=sha256:f89831ef99dd7dd169ab06d63a831adb9e20a87aac6d380266bbda5823349169 \ + --hash=sha256:fd9192b7b70c573d7f214eb1ae35e00d359f6f5e4b27c7e21e30de1fc6204645 distro==1.9.0 \ --hash=sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed \ --hash=sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2 @@ -608,18 +608,18 @@ langchain-openai==1.2.1 \ langchain-protocol==0.0.18 \ --hash=sha256:70b53a86fbf9cedc863555effe44da192ab02d556ddbf2cf95b8873adcf41b5a \ --hash=sha256:ec3e11782f1ed0c9db38e5a9ed01b0e7a0d3fba406faa8aef6594b73c56a63e6 -langgraph==1.2.0 \ - --hash=sha256:03fd5895a8d4b70db1ff63ebc3bacead29dd20cd794a8b1a483e7ec9018f7a65 \ - --hash=sha256:4a9baaf62afc5d5f63144a50095140a34b9aa9b7cea695d25326d564775348e7 +langgraph==1.2.3 \ + --hash=sha256:22e6c89084adb3edb7855f6ffa692af3d75925f9c70deb43b86da14db1b02c78 \ + --hash=sha256:7f89cd5f0946fe29bd7ca048e2a84d3c14e7f652e38bb98e00f0ba8b7004b9d0 langgraph-checkpoint==4.1.1 \ --hash=sha256:25d29144b082827218e7bc3f1e9b0566a4bb007895cd6cc26f66a8428739f56e \ --hash=sha256:6c2bdb530c91f91d7d9c1bd100925d0fc4f498d418c17f3587d1526279482a25 langgraph-prebuilt==1.1.0 \ --hash=sha256:3c579cf6eed2d17f9c157c2d0fcaddcd8688524e7022d3b22b37a3bf4589d528 \ --hash=sha256:51e311747d755b751d5c6b39b0c1446124d3a7643d2515017e6714b323508fc9 -langgraph-sdk==0.3.15 \ - --hash=sha256:29e805003d2c6e296823dd71992610976fd0428cefaa8b3304fd91f2247037de \ - --hash=sha256:3838773acf7456d158165385d49f48f1e856f28b56ccd99ea139a8f27004815d +langgraph-sdk==0.4.4 \ + --hash=sha256:39afe416c91742925e6f8a93715f566d499b36e1b636b804a4ffe3190e4f4e64 \ + --hash=sha256:4e651ffa09de695681579396375377bdde23bedbc8e35b070e615cbd5af7da8b langsmith==0.9.7 \ --hash=sha256:40f8a66a466a7abd991a9df1b50de0a9d30c48ee0d3da46ce00516b68e41c9d7 \ --hash=sha256:a5ff9179b77eb0c3a0129294d30924eab13e51e2720f374372dbbc014c892911 diff --git a/infrastructure/images/security-bases/ingestion-clamd/Dockerfile b/infrastructure/images/security-bases/ingestion-clamd/Dockerfile new file mode 100644 index 0000000..4a2e756 --- /dev/null +++ b/infrastructure/images/security-bases/ingestion-clamd/Dockerfile @@ -0,0 +1,9 @@ +FROM clamav/clamav:1.4.6@sha256:a5f03c12a79dbe9f6d8a527b6bb1ea053fa8dd061d3738a26897f055ee2d9303 +ARG SOURCE_DATE_EPOCH +COPY --chmod=0555 verify-pcre2.sh /usr/local/libexec/itworx-verify-pcre2 +# Alpine 3.24 security database explicitly fixes CVE-2026-103111 in this exact package. +RUN set -eux; \ + test "$(cut -d. -f1,2 /etc/alpine-release)" = '3.24'; \ + apk add --no-cache --upgrade 'pcre2=10.49-r0'; \ + /usr/local/libexec/itworx-verify-pcre2 +LABEL org.itworx.security-base.pcre2.version="10.49-r0" diff --git a/infrastructure/images/security-bases/ingestion-clamd/verify-pcre2.sh b/infrastructure/images/security-bases/ingestion-clamd/verify-pcre2.sh new file mode 100644 index 0000000..7cee43a --- /dev/null +++ b/infrastructure/images/security-bases/ingestion-clamd/verify-pcre2.sh @@ -0,0 +1,31 @@ +#!/bin/sh +set -eu + +# Query the authoritative installed database, never apk-info's verbose display fields. +version=$(awk -v RS='' ' +{ + name_count = 0; version_count = 0; selected = 0; package_version = "" + count = split($0, lines, "\n") + for (i = 1; i <= count; i++) { + if (substr(lines[i], 1, 2) == "P:") { + name_count++ + if (lines[i] == "P:pcre2") selected = 1 + } + if (substr(lines[i], 1, 2) == "V:") { + version_count++ + package_version = substr(lines[i], 3) + } + } + if (selected) { + matches++ + if (name_count != 1 || version_count != 1) invalid = 1 + selected_version = package_version + } +} +END { + if (matches != 1 || invalid || selected_version != "10.49-r0") exit 1 + print "10.49-r0" +} +' /lib/apk/db/installed) +test "$version" = '10.49-r0' +clamd --version | grep -Eq '^ClamAV 1\.4\.6(/|$)' diff --git a/infrastructure/images/security-bases/postgres/Dockerfile b/infrastructure/images/security-bases/postgres/Dockerfile new file mode 100644 index 0000000..e22b99e --- /dev/null +++ b/infrastructure/images/security-bases/postgres/Dockerfile @@ -0,0 +1,42 @@ +FROM docker.io/library/golang:1.26.8-bookworm@sha256:abe4f87f354c4f6d7ee3fb11b241c6b6c24a32ca50a2ebcc30493a2168e14048 AS gosu-build +ARG SOURCE_DATE_EPOCH +ARG GOSU_MODULE=github.com/tianon/gosu +ARG GOSU_MODULE_VERSION=v0.0.0-20250923190938-6456aaa0f3c8 +ARG GOSU_MODULE_SUM=h1:HIpXk5mGBQGfOqcaBbRT4Vnss8NPICnMGlD5xTlPBdQ= +ARG GOSU_GOMOD_SUM=h1:SwhRwWsO6iqXZN9CpIaU9CnOrUqpWDINW16KaaSqnrU= +ARG GOSU_SOURCE_COMMIT=6456aaa0f3c854d199d0f037f068eb97515b7513 +ENV CGO_ENABLED=0 GOTOOLCHAIN=local GOOS=linux GOARCH=amd64 GOBIN=/out/bin GOPROXY=https://proxy.golang.org GOSUMDB=sum.golang.org +COPY go-toolchain.tar.gz gosu-source.tar.gz verify-gosu-metadata.sh /tmp/ +RUN set -eux; \ + printf '%s %s\n' 42d158b4d8f7b61ac0a830567c940a86098fb7aac52e467a5ebec03ef5cc2f8d /tmp/go-toolchain.tar.gz | sha256sum -c -; \ + printf '%s %s\n' 33d7537d588ea49458b9509bcf4554bdf5ceacc66da71e5caa1058ea3b689c3b /tmp/gosu-source.tar.gz | sha256sum -c -; \ + rm -rf /usr/local/go; tar -C /usr/local -xzf /tmp/go-toolchain.tar.gz; \ + test "$(go version)" = 'go version go1.26.9 linux/amd64'; \ + chmod 0555 /tmp/verify-gosu-metadata.sh; \ + module_info="$(go mod download -json "${GOSU_MODULE}@${GOSU_MODULE_VERSION}")"; \ + module_dir="$(go env GOMODCACHE)/${GOSU_MODULE}@${GOSU_MODULE_VERSION}"; \ + printf '%s\n' "$module_info" | /tmp/verify-gosu-metadata.sh json-field Path "$GOSU_MODULE"; \ + printf '%s\n' "$module_info" | /tmp/verify-gosu-metadata.sh json-field Version "$GOSU_MODULE_VERSION"; \ + printf '%s\n' "$module_info" | /tmp/verify-gosu-metadata.sh json-field Dir "$module_dir"; \ + printf '%s\n' "$module_info" | /tmp/verify-gosu-metadata.sh json-field Sum "$GOSU_MODULE_SUM"; \ + printf '%s\n' "$module_info" | /tmp/verify-gosu-metadata.sh json-field GoModSum "$GOSU_GOMOD_SUM"; \ + printf '%s\n' "$module_info" | /tmp/verify-gosu-metadata.sh json-field Hash "$GOSU_SOURCE_COMMIT"; \ + printf '%s\n' "$module_info" | /tmp/verify-gosu-metadata.sh json-field VCS git; \ + printf '%s\n' "$module_info" | /tmp/verify-gosu-metadata.sh json-field URL https://github.com/tianon/gosu; \ + (cd "$module_dir" && go test -mod=readonly ./...); \ + go install -trimpath -buildvcs=false "${GOSU_MODULE}@${GOSU_MODULE_VERSION}"; \ + go version -m /out/bin/gosu > /tmp/gosu-build-info.txt; \ + test "$(head -n 1 /tmp/gosu-build-info.txt)" = '/out/bin/gosu: go1.26.9'; \ + /tmp/verify-gosu-metadata.sh module-row "$GOSU_MODULE" "$GOSU_MODULE_VERSION" "$GOSU_MODULE_SUM" < /tmp/gosu-build-info.txt; \ + mkdir -p /out/licenses; \ + install -m 0644 "$module_dir/LICENSE" /out/licenses/gosu-LICENSE; \ + install -m 0644 "$(go env GOMODCACHE)/github.com/moby/sys/user@v0.1.0/LICENSE" /out/licenses/moby-sys-user-LICENSE; \ + install -m 0644 "$(go env GOMODCACHE)/golang.org/x/sys@v0.1.0/LICENSE" /out/licenses/golang-x-sys-LICENSE; \ + install -m 0644 /usr/local/go/LICENSE /out/licenses/go-toolchain-LICENSE; \ + install -m 0644 /usr/local/go/PATENTS /out/licenses/go-toolchain-PATENTS + +FROM gitea.example.com/jens/itworx-mcp-hub/postgres:wp400-gosu-v2-20261002-a94e0b4d2029@sha256:a60d81e06987671ae903757f1b71cdfbbff4d481b42610e4de2b7dcfa32aad73 +LABEL org.itworx.gosu.go.version="go1.26.9" \ + org.itworx.gosu.go.archive.sha256="42d158b4d8f7b61ac0a830567c940a86098fb7aac52e467a5ebec03ef5cc2f8d" +COPY --from=gosu-build --chmod=0755 /out/bin/gosu /usr/local/bin/gosu +COPY --from=gosu-build /out/licenses/ /usr/share/doc/gosu/licenses/ diff --git a/scripts/contextforge_candidate_recipe.py b/scripts/contextforge_candidate_recipe.py index 0f726cc..f7b5b42 100644 --- a/scripts/contextforge_candidate_recipe.py +++ b/scripts/contextforge_candidate_recipe.py @@ -28,7 +28,14 @@ UV_VERSION = "0.12.10" LOCK_TIMESTAMP = "2026-10-02T00:00:00Z" GLOBAL_LOCK_CUTOFF = "2026-09-22T00:00:00Z" EXTRAS = ("redis", "postgres", "observability", "granian", "plugins", "llmchat") -CONSTRAINTS = ("mcp==1.30.0", "PyJWT==2.15.0", "urllib3==2.8.0") +SECURITY_LOCK_CUTOFF = "2026-10-14T00:00:00Z" +SECURITY_CONSTRAINTS = ( + "anyio==4.14.2", + "cryptography==50.0.0", + "langgraph-sdk==0.4.4", + "langgraph==1.2.3", +) +CONSTRAINTS = ("mcp==1.30.0", "PyJWT==2.15.0", "urllib3==2.8.0", *SECURITY_CONSTRAINTS) BUILD_TOOLS = ("pip==26.2.1", "setuptools==82.0.1", "wheel==0.46.2") SOURCE_HASHES = { @@ -125,6 +132,24 @@ def binary_postgres_pyproject(source_text: str) -> str: return source_text.replace(original, '"psycopg[binary]>=3.3.4"', 1) +def runtime_pyproject(source_text: str) -> str: + """Preserve upstream metadata except the reviewed binary and SDK compatibility fixes.""" + import tomllib + + text = binary_postgres_pyproject(source_text) + llmchat = tomllib.loads(text).get("project", {}).get("optional-dependencies", {}).get("llmchat") + if llmchat is not None: + original = '"langgraph<=1.2.0"' + if ( + not isinstance(llmchat, list) + or llmchat.count("langgraph<=1.2.0") != 1 + or text.count(original) != 1 + ): + raise CandidateRecipeError("upstream LangGraph compatibility contract changed") + text = text.replace(original, '"langgraph==1.2.3"', 1) + return text + + def candidate_pyproject(source_text: str) -> str: """Apply the narrowly scoped candidate-only dependency resolution policy.""" try: @@ -149,7 +174,7 @@ def candidate_pyproject(source_text: str) -> str: }: raise CandidateRecipeError("upstream uv source override shape changed") - text = binary_postgres_pyproject(source_text).replace("\r\n", "\n") + text = runtime_pyproject(source_text).replace("\r\n", "\n") source_start, source_end, source_table = _table_range(text, "tool.uv.sources") source_lines = source_table.strip().splitlines() expected_source_lines = [ @@ -171,7 +196,7 @@ def candidate_pyproject(source_text: str) -> str: text = text.replace( uv_anchor, f'exclude-newer = "{GLOBAL_LOCK_CUTOFF}"\n' - + 'constraint-dependencies = ["mcp==1.30.0", "PyJWT==2.15.0", "urllib3==2.8.0"]\n', + + f"constraint-dependencies = {json.dumps(list(CONSTRAINTS))}\n", 1, ) @@ -182,6 +207,10 @@ def candidate_pyproject(source_text: str) -> str: f'{name.lower()} = "{LOCK_TIMESTAMP}"\n' for name in ("mcp", "pyjwt", "urllib3", "pip", "setuptools", "wheel") ) + package_overrides += "".join( + f'{requirement.split("==", 1)[0]} = "{SECURITY_LOCK_CUTOFF}"\n' + for requirement in SECURITY_CONSTRAINTS + ) text = text.replace(package_table, package_table + package_overrides, 1) text = text.rstrip() + "\n\n[dependency-groups]\nbuild-tools = [\n" @@ -298,6 +327,7 @@ def _make_manifest( "buildTools": list(BUILD_TOOLS), "globalExcludeNewerUtc": GLOBAL_LOCK_CUTOFF, "freshnessOverrideUtc": LOCK_TIMESTAMP, + "securityFreshnessOverrideUtc": SECURITY_LOCK_CUTOFF, "requirements": { "production-requirements.lock": production_hash, "build-tools.lock": build_tools_hash, @@ -459,7 +489,7 @@ def prepare_source(source_dir: Path) -> None: manifest = _read_lock_manifest() verify_lock_contents(manifest) transformed = transform_containerfile(source["Containerfile"]) - project = binary_postgres_pyproject(source["pyproject.toml"].decode("utf-8")) + project = runtime_pyproject(source["pyproject.toml"].decode("utf-8")) for source_name, target_name in LOCK_OUTPUT_NAMES.items(): _write_atomic(source_dir / target_name, _require_file(LOCK_DIR / source_name)) diff --git a/scripts/verify.py b/scripts/verify.py index c62bebf..561076a 100644 --- a/scripts/verify.py +++ b/scripts/verify.py @@ -85,6 +85,7 @@ def main() -> int: "scripts/promotion_preflight.py", "scripts/publish_mcp_hub_ingestion.py", "scripts/build_mcp_hub_release_images.py", + "scripts/prepare_release_security_bases.py", "scripts/package_mcp_hub_release_artifact.py", "scripts/export_release.py", "scripts/render_release_record.py", @@ -304,6 +305,7 @@ def main() -> int: "scripts/promotion_preflight.py", "scripts/publish_mcp_hub_ingestion.py", "scripts/build_mcp_hub_release_images.py", + "scripts/prepare_release_security_bases.py", "scripts/package_mcp_hub_release_artifact.py", "scripts/discover_unraid.py", "scripts/revoke_demo_token.py", diff --git a/tests/test_contextforge_candidate_recipe.py b/tests/test_contextforge_candidate_recipe.py index 3d21057..9175550 100644 --- a/tests/test_contextforge_candidate_recipe.py +++ b/tests/test_contextforge_candidate_recipe.py @@ -23,6 +23,30 @@ def _hash(data: bytes) -> str: return hashlib.sha256(data).hexdigest() +def _prior_recipe_receipt_hash(data: bytes) -> str: + """Only this Git LF-normalized historical JSON receipt permits CRLF checkout bytes.""" + normalized = data.replace(b"\r\n", b"\n") + if b"\r" in normalized: + raise ValueError("unsupported historical receipt newline") + return _hash(normalized) + + +def test_prior_recipe_receipt_identity_accepts_only_git_lf_or_crlf_checkout() -> None: + import subprocess + + path = "evidence/WP-400/CF_POSTGRES_BINARY_RECIPE.json" + blob = subprocess.check_output(["git", "show", "HEAD:" + path], cwd=ROOT) # noqa: S603,S607 - fixed local Git object + assert b"\r" not in blob + assert ( + _prior_recipe_receipt_hash(blob) + == _prior_recipe_receipt_hash(blob.replace(b"\n", b"\r\n")) + == _hash(blob) + ) + assert _prior_recipe_receipt_hash(blob + b" ") != _hash(blob) + with pytest.raises(ValueError, match="unsupported historical receipt newline"): + _prior_recipe_receipt_hash(blob.replace(b"\n", b"\r", 1)) + + def _containerfile_fixture() -> bytes: return ( b"FROM base\n" @@ -134,10 +158,38 @@ dev = ["pytest>=8"] "2026-07-01T00:00:00Z" ) assert parsed["tool"]["uv"]["exclude-newer-package"]["pyjwt"] == "2026-10-02T00:00:00Z" + for package, version in ( + ("anyio", "4.14.2"), + ("cryptography", "50.0.0"), + ("langgraph-sdk", "0.4.4"), + ("langgraph", "1.2.3"), + ): + assert f"{package}=={version}" in parsed["tool"]["uv"]["constraint-dependencies"] + assert parsed["tool"]["uv"]["exclude-newer-package"][package] == "2026-10-14T00:00:00Z" assert parsed["dependency-groups"]["build-tools"] == list(recipe.BUILD_TOOLS) assert "dev" not in parsed["dependency-groups"] +@pytest.mark.parametrize( + "requirement", ["langgraph<=1.2.0", "langgraph==1.2.0", 'langgraph<=1.2.0","langgraph<=1.2.0'] +) +def test_runtime_langgraph_compatibility_patch_has_one_exact_source_anchor( + requirement: str, +) -> None: + source = ( + '[project.optional-dependencies]\npostgres=["psycopg[c,binary]>=3.3.4"]\n' + f'llmchat=["{requirement}"]\n' + ) + if requirement == "langgraph<=1.2.0": + result = recipe.runtime_pyproject(source) + assert 'llmchat=["langgraph==1.2.3"]' in result + assert 'postgres=["psycopg[binary]>=3.3.4"]' in result + assert "langgraph<=1.2.0" in source + else: + with pytest.raises(recipe.CandidateRecipeError, match="LangGraph compatibility"): + recipe.runtime_pyproject(source) + + def test_runtime_lock_rejects_source_only_postgres_adapter(monkeypatch) -> None: manifest = recipe._read_lock_manifest() original = recipe._require_file @@ -399,8 +451,16 @@ def test_frozen_base_evidence_matches_candidate_pins_and_repository_inputs() -> assert delta["historicalInputsSha256"] == { name: evidence["hubInputsSha256"][name] for name in changed } + security = json.loads((ROOT / "evidence/WP-400/CF_SECURITY_RECIPE.json").read_text()) + assert security["schema"] == "itworx.contextforge-security-recipe/v1" + assert security["priorRecipeEvidenceHashMode"] == "git-blob-lf" + assert security["priorRecipeEvidenceSha256"] == _prior_recipe_receipt_hash( + (ROOT / "evidence/WP-400/CF_POSTGRES_BINARY_RECIPE.json").read_bytes() + ) + assert security["historicalInputsSha256"] == delta["candidateInputsSha256"] + assert set(security["candidateInputsSha256"]) == changed for name, historical_hash in evidence["hubInputsSha256"].items(): - expected = delta["candidateInputsSha256"].get(name, historical_hash) + expected = security["candidateInputsSha256"].get(name, historical_hash) assert _hash((ROOT / name).read_bytes()) == expected assert delta["finalCandidateBuilt"] is False assert delta["policy"] == "psycopg[binary]==3.3.4; hash-locked wheels only" diff --git a/tests/test_release_images_workflow.py b/tests/test_release_images_workflow.py index decd770..3c96292 100644 --- a/tests/test_release_images_workflow.py +++ b/tests/test_release_images_workflow.py @@ -60,9 +60,12 @@ def test_release_credentials_only_reach_the_bounded_producer() -> None: command = steps[1]["run"] assert "set -euo pipefail" in command assert "set -x" not in command and "docker login" not in command - assert 'python3 scripts/build_mcp_hub_release_images.py --revision "$GITHUB_SHA"' in command + assert ( + "python3 scripts/build_mcp_hub_release_images.py --prepare-security-bases " + '--revision "$GITHUB_SHA"' in command + ) assert '--run-id "$GITHUB_RUN_ID"' in command - assert '--output "${RUNNER_TEMP}/hub-release-candidate-${GITHUB_SHA}"' in command + assert '--output "${RUNNER_TEMP}/hub-release-security-bases-${GITHUB_SHA}"' in command def test_release_artifact_is_pinned_uncompressed_and_cannot_overwrite() -> None: @@ -70,8 +73,8 @@ def test_release_artifact_is_pinned_uncompressed_and_cannot_overwrite() -> None: assert upload["uses"] == UPLOAD assert "if" not in upload assert upload["with"] == { - "name": "hub-release-candidate-${{ github.sha }}", - "path": "${{ runner.temp }}/hub-release-candidate-${{ github.sha }}/*", + "name": "hub-release-security-bases-${{ github.sha }}", + "path": "${{ runner.temp }}/hub-release-security-bases-${{ github.sha }}/*", "if-no-files-found": "error", "retention-days": 90, "compression-level": 0, diff --git a/tests/test_security_base_preparation.py b/tests/test_security_base_preparation.py new file mode 100644 index 0000000..fff3832 --- /dev/null +++ b/tests/test_security_base_preparation.py @@ -0,0 +1,214 @@ +import copy +import io +import json +import subprocess +import sys +from pathlib import Path + +import pytest + +from scripts import build_mcp_hub_release_images as producer +from scripts import prepare_release_security_bases as preparation + + +def test_actual_cli_loader_preserves_closed_exception_identity() -> None: + root = Path(__file__).resolve().parents[1] + program = """import ast,sys,types +from pathlib import Path +root=Path.cwd() +sys.path.insert(0,str(root/'scripts')) +path=root/'scripts/build_mcp_hub_release_images.py' +syntax=ast.parse(path.read_text()) +assert isinstance(syntax.body[-1],ast.If) +syntax.body.pop() +module=types.ModuleType('__main__') +module.__file__=str(path) +module.__package__='' +sys.modules['__main__']=module +exec(compile(syntax,str(path),'exec'),module.__dict__) +helper=module._security_preparation_module() +assert helper.producer is module +assert helper.producer.BuildRejected is module.BuildRejected +assert helper.producer.VulnerabilityRejected is module.VulnerabilityRejected +try: + helper.tag('unsupported','a'*40,123,1) +except module.BuildRejected as error: + assert str(error)=='security base publication identity is invalid' +else: + raise AssertionError('closed rejection missing') +print('CLI exception identity PASS') +""" + result = subprocess.run( # noqa: S603 - fixed interpreter and static CLI identity harness + [sys.executable, "-c", program], cwd=root, capture_output=True, text=True, timeout=20 + ) + assert result.returncode == 0, result.stderr + assert result.stdout.strip() == "CLI exception identity PASS" + + +def test_fixed_publication_names_bind_exact_protected_attempt() -> None: + assert preparation.tag("postgres", "a" * 40, 123, 2) == ( + producer.IMAGE_PREFIX + "/postgres:" + "a" * 40 + "-security-run-123-attempt-2" + ) + for args in [ + ("worker", "a" * 40, 123, 2), + ("postgres", "secret", 123, 2), + ("postgres", "a" * 40, 0, 2), + ("postgres", "a" * 40, 123, 0), + ]: + with pytest.raises(producer.BuildRejected): + preparation.tag(*args) + + +@pytest.mark.parametrize( + "field", + ["User", "Entrypoint", "Cmd", "Volumes", "Env", "Healthcheck", "WorkingDir", "StopSignal"], +) +def test_base_lifecycle_cannot_be_changed_or_echoed(field: str) -> None: + base = {"config": {field: "private-value"}} + preparation.verify_lifecycle(base, {"config": dict(base["config"])}) + with pytest.raises(producer.BuildRejected) as error: + preparation.verify_lifecycle(base, {"config": {field: "private-new-value"}}) + assert str(error.value) == "security base runtime lifecycle differs from its exact parent" + + +def test_unknown_download_asset_is_denied_before_network(tmp_path: Path) -> None: + with pytest.raises(producer.BuildRejected): + preparation.download_asset( + "secret", tmp_path, opener=lambda *args, **kwargs: pytest.fail("network called") + ) + + +def test_arbitrary_scanner_subset_cannot_replace_full_release(tmp_path: Path) -> None: + with pytest.raises(producer.BuildRejected, match="fixed release or preparation"): + producer._install_and_scan( + images={}, + temp_root=tmp_path, + env={}, + cwd=tmp_path, + runner=lambda *args, **kwargs: None, + services=("connector-gitea",), + ) + + +@pytest.mark.parametrize("data", [b"bad", b"longer-than-fixed-size"]) +def test_corrupt_assets_are_rejected_without_writing( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, data: bytes +) -> None: + monkeypatch.setitem(preparation.ASSETS, "go", ("https://go.dev/fixed", "asset", "a" * 64, 3)) + with pytest.raises(producer.BuildRejected) as error: + preparation.download_asset("go", tmp_path, opener=lambda *args, **kwargs: io.BytesIO(data)) + assert "bad" not in str(error.value) + assert not list(tmp_path.iterdir()) + + +@pytest.mark.parametrize("failure", [None, "scan", "lifecycle", "pull"]) +def test_only_verified_scanned_images_publish_and_no_candidate_is_emitted( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, failure: str | None +) -> None: + root = Path(__file__).resolve().parents[1] + output = tmp_path / "published" + events: list[str] = [] + built: dict[str, dict] = {} + parent = { + "imageId": "sha256:" + "b" * 64, + "config": { + "User": "", + "Entrypoint": ["/original"], + "Cmd": ["original"], + "Env": ["STATIC=unchanged"], + "Volumes": {"/original": {}}, + }, + } + + def execute(command: list[str], **kwargs: object) -> None: + stage = str(kwargs["stage"]) + events.append(stage) + if stage == "security_base_build": + reference = command[command.index("--tag") + 1] + config = copy.deepcopy(parent) + config["config"]["Labels"] = dict( + command[index + 1].split("=", 1) + for index, value in enumerate(command) + if value == "--label" + ) + if failure == "lifecycle": + config["config"]["User"] = "10001" + built[reference] = config + + def inspect(reference: str, *args: object, **kwargs: object) -> dict: + if reference in {value[0] for value in producer.EXTERNAL_IMAGES.values()}: + return copy.deepcopy(parent) + result = copy.deepcopy(built[reference.split("@", 1)[0]]) + if "@" in reference and failure == "pull": + result["imageId"] = "sha256:" + "c" * 64 + return result + + def scan(**kwargs: object) -> Path: + events.append("scan-both") + assert kwargs["services"] == producer.SECURITY_BASE_SERVICES + assert set(kwargs["images"]) == set(producer.SECURITY_BASE_SERVICES) + if failure == "scan": + raise producer.BuildRejected("synthetic scan denial") + reports = tmp_path / "reports" + reports.mkdir() + names = {"trivy-db-metadata.json"} | { + f"{kind}-{service}.json" + for service in producer.SECURITY_BASE_SERVICES + for kind in ("vulnerability", "secret") + } + for name in names: + producer._write_new(reports / name, b"{}\n") + return reports + + monkeypatch.setattr(producer, "_execute", execute) + monkeypatch.setattr(producer, "_inspect_image", inspect) + monkeypatch.setattr(producer, "_registry_tag_absent", lambda *args, **kwargs: None) + monkeypatch.setattr( + producer, + "_capture", + lambda *args, **kwargs: json.dumps( + { + "digest": "sha256:" + "d" * 64, + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "size": 123, + } + ), + ) + monkeypatch.setattr(producer, "_install_and_scan", scan) + monkeypatch.setattr(preparation, "download_asset", lambda *args, **kwargs: None) + arguments = dict( + root=root, + temp_root=tmp_path, + output=output, + env={}, + revision="a" * 40, + tree="e" * 40, + epoch="123", + built_at="2026-10-14T00:00:00Z", + run_id=123, + attempt=1, + runner=lambda *args, **kwargs: None, + ) + if failure is not None: + with pytest.raises(producer.BuildRejected): + preparation.prepare(**arguments) + assert not output.exists() + if failure != "pull": + assert "security_base_push" not in events + else: + receipt = preparation.prepare(**arguments) + assert ( + events.count("security_base_build") == events.count("security_base_runtime_verify") == 2 + ) + assert events.count("security_base_push") == 2 + assert events.index("scan-both") < events.index("security_base_push") + assert receipt["candidateEmitted"] is False and receipt["reportCount"] == 5 + assert [row["service"] for row in receipt["preparedImages"]] == list( + producer.SECURITY_BASE_SERVICES + ) + assert all( + row["archiveRef"].endswith("-security-run-123-attempt-1") + for row in receipt["preparedImages"] + ) + assert not (output / "candidate.json").exists() + assert not (output / "images.tar").exists() diff --git a/tests/test_security_pcre_inventory.py b/tests/test_security_pcre_inventory.py new file mode 100644 index 0000000..94fdc21 --- /dev/null +++ b/tests/test_security_pcre_inventory.py @@ -0,0 +1,47 @@ +import shutil +import subprocess +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parents[1] +SCRIPT = ROOT / "infrastructure/images/security-bases/ingestion-clamd/verify-pcre2.sh" + + +@pytest.mark.parametrize( + ("inventory", "accepted"), + [ + ("P:unrelated\nV:1.0\n\nP:pcre2\nV:10.49-r0\nT:normal description\n", True), + ("P:pcre2\nV:10.48-r0\n", False), + ("P:pcre2\n", False), + ("P:pcre2\nV:10.49-r0\nV:10.49-r0\n", False), + ("P:pcre2\nV:10.49-r0\n\nP:pcre2\nV:10.49-r0\n", False), + ("P:pcre2\nP:pcre2\nV:10.49-r0\n", False), + ("P:unrelated\nV:10.49-r0\n", False), + ("P:pcre2\nV:private-secret-never-echo\n", False), + ( + "pcre2-10.49-r0 description:\nPerl-compatible regular expressions library\n" + "pcre2-10.49-r0 webpage:\nhttps://www.pcre.org/\n" + "pcre2-10.49-r0 installed size:\n1234 B\n", + False, + ), + ], +) +def test_actual_inventory_query_rejects_ambiguous_or_verbose_apk_output( + inventory: str, accepted: bool +) -> None: + source = SCRIPT.read_text() + assert "apk info" not in source + query = source.split("awk -v RS='' '", 1)[1].split("\n' /lib/apk/db/installed", 1)[0] + binary = shutil.which("awk") + fallback = Path("C:/Program Files/Git/usr/bin/awk.exe") + if binary is None and fallback.is_file(): + binary = str(fallback) + if binary is None: + pytest.skip("real awk query runs in required native Linux CI") + result = subprocess.run( # noqa: S603 - exact trusted installed-inventory query + [binary, "-v", "RS=", query], input=inventory, text=True, capture_output=True, timeout=10 + ) + assert (result.returncode == 0) is accepted + assert result.stdout == ("10.49-r0\n" if accepted else "") + assert "private-secret" not in result.stdout + result.stderr