From 6abbce11ccd754a7b54907cd283a8bfa7f9e1c22 Mon Sep 17 00:00:00 2001 From: Dominic Ayre Date: Wed, 24 Sep 2025 11:20:18 +0000 Subject: [PATCH 01/18] Restore default behaviour of --upload-fragment and give new args for standalone fragments --- src/confcom/azext_confcom/_params.py | 8 +++ src/confcom/azext_confcom/custom.py | 22 ++++-- src/confcom/samples/certs/create_certchain.sh | 72 ++++++++++--------- 3 files changed, 64 insertions(+), 38 deletions(-) diff --git a/src/confcom/azext_confcom/_params.py b/src/confcom/azext_confcom/_params.py index 855973176ac..b376535b52e 100644 --- a/src/confcom/azext_confcom/_params.py +++ b/src/confcom/azext_confcom/_params.py @@ -319,6 +319,14 @@ def load_arguments(self, _): help="Upload a policy fragment to a container registry", validator=validate_upload_fragment, ) + c.argument( + "push_fragment_to", + help="The reference to push the fragment to", + ) + c.argument( + "attach_fragment_to", + help="The image reference to attach the fragment to", + ) c.argument( "no_print", options_list=("--no-print",), diff --git a/src/confcom/azext_confcom/custom.py b/src/confcom/azext_confcom/custom.py index 4405fefcc14..baf5abea035 100644 --- a/src/confcom/azext_confcom/custom.py +++ b/src/confcom/azext_confcom/custom.py @@ -5,6 +5,7 @@ import os import sys +from typing import Optional from azext_confcom import oras_proxy, os_util, security_policy from azext_confcom.config import ( @@ -232,6 +233,8 @@ def acifragmentgen_confcom( output_filename: str = "", outraw: bool = False, upload_fragment: bool = False, + push_fragment_to: Optional[str] = None, + attach_fragment_to: Optional[str] = None, no_print: bool = False, fragments_json: str = "", ): @@ -335,10 +338,21 @@ def acifragmentgen_confcom( out_path = filename + ".cose" cose_proxy.cose_sign(filename, key, chain, feed, iss, algo, out_path) - if upload_fragment and image_target: - oras_proxy.attach_fragment_to_image(image_target, out_path) - elif upload_fragment: - oras_proxy.push_fragment_to_registry(feed, out_path) + + # Preserve default behaviour established since version 1.1.0 of attaching + # the fragment to the first image specified in input + # (or --image-target if specified) + if upload_fragment: + oras_proxy.attach_fragment_to_image( + image_name=image_target or policy_images[0].containerImage, + filename=out_path, + ) + + if push_fragment_to: + oras_proxy.push_fragment_to_registry(push_fragment_to, out_path) + + if attach_fragment_to: + oras_proxy.attach_fragment_to_image(attach_fragment_to, out_path) def katapolicygen_confcom( diff --git a/src/confcom/samples/certs/create_certchain.sh b/src/confcom/samples/certs/create_certchain.sh index 5e94f4c6f4e..48575efef3f 100755 --- a/src/confcom/samples/certs/create_certchain.sh +++ b/src/confcom/samples/certs/create_certchain.sh @@ -3,87 +3,91 @@ OriginalPath=`pwd` RootPath=`realpath $(dirname $0)` -cd $RootPath +OutPath=${1:-$RootPath} + +mkdir -p $OutPath + +cd $OutPath # create dirs for root CA -mkdir -p $RootPath/rootCA/{certs,crl,newcerts,private,csr} -mkdir -p $RootPath/intermediateCA/{certs,crl,newcerts,private,csr} +mkdir -p $OutPath/rootCA/{certs,crl,newcerts,private,csr} +mkdir -p $OutPath/intermediateCA/{certs,crl,newcerts,private,csr} # create index files -echo 1000 > $RootPath/rootCA/serial -echo 1000 > $RootPath/intermediateCA/serial +echo 1000 > $OutPath/rootCA/serial +echo 1000 > $OutPath/intermediateCA/serial # create crlnumbers -echo 0100 > $RootPath/rootCA/crlnumber -echo 0100 > $RootPath/intermediateCA/crlnumber +echo 0100 > $OutPath/rootCA/crlnumber +echo 0100 > $OutPath/intermediateCA/crlnumber # create index files -touch $RootPath/rootCA/index.txt -touch $RootPath/intermediateCA/index.txt +touch $OutPath/rootCA/index.txt +touch $OutPath/intermediateCA/index.txt # NOTE: needed for testing -echo "unique_subject = no" >> $RootPath/rootCA/index.txt.attr -echo "unique_subject = no" >> $RootPath/intermediateCA/index.txt.attr +echo "unique_subject = no" >> $OutPath/rootCA/index.txt.attr +echo "unique_subject = no" >> $OutPath/intermediateCA/index.txt.attr # generate root key -openssl genrsa -out $RootPath/rootCA/private/ca.key.pem 4096 -chmod 400 $RootPath/rootCA/private/ca.key.pem +openssl genrsa -out $OutPath/rootCA/private/ca.key.pem 4096 +chmod 400 $OutPath/rootCA/private/ca.key.pem # view the key -# openssl rsa -noout -text -in $RootPath/rootCA/private/ca.key.pem +# openssl rsa -noout -text -in $OutPath/rootCA/private/ca.key.pem # generate root cert -openssl req -config openssl_root.cnf -key $RootPath/rootCA/private/ca.key.pem -new -x509 -days 7300 -sha256 -extensions v3_ca -out $RootPath/rootCA/certs/ca.cert.pem -subj "/C=US/ST=Georgia/L=Atlanta/O=Microsoft/OU=ACCCT/CN=Root CA" +openssl req -config $RootPath/openssl_root.cnf -key $OutPath/rootCA/private/ca.key.pem -new -x509 -days 7300 -sha256 -extensions v3_ca -out $OutPath/rootCA/certs/ca.cert.pem -subj "/C=US/ST=Georgia/L=Atlanta/O=Microsoft/OU=ACCCT/CN=Root CA" # change permissions on root key so it's not globally readable -chmod 644 $RootPath/rootCA/certs/ca.cert.pem +chmod 644 $OutPath/rootCA/certs/ca.cert.pem # verify root cert -openssl x509 -noout -text -in $RootPath/rootCA/certs/ca.cert.pem +openssl x509 -noout -text -in $OutPath/rootCA/certs/ca.cert.pem # generate intermediate key -openssl genrsa -out $RootPath/intermediateCA/private/intermediate.key.pem 4096 -chmod 600 $RootPath/intermediateCA/private/intermediate.key.pem +openssl genrsa -out $OutPath/intermediateCA/private/intermediate.key.pem 4096 +chmod 600 $OutPath/intermediateCA/private/intermediate.key.pem # make CSR for intermediate -openssl req -config openssl_intermediate.cnf -key $RootPath/intermediateCA/private/intermediate.key.pem -new -sha256 -out $RootPath/intermediateCA/certs/intermediate.csr.pem -subj "/C=US/ST=Georgia/L=Atlanta/O=Microsoft/OU=ACCCT/CN=Intermediate CA" +openssl req -config $RootPath/openssl_intermediate.cnf -key $OutPath/intermediateCA/private/intermediate.key.pem -new -sha256 -out $OutPath/intermediateCA/certs/intermediate.csr.pem -subj "/C=US/ST=Georgia/L=Atlanta/O=Microsoft/OU=ACCCT/CN=Intermediate CA" # sign intermediate cert with root -openssl ca -config openssl_root.cnf -extensions v3_intermediate_ca -days 3650 -notext -md sha256 -in $RootPath/intermediateCA/certs/intermediate.csr.pem -out $RootPath/intermediateCA/certs/intermediate.cert.pem -batch +openssl ca -config $RootPath/openssl_root.cnf -extensions v3_intermediate_ca -days 3650 -notext -md sha256 -in $OutPath/intermediateCA/certs/intermediate.csr.pem -out $OutPath/intermediateCA/certs/intermediate.cert.pem -batch # make it readable by everyone -chmod 644 $RootPath/intermediateCA/certs/intermediate.cert.pem +chmod 644 $OutPath/intermediateCA/certs/intermediate.cert.pem # print the cert -# openssl x509 -noout -text -in $RootPath/intermediateCA/certs/intermediate.cert.pem +# openssl x509 -noout -text -in $OutPath/intermediateCA/certs/intermediate.cert.pem # verify intermediate cert -openssl verify -CAfile $RootPath/rootCA/certs/ca.cert.pem $RootPath/intermediateCA/certs/intermediate.cert.pem +openssl verify -CAfile $OutPath/rootCA/certs/ca.cert.pem $OutPath/intermediateCA/certs/intermediate.cert.pem # create chain file -cat $RootPath/intermediateCA/certs/intermediate.cert.pem $RootPath/rootCA/certs/ca.cert.pem > $RootPath/intermediateCA/certs/ca-chain.cert.pem +cat $OutPath/intermediateCA/certs/intermediate.cert.pem $OutPath/rootCA/certs/ca.cert.pem > $OutPath/intermediateCA/certs/ca-chain.cert.pem # verify chain -openssl verify -CAfile $RootPath/intermediateCA/certs/ca-chain.cert.pem $RootPath/intermediateCA/certs/intermediate.cert.pem +openssl verify -CAfile $OutPath/intermediateCA/certs/ca-chain.cert.pem $OutPath/intermediateCA/certs/intermediate.cert.pem # create server key -openssl ecparam -out $RootPath/intermediateCA/private/www.contoso.com.key.pem -name secp384r1 -genkey -openssl pkcs8 -topk8 -nocrypt -in $RootPath/intermediateCA/private/www.contoso.com.key.pem -out $RootPath/intermediateCA/private/ec_p384_private.pem +openssl ecparam -out $OutPath/intermediateCA/private/www.contoso.com.key.pem -name secp384r1 -genkey +openssl pkcs8 -topk8 -nocrypt -in $OutPath/intermediateCA/private/www.contoso.com.key.pem -out $OutPath/intermediateCA/private/ec_p384_private.pem -chmod 600 $RootPath/intermediateCA/private/www.contoso.com.key.pem +chmod 600 $OutPath/intermediateCA/private/www.contoso.com.key.pem # create csr for server -openssl req -config openssl_intermediate.cnf -key $RootPath/intermediateCA/private/www.contoso.com.key.pem -new -sha384 -out $RootPath/intermediateCA/csr/www.contoso.com.csr.pem -batch +openssl req -config $RootPath/openssl_intermediate.cnf -key $OutPath/intermediateCA/private/www.contoso.com.key.pem -new -sha384 -out $OutPath/intermediateCA/csr/www.contoso.com.csr.pem -batch # sign server cert with intermediate key -openssl ca -config openssl_intermediate.cnf -extensions server_cert -days 375 -notext -md sha384 -in $RootPath/intermediateCA/csr/www.contoso.com.csr.pem -out $RootPath/intermediateCA/certs/www.contoso.com.cert.pem -batch +openssl ca -config $RootPath/openssl_intermediate.cnf -extensions server_cert -days 375 -notext -md sha384 -in $OutPath/intermediateCA/csr/www.contoso.com.csr.pem -out $OutPath/intermediateCA/certs/www.contoso.com.cert.pem -batch # print the cert -# openssl x509 -noout -text -in $RootPath/intermediateCA/certs/www.contoso.com.cert.pem +# openssl x509 -noout -text -in $OutPath/intermediateCA/certs/www.contoso.com.cert.pem # make a public key -# openssl x509 -pubkey -noout -in $RootPath/intermediateCA/certs/www.contoso.com.cert.pem -out $RootPath/intermediateCA/certs/pubkey.pem +# openssl x509 -pubkey -noout -in $OutPath/intermediateCA/certs/www.contoso.com.cert.pem -out $OutPath/intermediateCA/certs/pubkey.pem # create chain file -cat $RootPath/intermediateCA/certs/www.contoso.com.cert.pem $RootPath/intermediateCA/certs/intermediate.cert.pem $RootPath/rootCA/certs/ca.cert.pem > $RootPath/intermediateCA/certs/www.contoso.com.chain.cert.pem +cat $OutPath/intermediateCA/certs/www.contoso.com.cert.pem $OutPath/intermediateCA/certs/intermediate.cert.pem $OutPath/rootCA/certs/ca.cert.pem > $OutPath/intermediateCA/certs/www.contoso.com.chain.cert.pem cd $OriginalPath \ No newline at end of file From 7e956d675d6ebc262411d40007015ef7371ffdc3 Mon Sep 17 00:00:00 2001 From: Dominic Ayre Date: Wed, 24 Sep 2025 14:27:42 +0000 Subject: [PATCH 02/18] Add testing to enforce behaviour --- .../latest/test_confcom_acifragmentgen.py | 205 ++++++++++++++++++ 1 file changed, 205 insertions(+) create mode 100644 src/confcom/azext_confcom/tests/latest/test_confcom_acifragmentgen.py diff --git a/src/confcom/azext_confcom/tests/latest/test_confcom_acifragmentgen.py b/src/confcom/azext_confcom/tests/latest/test_confcom_acifragmentgen.py new file mode 100644 index 00000000000..211aece3cac --- /dev/null +++ b/src/confcom/azext_confcom/tests/latest/test_confcom_acifragmentgen.py @@ -0,0 +1,205 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from itertools import product +import json +import os +import subprocess +import tempfile +import pytest +import docker + +from azext_confcom.custom import acifragmentgen_confcom + +TEST_DIR = os.path.abspath(os.path.join(os.path.abspath(__file__), "..")) +SAMPLES_DIR = os.path.abspath(os.path.join(TEST_DIR, "..", "..", "..", "samples")) + + +@pytest.fixture() +def docker_image(): + + client = docker.from_env() + + registry_container = client.containers.run( + image="registry:2", + detach=True, + ports={"5000/tcp": 0}, + ) + registry_container.reload() + registry_port = registry_container.attrs['NetworkSettings']['Ports']['5000/tcp'][0]['HostPort'] + + test_container_ref = f"localhost:{registry_port}/hello-world:latest" + client.images.pull("hello-world").tag(test_container_ref) + client.images.push(test_container_ref) + + with tempfile.NamedTemporaryFile(mode="w+", encoding="utf-8", delete=True) as temp_file: + json.dump({ + "version": "1.0.0", + "containers": [ + { + "name": "hello-world", + "properties": { + "image": test_container_ref, + }, + } + ] + }, temp_file) + temp_file.flush() + + yield test_container_ref, temp_file.name + + registry_container.stop() + registry_container.remove() + + +@pytest.fixture(scope="session") +def cert_chain(): + with tempfile.TemporaryDirectory(delete=True) as temp_dir: + subprocess.run( + [ + os.path.join(SAMPLES_DIR, "certs", "create_certchain.sh"), + temp_dir + ], + check=True, + ) + yield temp_dir + + +def test_acifragmentgen_fragment_gen(docker_image): + + image_ref, spec_file_path = docker_image + + with tempfile.TemporaryDirectory(delete=True) as temp_dir: # Prevent test writing files to repo + acifragmentgen_confcom( + image_name=None, + tar_mapping_location=None, + key=None, + chain=None, + minimum_svn=None, + input_path=spec_file_path, + svn="1", + namespace="contoso", + feed="test-feed", + outraw=True, + output_filename=os.path.join(temp_dir, "fragment.rego"), + ) + + # TODO: Implement a proper validation for the fragment, this is hard + # because each test run will have a unique image to have unique local + # registries on different ports + + +def test_acifragmentgen_fragment_sign(docker_image, cert_chain): + + image_ref, spec_file_path = docker_image + + with tempfile.TemporaryDirectory(delete=True) as temp_dir: # Prevent test writing files to repo + acifragmentgen_confcom( + image_name=None, + tar_mapping_location=None, + key=os.path.join(cert_chain, "intermediateCA", "private", "ec_p384_private.pem"), + chain=os.path.join(cert_chain, "intermediateCA", "certs", "www.contoso.com.chain.cert.pem"), + minimum_svn=None, + input_path=spec_file_path, + svn="1", + namespace="contoso", + feed="test-feed", + outraw=True, + output_filename=os.path.join(temp_dir, "fragment.rego"), + ) + + # TODO: Implement a proper validation for the cose document + + +def test_acifragmentgen_fragment_upload_fragment(docker_image, cert_chain): + + image_ref, spec_file_path = docker_image + + with tempfile.TemporaryDirectory(delete=True) as temp_dir: # Prevent test writing files to repo + acifragmentgen_confcom( + image_name=None, + tar_mapping_location=None, + key=os.path.join(cert_chain, "intermediateCA", "private", "ec_p384_private.pem"), + chain=os.path.join(cert_chain, "intermediateCA", "certs", "www.contoso.com.chain.cert.pem"), + minimum_svn=None, + input_path=spec_file_path, + svn="1", + namespace="contoso", + feed="test-feed", + outraw=True, + upload_fragment=True, + output_filename=os.path.relpath(os.path.join(temp_dir, "fragment.rego"), os.getcwd()), # Must be relative for oras + ) + + oras_referrers = subprocess.run( + ["oras", "discover", image_ref], + stdout=subprocess.PIPE, + text=True, + check=True + ).stdout + + # Confirm the fragment is attached to the image + assert "application/x-ms-ccepolicy-frag" in oras_referrers + + +def test_acifragmentgen_fragment_push(docker_image, cert_chain): + + image_ref, spec_file_path = docker_image + fragment_ref = image_ref.replace("hello-world", "fragment") + + with tempfile.TemporaryDirectory(delete=True) as temp_dir: # Prevent test writing files to repo + acifragmentgen_confcom( + image_name=None, + tar_mapping_location=None, + key=os.path.join(cert_chain, "intermediateCA", "private", "ec_p384_private.pem"), + chain=os.path.join(cert_chain, "intermediateCA", "certs", "www.contoso.com.chain.cert.pem"), + minimum_svn=None, + input_path=spec_file_path, + svn="1", + namespace="contoso", + feed="test-feed", + outraw=True, + push_fragment_to=fragment_ref, + output_filename=os.path.relpath(os.path.join(temp_dir, "fragment.rego"), os.getcwd()), # Must be relative for oras + ) + + # Confirm the fragment exists in the registry + subprocess.run( + ["oras", "discover", fragment_ref], + stdout=subprocess.PIPE, + text=True, + check=True, + ).stdout + + +def test_acifragmentgen_fragment_attach(docker_image, cert_chain): + + image_ref, spec_file_path = docker_image + + with tempfile.TemporaryDirectory(delete=True) as temp_dir: # Prevent test writing files to repo + acifragmentgen_confcom( + image_name=None, + tar_mapping_location=None, + key=os.path.join(cert_chain, "intermediateCA", "private", "ec_p384_private.pem"), + chain=os.path.join(cert_chain, "intermediateCA", "certs", "www.contoso.com.chain.cert.pem"), + minimum_svn=None, + input_path=spec_file_path, + svn="1", + namespace="contoso", + feed="test-feed", + outraw=True, + attach_fragment_to=image_ref, + output_filename=os.path.relpath(os.path.join(temp_dir, "fragment.rego"), os.getcwd()), # Must be relative for oras + ) + + oras_referrers = subprocess.run( + ["oras", "discover", image_ref], + stdout=subprocess.PIPE, + text=True, + check=True + ).stdout + + # Confirm the fragment is attached to the image + assert "application/x-ms-ccepolicy-frag" in oras_referrers, oras_referrers From 2bcdc2f77a730a18ecb04407aae79ac10eaffd08 Mon Sep 17 00:00:00 2001 From: Dominic Ayre Date: Wed, 24 Sep 2025 15:28:21 +0000 Subject: [PATCH 03/18] Test possible fixes for ado pipeline failure --- .../tests/latest/test_confcom_acifragmentgen.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/confcom/azext_confcom/tests/latest/test_confcom_acifragmentgen.py b/src/confcom/azext_confcom/tests/latest/test_confcom_acifragmentgen.py index 211aece3cac..730df20e2aa 100644 --- a/src/confcom/azext_confcom/tests/latest/test_confcom_acifragmentgen.py +++ b/src/confcom/azext_confcom/tests/latest/test_confcom_acifragmentgen.py @@ -8,6 +8,7 @@ import os import subprocess import tempfile +import time import pytest import docker @@ -27,12 +28,15 @@ def docker_image(): detach=True, ports={"5000/tcp": 0}, ) + time.sleep(10) # TODO: Replace with polling registry_container.reload() registry_port = registry_container.attrs['NetworkSettings']['Ports']['5000/tcp'][0]['HostPort'] - test_container_ref = f"localhost:{registry_port}/hello-world:latest" - client.images.pull("hello-world").tag(test_container_ref) - client.images.push(test_container_ref) + test_container_repo = f"localhost:{registry_port}/hello-world" + test_container_tag = "latest" + test_container_ref = f"{test_container_repo}:{test_container_tag}" + client.images.pull("hello-world").tag(repository=test_container_repo, tag=test_container_tag) + client.images.push(repository=test_container_repo, tag=test_container_tag) with tempfile.NamedTemporaryFile(mode="w+", encoding="utf-8", delete=True) as temp_file: json.dump({ From 7705c49d2dbfa332b65cd38d3bfa2c30fca7e47e Mon Sep 17 00:00:00 2001 From: Dominic Ayre Date: Wed, 24 Sep 2025 15:51:16 +0000 Subject: [PATCH 04/18] Avoid using localhost for docker operations --- .../azext_confcom/tests/latest/test_confcom_acifragmentgen.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/confcom/azext_confcom/tests/latest/test_confcom_acifragmentgen.py b/src/confcom/azext_confcom/tests/latest/test_confcom_acifragmentgen.py index 730df20e2aa..98650990275 100644 --- a/src/confcom/azext_confcom/tests/latest/test_confcom_acifragmentgen.py +++ b/src/confcom/azext_confcom/tests/latest/test_confcom_acifragmentgen.py @@ -32,9 +32,9 @@ def docker_image(): registry_container.reload() registry_port = registry_container.attrs['NetworkSettings']['Ports']['5000/tcp'][0]['HostPort'] - test_container_repo = f"localhost:{registry_port}/hello-world" + test_container_repo = f"127.0.0.1:{registry_port}/hello-world" test_container_tag = "latest" - test_container_ref = f"{test_container_repo}:{test_container_tag}" + test_container_ref = f"localhost:{registry_port}/hello-world:{test_container_tag}" client.images.pull("hello-world").tag(repository=test_container_repo, tag=test_container_tag) client.images.push(repository=test_container_repo, tag=test_container_tag) From 1bc04fa96db18a188e3bac91aab0ca5713bb8075 Mon Sep 17 00:00:00 2001 From: Dominic Ayre Date: Wed, 24 Sep 2025 16:08:35 +0000 Subject: [PATCH 05/18] Remove arg for TemporaryDirectory which is only in newer python version --- .../tests/latest/test_confcom_acifragmentgen.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/confcom/azext_confcom/tests/latest/test_confcom_acifragmentgen.py b/src/confcom/azext_confcom/tests/latest/test_confcom_acifragmentgen.py index 98650990275..12ff9ce47d4 100644 --- a/src/confcom/azext_confcom/tests/latest/test_confcom_acifragmentgen.py +++ b/src/confcom/azext_confcom/tests/latest/test_confcom_acifragmentgen.py @@ -60,7 +60,7 @@ def docker_image(): @pytest.fixture(scope="session") def cert_chain(): - with tempfile.TemporaryDirectory(delete=True) as temp_dir: + with tempfile.TemporaryDirectory() as temp_dir: subprocess.run( [ os.path.join(SAMPLES_DIR, "certs", "create_certchain.sh"), @@ -75,7 +75,7 @@ def test_acifragmentgen_fragment_gen(docker_image): image_ref, spec_file_path = docker_image - with tempfile.TemporaryDirectory(delete=True) as temp_dir: # Prevent test writing files to repo + with tempfile.TemporaryDirectory() as temp_dir: # Prevent test writing files to repo acifragmentgen_confcom( image_name=None, tar_mapping_location=None, @@ -99,7 +99,7 @@ def test_acifragmentgen_fragment_sign(docker_image, cert_chain): image_ref, spec_file_path = docker_image - with tempfile.TemporaryDirectory(delete=True) as temp_dir: # Prevent test writing files to repo + with tempfile.TemporaryDirectory() as temp_dir: # Prevent test writing files to repo acifragmentgen_confcom( image_name=None, tar_mapping_location=None, @@ -121,7 +121,7 @@ def test_acifragmentgen_fragment_upload_fragment(docker_image, cert_chain): image_ref, spec_file_path = docker_image - with tempfile.TemporaryDirectory(delete=True) as temp_dir: # Prevent test writing files to repo + with tempfile.TemporaryDirectory() as temp_dir: # Prevent test writing files to repo acifragmentgen_confcom( image_name=None, tar_mapping_location=None, @@ -153,7 +153,7 @@ def test_acifragmentgen_fragment_push(docker_image, cert_chain): image_ref, spec_file_path = docker_image fragment_ref = image_ref.replace("hello-world", "fragment") - with tempfile.TemporaryDirectory(delete=True) as temp_dir: # Prevent test writing files to repo + with tempfile.TemporaryDirectory() as temp_dir: # Prevent test writing files to repo acifragmentgen_confcom( image_name=None, tar_mapping_location=None, @@ -182,7 +182,7 @@ def test_acifragmentgen_fragment_attach(docker_image, cert_chain): image_ref, spec_file_path = docker_image - with tempfile.TemporaryDirectory(delete=True) as temp_dir: # Prevent test writing files to repo + with tempfile.TemporaryDirectory() as temp_dir: # Prevent test writing files to repo acifragmentgen_confcom( image_name=None, tar_mapping_location=None, From 5c3a4e88b035b7da9fb59ff9271e02eb03012a65 Mon Sep 17 00:00:00 2001 From: Dominic Ayre Date: Wed, 24 Sep 2025 16:42:37 +0000 Subject: [PATCH 06/18] Replace docker python SDK with CLI --- .../latest/test_confcom_acifragmentgen.py | 34 ++++++++----------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/src/confcom/azext_confcom/tests/latest/test_confcom_acifragmentgen.py b/src/confcom/azext_confcom/tests/latest/test_confcom_acifragmentgen.py index 12ff9ce47d4..c9f3028ab37 100644 --- a/src/confcom/azext_confcom/tests/latest/test_confcom_acifragmentgen.py +++ b/src/confcom/azext_confcom/tests/latest/test_confcom_acifragmentgen.py @@ -3,14 +3,11 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- -from itertools import product import json import os import subprocess import tempfile -import time import pytest -import docker from azext_confcom.custom import acifragmentgen_confcom @@ -21,22 +18,22 @@ @pytest.fixture() def docker_image(): - client = docker.from_env() + registry_id = subprocess.run( + ["docker", "run", "-d", "-p", "0:5000", "registry:2"], + stdout=subprocess.PIPE, + text=True, + ).stdout - registry_container = client.containers.run( - image="registry:2", - detach=True, - ports={"5000/tcp": 0}, - ) - time.sleep(10) # TODO: Replace with polling - registry_container.reload() - registry_port = registry_container.attrs['NetworkSettings']['Ports']['5000/tcp'][0]['HostPort'] + registry_port = subprocess.run( + ["docker", "port", registry_id], + stdout=subprocess.PIPE, + text=True, + ).stdout.split(":")[-1].strip() - test_container_repo = f"127.0.0.1:{registry_port}/hello-world" - test_container_tag = "latest" - test_container_ref = f"localhost:{registry_port}/hello-world:{test_container_tag}" - client.images.pull("hello-world").tag(repository=test_container_repo, tag=test_container_tag) - client.images.push(repository=test_container_repo, tag=test_container_tag) + test_container_ref = f"localhost:{registry_port}/hello-world:latest" + subprocess.run(["docker", "pull", "hello-world"]) + subprocess.run(["docker", "tag", "hello-world", test_container_ref]) + subprocess.run(["docker", "push", test_container_ref]) with tempfile.NamedTemporaryFile(mode="w+", encoding="utf-8", delete=True) as temp_file: json.dump({ @@ -54,8 +51,7 @@ def docker_image(): yield test_container_ref, temp_file.name - registry_container.stop() - registry_container.remove() + subprocess.run(["docker", "stop", registry_id]) @pytest.fixture(scope="session") From ccc4478dac63b319d1cfd9178efec04b29b74d36 Mon Sep 17 00:00:00 2001 From: Dominic Ayre Date: Wed, 24 Sep 2025 17:11:41 +0000 Subject: [PATCH 07/18] Bump the version of confcom --- src/confcom/HISTORY.rst | 6 ++++++ src/confcom/setup.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/confcom/HISTORY.rst b/src/confcom/HISTORY.rst index 3006fd18e73..0edb9c8b37d 100644 --- a/src/confcom/HISTORY.rst +++ b/src/confcom/HISTORY.rst @@ -3,6 +3,12 @@ Release History =============== +1.3.0 +++++++ +* restored the behaviour of --upload-fragment in acifragmentgen to attach to first image in input +* added --push-fragment-to flag to acifragmentgen to allow explicit uploading of standalone fragments +* added --attach-fragment-to flag to acifragmentgen to allow explicit uploading of image attached fragments + 1.2.7 ++++++ * bugfix making it so that oras discover function doesn't error when no fragments are found in the remote repository diff --git a/src/confcom/setup.py b/src/confcom/setup.py index 3ce7907b25a..df6c58e0180 100644 --- a/src/confcom/setup.py +++ b/src/confcom/setup.py @@ -19,7 +19,7 @@ logger.warn("Wheel is not available, disabling bdist_wheel hook") -VERSION = "1.2.7" +VERSION = "1.3.0" # The full list of classifiers is available at # https://pypi.python.org/pypi?%3Aaction=list_classifiers From f1c710edfd9be91ceadfdaefc83dfbbeca432b33 Mon Sep 17 00:00:00 2001 From: Dominic Ayre Date: Thu, 25 Sep 2025 14:28:29 +0000 Subject: [PATCH 08/18] Split fragment push and fragment attach into standalone tools --- src/confcom/azext_confcom/_params.py | 43 +++++- .../azext_confcom/command/fragment_attach.py | 42 ++++++ .../azext_confcom/command/fragment_push.py | 42 ++++++ src/confcom/azext_confcom/commands.py | 4 + src/confcom/azext_confcom/custom.py | 43 ++++-- .../latest/test_confcom_acifragmentgen.py | 139 +++++++++++------- .../tests/latest/test_confcom_fragment.py | 6 +- 7 files changed, 246 insertions(+), 73 deletions(-) create mode 100644 src/confcom/azext_confcom/command/fragment_attach.py create mode 100644 src/confcom/azext_confcom/command/fragment_push.py diff --git a/src/confcom/azext_confcom/_params.py b/src/confcom/azext_confcom/_params.py index b376535b52e..f45255d172b 100644 --- a/src/confcom/azext_confcom/_params.py +++ b/src/confcom/azext_confcom/_params.py @@ -4,6 +4,8 @@ # -------------------------------------------------------------------------------------------- # pylint: disable=line-too-long +import argparse +import sys from knack.arguments import CLIArgumentType from azext_confcom._validators import ( validate_params_file, @@ -42,6 +44,32 @@ def load_arguments(self, _): c.argument("tags", tags_type) c.argument("confcom_name", confcom_name_type, options_list=["--name", "-n"]) + with self.argument_context("confcom fragment attach") as c: + c.positional( + "signed_fragment", + nargs='?', + type=argparse.FileType('rb'), + default=sys.stdin.buffer, + help="Signed fragment to attach", + ) + c.argument( + "manifest_tag", + help="Manifest tag for the fragment", + ) + + with self.argument_context("confcom fragment push") as c: + c.positional( + "signed_fragment", + nargs='?', + type=argparse.FileType('rb'), + default=sys.stdin.buffer, + help="Signed fragment to push", + ) + c.argument( + "manifest_tag", + help="Manifest tag for the fragment", + ) + with self.argument_context("confcom acipolicygen") as c: c.argument( "input_path", @@ -319,14 +347,6 @@ def load_arguments(self, _): help="Upload a policy fragment to a container registry", validator=validate_upload_fragment, ) - c.argument( - "push_fragment_to", - help="The reference to push the fragment to", - ) - c.argument( - "attach_fragment_to", - help="The image reference to attach the fragment to", - ) c.argument( "no_print", options_list=("--no-print",), @@ -340,6 +360,13 @@ def load_arguments(self, _): help="Path to JSON file to write fragment import information. This is used with --generate-import. If not specified, the import statement will print to the console", validator=validate_fragment_json, ) + c.argument( + "out_signed_fragment", + action="store_true", + default=False, + required=False, + help="Emit only the signed fragment bytes", + ) with self.argument_context("confcom katapolicygen") as c: c.argument( diff --git a/src/confcom/azext_confcom/command/fragment_attach.py b/src/confcom/azext_confcom/command/fragment_attach.py new file mode 100644 index 00000000000..928dbc345e3 --- /dev/null +++ b/src/confcom/azext_confcom/command/fragment_attach.py @@ -0,0 +1,42 @@ + +import os +import subprocess +import tempfile +from typing import BinaryIO + + +def oras_attach( + signed_fragment: BinaryIO, + manifest_tag: str, +) -> None: + subprocess.run( + [ + "oras", + "attach", + "--artifact-type", "application/x-ms-ccepolicy-frag", + manifest_tag, + os.path.relpath(signed_fragment.name, start=os.getcwd()), + ], + check=True, + timeout=120, + ) + + +def fragment_attach( + signed_fragment: BinaryIO, + manifest_tag: str, +) -> None: + + if signed_fragment.name == "": + with tempfile.NamedTemporaryFile(delete=True) as temp_signed_fragment: + temp_signed_fragment.write(signed_fragment.read()) + temp_signed_fragment.flush() + oras_attach( + signed_fragment=temp_signed_fragment, + manifest_tag=manifest_tag, + ) + else: + oras_attach( + signed_fragment=signed_fragment, + manifest_tag=manifest_tag, + ) diff --git a/src/confcom/azext_confcom/command/fragment_push.py b/src/confcom/azext_confcom/command/fragment_push.py new file mode 100644 index 00000000000..1f90b57fab5 --- /dev/null +++ b/src/confcom/azext_confcom/command/fragment_push.py @@ -0,0 +1,42 @@ + +import os +import subprocess +import tempfile +from typing import BinaryIO + + +def oras_push( + signed_fragment: BinaryIO, + manifest_tag: str, +) -> None: + subprocess.run( + [ + "oras", + "push", + "--artifact-type", "application/x-ms-ccepolicy-frag", + manifest_tag, + os.path.relpath(signed_fragment.name, start=os.getcwd()), + ], + check=True, + timeout=120, + ) + + +def fragment_push( + signed_fragment: BinaryIO, + manifest_tag: str, +) -> None: + + if signed_fragment.name == "": + with tempfile.NamedTemporaryFile(delete=True) as temp_signed_fragment: + temp_signed_fragment.write(signed_fragment.read()) + temp_signed_fragment.flush() + oras_push( + signed_fragment=temp_signed_fragment, + manifest_tag=manifest_tag, + ) + else: + oras_push( + signed_fragment=signed_fragment, + manifest_tag=manifest_tag, + ) diff --git a/src/confcom/azext_confcom/commands.py b/src/confcom/azext_confcom/commands.py index 1d2bb45f724..7e1e93eabca 100644 --- a/src/confcom/azext_confcom/commands.py +++ b/src/confcom/azext_confcom/commands.py @@ -11,5 +11,9 @@ def load_command_table(self, _): g.custom_command("acifragmentgen", "acifragmentgen_confcom") g.custom_command("katapolicygen", "katapolicygen_confcom") + with self.command_group("confcom fragment") as g: + g.custom_command("attach", "fragment_attach", is_preview=True) + g.custom_command("push", "fragment_push", is_preview=True) + with self.command_group("confcom"): pass diff --git a/src/confcom/azext_confcom/custom.py b/src/confcom/azext_confcom/custom.py index baf5abea035..5cb67a0e29e 100644 --- a/src/confcom/azext_confcom/custom.py +++ b/src/confcom/azext_confcom/custom.py @@ -5,7 +5,7 @@ import os import sys -from typing import Optional +from typing import BinaryIO from azext_confcom import oras_proxy, os_util, security_policy from azext_confcom.config import ( @@ -21,6 +21,8 @@ get_image_name, inject_policy_into_template, inject_policy_into_yaml, pretty_print_func, print_existing_policy_from_arm_template, print_existing_policy_from_yaml, print_func, str_to_sha256) +from azext_confcom.command.fragment_attach import fragment_attach as _fragment_attach +from azext_confcom.command.fragment_push import fragment_push as _fragment_push from knack.log import get_logger from pkg_resources import parse_version @@ -233,10 +235,9 @@ def acifragmentgen_confcom( output_filename: str = "", outraw: bool = False, upload_fragment: bool = False, - push_fragment_to: Optional[str] = None, - attach_fragment_to: Optional[str] = None, no_print: bool = False, fragments_json: str = "", + out_signed_fragment: bool = False, ): output_type = get_fragment_output_type(outraw) @@ -324,12 +325,16 @@ def acifragmentgen_confcom( fragment_text = policy.generate_fragment(namespace, svn, output_type, omit_id=omit_id) - if output_type != security_policy.OutputType.DEFAULT and not no_print: + if output_type != security_policy.OutputType.DEFAULT and not no_print and not out_signed_fragment: print(fragment_text) # take ".rego" off the end of the filename if it's there, it'll get added back later output_filename = output_filename.replace(".rego", "") filename = f"{output_filename or namespace}.rego" + + if out_signed_fragment: + filename = os.path.join("/tmp", filename) + os_util.write_str_to_file(filename, fragment_text) if key: @@ -337,6 +342,9 @@ def acifragmentgen_confcom( iss = cose_proxy.create_issuer(chain) out_path = filename + ".cose" + if out_signed_fragment: + out_path = os.path.join("/tmp", os.path.basename(out_path)) + cose_proxy.cose_sign(filename, key, chain, feed, iss, algo, out_path) # Preserve default behaviour established since version 1.1.0 of attaching @@ -348,11 +356,8 @@ def acifragmentgen_confcom( filename=out_path, ) - if push_fragment_to: - oras_proxy.push_fragment_to_registry(push_fragment_to, out_path) - - if attach_fragment_to: - oras_proxy.attach_fragment_to_image(attach_fragment_to, out_path) + if out_signed_fragment: + sys.stdout.buffer.write(open(out_path, "rb").read()) def katapolicygen_confcom( @@ -486,3 +491,23 @@ def get_fragment_output_type(outraw): if outraw: output_type = security_policy.OutputType.RAW return output_type + + +def fragment_attach( + signed_fragment: BinaryIO, + manifest_tag: str, +) -> None: + _fragment_attach( + signed_fragment=signed_fragment, + manifest_tag=manifest_tag + ) + + +def fragment_push( + signed_fragment: BinaryIO, + manifest_tag: str, +) -> None: + _fragment_push( + signed_fragment=signed_fragment, + manifest_tag=manifest_tag + ) diff --git a/src/confcom/azext_confcom/tests/latest/test_confcom_acifragmentgen.py b/src/confcom/azext_confcom/tests/latest/test_confcom_acifragmentgen.py index c9f3028ab37..4f1c09fdf65 100644 --- a/src/confcom/azext_confcom/tests/latest/test_confcom_acifragmentgen.py +++ b/src/confcom/azext_confcom/tests/latest/test_confcom_acifragmentgen.py @@ -3,13 +3,15 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- +import contextlib +import io import json import os import subprocess import tempfile import pytest -from azext_confcom.custom import acifragmentgen_confcom +from azext_confcom.custom import acifragmentgen_confcom, fragment_push, fragment_attach TEST_DIR = os.path.abspath(os.path.join(os.path.abspath(__file__), "..")) SAMPLES_DIR = os.path.abspath(os.path.join(TEST_DIR, "..", "..", "..", "samples")) @@ -84,6 +86,7 @@ def test_acifragmentgen_fragment_gen(docker_image): feed="test-feed", outraw=True, output_filename=os.path.join(temp_dir, "fragment.rego"), + out_signed_fragment=False, ) # TODO: Implement a proper validation for the fragment, this is hard @@ -108,6 +111,7 @@ def test_acifragmentgen_fragment_sign(docker_image, cert_chain): feed="test-feed", outraw=True, output_filename=os.path.join(temp_dir, "fragment.rego"), + out_signed_fragment=False, ) # TODO: Implement a proper validation for the cose document @@ -131,75 +135,104 @@ def test_acifragmentgen_fragment_upload_fragment(docker_image, cert_chain): outraw=True, upload_fragment=True, output_filename=os.path.relpath(os.path.join(temp_dir, "fragment.rego"), os.getcwd()), # Must be relative for oras + out_signed_fragment=False, ) - oras_referrers = subprocess.run( - ["oras", "discover", image_ref], - stdout=subprocess.PIPE, - text=True, - check=True - ).stdout + # Confirm the fragment exists and is attached in the registry + fragment_ref = json.loads(subprocess.run( + ["oras", "discover", image_ref, "--format", "json"], + stdout=subprocess.PIPE, + check=True, + ).stdout)["referrers"][0]["reference"] + + fragment_path = json.loads(subprocess.run( + ["oras", "pull", fragment_ref, "--format", "json", "-o", "/tmp"], + check=True, + stdout=subprocess.PIPE, + ).stdout)["files"][0]["path"] + - # Confirm the fragment is attached to the image - assert "application/x-ms-ccepolicy-frag" in oras_referrers + with open(fragment_path, "rb") as actual_fragment_file: + with open(os.path.join(temp_dir, "fragment.rego.cose"), "rb") as expected_fragment_file: + assert actual_fragment_file.read() == expected_fragment_file.read() -def test_acifragmentgen_fragment_push(docker_image, cert_chain): +def test_acifragmentgen_fragment_push(docker_image, cert_chain, capsysbinary): image_ref, spec_file_path = docker_image fragment_ref = image_ref.replace("hello-world", "fragment") - with tempfile.TemporaryDirectory() as temp_dir: # Prevent test writing files to repo - acifragmentgen_confcom( - image_name=None, - tar_mapping_location=None, - key=os.path.join(cert_chain, "intermediateCA", "private", "ec_p384_private.pem"), - chain=os.path.join(cert_chain, "intermediateCA", "certs", "www.contoso.com.chain.cert.pem"), - minimum_svn=None, - input_path=spec_file_path, - svn="1", - namespace="contoso", - feed="test-feed", - outraw=True, - push_fragment_to=fragment_ref, - output_filename=os.path.relpath(os.path.join(temp_dir, "fragment.rego"), os.getcwd()), # Must be relative for oras - ) + acifragmentgen_confcom( + image_name=None, + tar_mapping_location=None, + key=os.path.join(cert_chain, "intermediateCA", "private", "ec_p384_private.pem"), + chain=os.path.join(cert_chain, "intermediateCA", "certs", "www.contoso.com.chain.cert.pem"), + minimum_svn=None, + input_path=spec_file_path, + svn="1", + namespace="contoso", + feed="test-feed", + out_signed_fragment=True, + ) + + signed_fragment = capsysbinary.readouterr()[0] + signed_fragment_io = io.BytesIO(signed_fragment) + signed_fragment_io.name = "" + + fragment_push( + signed_fragment=signed_fragment_io, + manifest_tag=fragment_ref, + ) # Confirm the fragment exists in the registry - subprocess.run( - ["oras", "discover", fragment_ref], - stdout=subprocess.PIPE, - text=True, + fragment_path = json.loads(subprocess.run( + ["oras", "pull", fragment_ref, "--format", "json", "-o", "/tmp"], check=True, - ).stdout + stdout=subprocess.PIPE, + ).stdout)["files"][0]["path"] + + with open(fragment_path, "rb") as f: + assert f.read() == signed_fragment -def test_acifragmentgen_fragment_attach(docker_image, cert_chain): +def test_acifragmentgen_fragment_attach(docker_image, cert_chain, capsysbinary): image_ref, spec_file_path = docker_image - with tempfile.TemporaryDirectory() as temp_dir: # Prevent test writing files to repo - acifragmentgen_confcom( - image_name=None, - tar_mapping_location=None, - key=os.path.join(cert_chain, "intermediateCA", "private", "ec_p384_private.pem"), - chain=os.path.join(cert_chain, "intermediateCA", "certs", "www.contoso.com.chain.cert.pem"), - minimum_svn=None, - input_path=spec_file_path, - svn="1", - namespace="contoso", - feed="test-feed", - outraw=True, - attach_fragment_to=image_ref, - output_filename=os.path.relpath(os.path.join(temp_dir, "fragment.rego"), os.getcwd()), # Must be relative for oras - ) + acifragmentgen_confcom( + image_name=None, + tar_mapping_location=None, + key=os.path.join(cert_chain, "intermediateCA", "private", "ec_p384_private.pem"), + chain=os.path.join(cert_chain, "intermediateCA", "certs", "www.contoso.com.chain.cert.pem"), + minimum_svn=None, + input_path=spec_file_path, + svn="1", + namespace="contoso", + feed="test-feed", + out_signed_fragment=True, + ) + + signed_fragment = capsysbinary.readouterr()[0] + signed_fragment_io = io.BytesIO(signed_fragment) + signed_fragment_io.name = "" + + fragment_attach( + signed_fragment=signed_fragment_io, + manifest_tag=image_ref, + ) + + # Confirm the fragment exists and is attached in the registry + fragment_ref = json.loads(subprocess.run( + ["oras", "discover", image_ref, "--format", "json"], + stdout=subprocess.PIPE, + check=True, + ).stdout)["referrers"][0]["reference"] - oras_referrers = subprocess.run( - ["oras", "discover", image_ref], + fragment_path = json.loads(subprocess.run( + ["oras", "pull", fragment_ref, "--format", "json", "-o", "/tmp"], + check=True, stdout=subprocess.PIPE, - text=True, - check=True - ).stdout + ).stdout)["files"][0]["path"] - # Confirm the fragment is attached to the image - assert "application/x-ms-ccepolicy-frag" in oras_referrers, oras_referrers + with open(fragment_path, "rb") as f: + assert f.read() == signed_fragment diff --git a/src/confcom/azext_confcom/tests/latest/test_confcom_fragment.py b/src/confcom/azext_confcom/tests/latest/test_confcom_fragment.py index 2725ede31c0..3addfcee66e 100644 --- a/src/confcom/azext_confcom/tests/latest/test_confcom_fragment.py +++ b/src/confcom/azext_confcom/tests/latest/test_confcom_fragment.py @@ -493,7 +493,7 @@ def test_tar_file_fragment(self): try: with tempfile.TemporaryDirectory() as folder: filename = os.path.join(folder, "oci.tar") - filename2 = os.path.join(self.path, "oci2.tar") + filename2 = os.path.join(folder, "oci2.tar") tar_mapping_file = {"mcr.microsoft.com/aks/e2e/library-busybox:master.220314.1-linux-amd64": filename2} create_tar_file(filename) @@ -762,14 +762,14 @@ class FragmentPolicySigning(unittest.TestCase): """ @classmethod def setUpClass(cls): - cls.key_dir_parent = os.path.join(SAMPLES_DIR, 'certs') + cls.key_dir_parent = "/tmp/certs" cls.key = os.path.join(cls.key_dir_parent, 'intermediateCA', 'private', 'ec_p384_private.pem') cls.chain = os.path.join(cls.key_dir_parent, 'intermediateCA', 'certs', 'www.contoso.com.chain.cert.pem') if not os.path.exists(cls.key) or not os.path.exists(cls.chain): script_path = os.path.join(cls.key_dir_parent, 'create_certchain.sh') arg_list = [ - script_path, + script_path, cls.key_dir_parent ] os.chmod(script_path, 0o755) From b675b434970e87a1b12d36c1049379c0f3a62012 Mon Sep 17 00:00:00 2001 From: Dominic Ayre Date: Thu, 25 Sep 2025 16:12:48 +0000 Subject: [PATCH 09/18] Undo changes --- .../azext_confcom/tests/latest/test_confcom_fragment.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/confcom/azext_confcom/tests/latest/test_confcom_fragment.py b/src/confcom/azext_confcom/tests/latest/test_confcom_fragment.py index 3addfcee66e..24402abf7f2 100644 --- a/src/confcom/azext_confcom/tests/latest/test_confcom_fragment.py +++ b/src/confcom/azext_confcom/tests/latest/test_confcom_fragment.py @@ -762,14 +762,14 @@ class FragmentPolicySigning(unittest.TestCase): """ @classmethod def setUpClass(cls): - cls.key_dir_parent = "/tmp/certs" + cls.key_dir_parent = os.path.join(SAMPLES_DIR, 'certs') cls.key = os.path.join(cls.key_dir_parent, 'intermediateCA', 'private', 'ec_p384_private.pem') cls.chain = os.path.join(cls.key_dir_parent, 'intermediateCA', 'certs', 'www.contoso.com.chain.cert.pem') if not os.path.exists(cls.key) or not os.path.exists(cls.chain): script_path = os.path.join(cls.key_dir_parent, 'create_certchain.sh') arg_list = [ - script_path, cls.key_dir_parent + script_path, ] os.chmod(script_path, 0o755) From 472c6cf6db614ff7224b212f5faae4a337b7c0b3 Mon Sep 17 00:00:00 2001 From: Dominic Ayre Date: Thu, 25 Sep 2025 16:16:00 +0000 Subject: [PATCH 10/18] Print some debug info --- .../tests/latest/test_confcom_acifragmentgen.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/confcom/azext_confcom/tests/latest/test_confcom_acifragmentgen.py b/src/confcom/azext_confcom/tests/latest/test_confcom_acifragmentgen.py index 4f1c09fdf65..ddb2107029b 100644 --- a/src/confcom/azext_confcom/tests/latest/test_confcom_acifragmentgen.py +++ b/src/confcom/azext_confcom/tests/latest/test_confcom_acifragmentgen.py @@ -139,11 +139,13 @@ def test_acifragmentgen_fragment_upload_fragment(docker_image, cert_chain): ) # Confirm the fragment exists and is attached in the registry - fragment_ref = json.loads(subprocess.run( + oras_result = subprocess.run( ["oras", "discover", image_ref, "--format", "json"], stdout=subprocess.PIPE, check=True, - ).stdout)["referrers"][0]["reference"] + ).stdout + print(f"{oras_result.decode('utf-8')=}") + fragment_ref = json.loads(oras_result)["referrers"][0]["reference"] fragment_path = json.loads(subprocess.run( ["oras", "pull", fragment_ref, "--format", "json", "-o", "/tmp"], From 60f33fa962bbd65c0f7f9718c0bc6f2deab7be00 Mon Sep 17 00:00:00 2001 From: Dominic Ayre Date: Thu, 25 Sep 2025 16:16:55 +0000 Subject: [PATCH 11/18] Add missing licenses --- src/confcom/azext_confcom/command/fragment_attach.py | 4 ++++ src/confcom/azext_confcom/command/fragment_push.py | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/src/confcom/azext_confcom/command/fragment_attach.py b/src/confcom/azext_confcom/command/fragment_attach.py index 928dbc345e3..39f29ae48da 100644 --- a/src/confcom/azext_confcom/command/fragment_attach.py +++ b/src/confcom/azext_confcom/command/fragment_attach.py @@ -1,3 +1,7 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- import os import subprocess diff --git a/src/confcom/azext_confcom/command/fragment_push.py b/src/confcom/azext_confcom/command/fragment_push.py index 1f90b57fab5..89912c87637 100644 --- a/src/confcom/azext_confcom/command/fragment_push.py +++ b/src/confcom/azext_confcom/command/fragment_push.py @@ -1,3 +1,7 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- import os import subprocess From 312b74aff07aef81927dcde1708d0a1381d77374 Mon Sep 17 00:00:00 2001 From: Dominic Ayre Date: Thu, 25 Sep 2025 17:39:05 +0000 Subject: [PATCH 12/18] Handle case with attached fragments --- .../latest/test_confcom_acifragmentgen.py | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/confcom/azext_confcom/tests/latest/test_confcom_acifragmentgen.py b/src/confcom/azext_confcom/tests/latest/test_confcom_acifragmentgen.py index ddb2107029b..d966f94e497 100644 --- a/src/confcom/azext_confcom/tests/latest/test_confcom_acifragmentgen.py +++ b/src/confcom/azext_confcom/tests/latest/test_confcom_acifragmentgen.py @@ -139,13 +139,16 @@ def test_acifragmentgen_fragment_upload_fragment(docker_image, cert_chain): ) # Confirm the fragment exists and is attached in the registry - oras_result = subprocess.run( + oras_result = json.loads(subprocess.run( ["oras", "discover", image_ref, "--format", "json"], stdout=subprocess.PIPE, check=True, - ).stdout - print(f"{oras_result.decode('utf-8')=}") - fragment_ref = json.loads(oras_result)["referrers"][0]["reference"] + ).stdout) + + if oras_result.get("artifact_type") == "application/x-ms-ccepolicy-frag": + fragment_ref = oras_result["reference"] + else: + fragment_ref = oras_result["referrers"][0]["reference"] fragment_path = json.loads(subprocess.run( ["oras", "pull", fragment_ref, "--format", "json", "-o", "/tmp"], @@ -224,11 +227,16 @@ def test_acifragmentgen_fragment_attach(docker_image, cert_chain, capsysbinary): ) # Confirm the fragment exists and is attached in the registry - fragment_ref = json.loads(subprocess.run( + oras_result = json.loads(subprocess.run( ["oras", "discover", image_ref, "--format", "json"], stdout=subprocess.PIPE, check=True, - ).stdout)["referrers"][0]["reference"] + ).stdout) + + if oras_result.get("artifact_type") == "application/x-ms-ccepolicy-frag": + fragment_ref = oras_result["reference"] + else: + fragment_ref = oras_result["referrers"][0]["reference"] fragment_path = json.loads(subprocess.run( ["oras", "pull", fragment_ref, "--format", "json", "-o", "/tmp"], From 233391a2c8e9ea40829b0e9632796ec035e976a2 Mon Sep 17 00:00:00 2001 From: Dominic Ayre Date: Thu, 25 Sep 2025 18:36:29 +0000 Subject: [PATCH 13/18] Add fallback debug info --- .../tests/latest/test_confcom_acifragmentgen.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/confcom/azext_confcom/tests/latest/test_confcom_acifragmentgen.py b/src/confcom/azext_confcom/tests/latest/test_confcom_acifragmentgen.py index d966f94e497..59242701a89 100644 --- a/src/confcom/azext_confcom/tests/latest/test_confcom_acifragmentgen.py +++ b/src/confcom/azext_confcom/tests/latest/test_confcom_acifragmentgen.py @@ -147,8 +147,10 @@ def test_acifragmentgen_fragment_upload_fragment(docker_image, cert_chain): if oras_result.get("artifact_type") == "application/x-ms-ccepolicy-frag": fragment_ref = oras_result["reference"] - else: + elif "referrers" in oras_result: fragment_ref = oras_result["referrers"][0]["reference"] + else: + raise AssertionError(f"{oras_result=}") fragment_path = json.loads(subprocess.run( ["oras", "pull", fragment_ref, "--format", "json", "-o", "/tmp"], @@ -235,8 +237,10 @@ def test_acifragmentgen_fragment_attach(docker_image, cert_chain, capsysbinary): if oras_result.get("artifact_type") == "application/x-ms-ccepolicy-frag": fragment_ref = oras_result["reference"] - else: + elif "referrers" in oras_result: fragment_ref = oras_result["referrers"][0]["reference"] + else: + raise AssertionError(f"{oras_result=}") fragment_path = json.loads(subprocess.run( ["oras", "pull", fragment_ref, "--format", "json", "-o", "/tmp"], From d608b7079326a04af28b3f533061bc2b2debcff7 Mon Sep 17 00:00:00 2001 From: Dominic Ayre Date: Thu, 25 Sep 2025 19:55:26 +0000 Subject: [PATCH 14/18] Fix check --- .../tests/latest/test_confcom_acifragmentgen.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/confcom/azext_confcom/tests/latest/test_confcom_acifragmentgen.py b/src/confcom/azext_confcom/tests/latest/test_confcom_acifragmentgen.py index 59242701a89..97e3901e454 100644 --- a/src/confcom/azext_confcom/tests/latest/test_confcom_acifragmentgen.py +++ b/src/confcom/azext_confcom/tests/latest/test_confcom_acifragmentgen.py @@ -145,10 +145,10 @@ def test_acifragmentgen_fragment_upload_fragment(docker_image, cert_chain): check=True, ).stdout) - if oras_result.get("artifact_type") == "application/x-ms-ccepolicy-frag": - fragment_ref = oras_result["reference"] - elif "referrers" in oras_result: + if "referrers" in oras_result: fragment_ref = oras_result["referrers"][0]["reference"] + elif oras_result.get("manifests")[0].get("artifact_type") == "application/x-ms-ccepolicy-frag": + fragment_ref = oras_result["reference"] else: raise AssertionError(f"{oras_result=}") @@ -235,10 +235,10 @@ def test_acifragmentgen_fragment_attach(docker_image, cert_chain, capsysbinary): check=True, ).stdout) - if oras_result.get("artifact_type") == "application/x-ms-ccepolicy-frag": - fragment_ref = oras_result["reference"] - elif "referrers" in oras_result: + if "referrers" in oras_result: fragment_ref = oras_result["referrers"][0]["reference"] + elif oras_result["manifests"][0].get("artifact_type") == "application/x-ms-ccepolicy-frag": + fragment_ref = oras_result["reference"] else: raise AssertionError(f"{oras_result=}") From b939721bf89d30217474564fcd3125b06997c255 Mon Sep 17 00:00:00 2001 From: Dominic Ayre Date: Thu, 25 Sep 2025 20:29:38 +0000 Subject: [PATCH 15/18] Fix typo --- .../azext_confcom/tests/latest/test_confcom_acifragmentgen.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/confcom/azext_confcom/tests/latest/test_confcom_acifragmentgen.py b/src/confcom/azext_confcom/tests/latest/test_confcom_acifragmentgen.py index 97e3901e454..12e56cc91b4 100644 --- a/src/confcom/azext_confcom/tests/latest/test_confcom_acifragmentgen.py +++ b/src/confcom/azext_confcom/tests/latest/test_confcom_acifragmentgen.py @@ -147,7 +147,7 @@ def test_acifragmentgen_fragment_upload_fragment(docker_image, cert_chain): if "referrers" in oras_result: fragment_ref = oras_result["referrers"][0]["reference"] - elif oras_result.get("manifests")[0].get("artifact_type") == "application/x-ms-ccepolicy-frag": + elif oras_result.get("manifests")[0].get("artifactType") == "application/x-ms-ccepolicy-frag": fragment_ref = oras_result["reference"] else: raise AssertionError(f"{oras_result=}") @@ -237,7 +237,7 @@ def test_acifragmentgen_fragment_attach(docker_image, cert_chain, capsysbinary): if "referrers" in oras_result: fragment_ref = oras_result["referrers"][0]["reference"] - elif oras_result["manifests"][0].get("artifact_type") == "application/x-ms-ccepolicy-frag": + elif oras_result["manifests"][0].get("artifactType") == "application/x-ms-ccepolicy-frag": fragment_ref = oras_result["reference"] else: raise AssertionError(f"{oras_result=}") From 1adcde526728f46fbfc3f32baf86b4da371bea5f Mon Sep 17 00:00:00 2001 From: Dominic Ayre Date: Thu, 25 Sep 2025 20:50:23 +0000 Subject: [PATCH 16/18] Fix another typo --- .../azext_confcom/tests/latest/test_confcom_acifragmentgen.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/confcom/azext_confcom/tests/latest/test_confcom_acifragmentgen.py b/src/confcom/azext_confcom/tests/latest/test_confcom_acifragmentgen.py index 12e56cc91b4..5426e8ec707 100644 --- a/src/confcom/azext_confcom/tests/latest/test_confcom_acifragmentgen.py +++ b/src/confcom/azext_confcom/tests/latest/test_confcom_acifragmentgen.py @@ -148,7 +148,7 @@ def test_acifragmentgen_fragment_upload_fragment(docker_image, cert_chain): if "referrers" in oras_result: fragment_ref = oras_result["referrers"][0]["reference"] elif oras_result.get("manifests")[0].get("artifactType") == "application/x-ms-ccepolicy-frag": - fragment_ref = oras_result["reference"] + fragment_ref = oras_result["manifests"][0]["reference"] else: raise AssertionError(f"{oras_result=}") @@ -238,7 +238,7 @@ def test_acifragmentgen_fragment_attach(docker_image, cert_chain, capsysbinary): if "referrers" in oras_result: fragment_ref = oras_result["referrers"][0]["reference"] elif oras_result["manifests"][0].get("artifactType") == "application/x-ms-ccepolicy-frag": - fragment_ref = oras_result["reference"] + fragment_ref = oras_result["manifests"][0]["reference"] else: raise AssertionError(f"{oras_result=}") From 6d3c86fd3be6163e09ebf7b767660a78919e05fd Mon Sep 17 00:00:00 2001 From: Dominic Ayre Date: Fri, 26 Sep 2025 07:55:11 +0000 Subject: [PATCH 17/18] Satisfy azdev linter --- linter_exclusions.yml | 12 +++++++++ src/confcom/azext_confcom/_help.py | 43 ++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/linter_exclusions.yml b/linter_exclusions.yml index 81937685407..d52d538627f 100644 --- a/linter_exclusions.yml +++ b/linter_exclusions.yml @@ -3442,3 +3442,15 @@ neon postgres organization: neon postgres project: rule_exclusions: - require_wait_command_if_no_wait + +confcom fragment push: + parameters: + signed_fragment: + rule_exclusions: + - no_positional_parameters + +confcom fragment attach: + parameters: + signed_fragment: + rule_exclusions: + - no_positional_parameters diff --git a/src/confcom/azext_confcom/_help.py b/src/confcom/azext_confcom/_help.py index 15368cc61db..5106d083a64 100644 --- a/src/confcom/azext_confcom/_help.py +++ b/src/confcom/azext_confcom/_help.py @@ -278,3 +278,46 @@ - name: Input a Kubernetes YAML file with a custom containerd socket path text: az confcom katapolicygen --yaml "./pod.json" --containerd-pull --containerd-socket-path "/my/custom/containerd.sock" """ + +helps[ + "confcom fragment" +] = """ + type: group + short-summary: Commands to handle Confidential Container Policy Fragments. +""" + +helps[ + "confcom fragment push" +] = """ + type: command + short-summary: Push a Confidential Container Policy Fragment to an ORAS registry + + parameters: + - name: --manifest-tag + type: string + short-summary: 'The reference to push the signed fragment to' + + examples: + - name: Push a signed fragment to a registry + text: az confcom fragment push ./fragment.reg.cose --manifest-tag myregistry.azurecr.io/fragment:latest + - name: Push the output of acifragmentgen to a registry + text: az confcom acifragmentgen --chain my.cert.pem --key my_key.pem --svn "1" --namespace contoso --feed "test-feed" --input ./fragment_spec.json | az confcom fragment push --manifest-tag myregistry.azurecr.io/fragment:latest +""" + +helps[ + "confcom fragment attach" +] = """ + type: command + short-summary: Attach a Confidential Container Policy Fragment to an image in an ORAS registry. + + parameters: + - name: --manifest-tag + type: string + short-summary: 'The reference to attach the signed fragment to' + + examples: + - name: Attach a signed fragment to a registry + text: az confcom fragment attach ./fragment.reg.cose --manifest-tag myregistry.azurecr.io/image:latest + - name: Attach the output of acifragmentgen to a registry + text: az confcom acifragmentgen --chain my.cert.pem --key my_key.pem --svn "1" --namespace contoso --feed "test-feed" --input ./fragment_spec.json | az confcom fragment attach --manifest-tag myregistry.azurecr.io/image:latest +""" \ No newline at end of file From b6e057b1b78803e7ef4fb8bb61dc244c18ec8821 Mon Sep 17 00:00:00 2001 From: Dominic Ayre Date: Fri, 26 Sep 2025 07:59:50 +0000 Subject: [PATCH 18/18] Fix azdev style --- src/confcom/azext_confcom/_help.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/confcom/azext_confcom/_help.py b/src/confcom/azext_confcom/_help.py index 5106d083a64..9817bef723e 100644 --- a/src/confcom/azext_confcom/_help.py +++ b/src/confcom/azext_confcom/_help.py @@ -320,4 +320,4 @@ text: az confcom fragment attach ./fragment.reg.cose --manifest-tag myregistry.azurecr.io/image:latest - name: Attach the output of acifragmentgen to a registry text: az confcom acifragmentgen --chain my.cert.pem --key my_key.pem --svn "1" --namespace contoso --feed "test-feed" --input ./fragment_spec.json | az confcom fragment attach --manifest-tag myregistry.azurecr.io/image:latest -""" \ No newline at end of file +"""