Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions linter_exclusions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3504,3 +3504,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
6 changes: 6 additions & 0 deletions src/confcom/HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@
Release History
===============

1.5.0
++++++
* restored the behaviour of --upload-fragment in acifragmentgen to attach to first image in input
* added confcom fragment push command to allow explicit uploading of standalone fragments
* added confcom fragment attach command to allow explicit uploading of image attached fragments

1.4.5
++++++
* Drop the dependency on OPA
Expand Down
43 changes: 43 additions & 0 deletions src/confcom/azext_confcom/_help.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment on lines +304 to +322
Copy link

Copilot AI Dec 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing required flag in help examples. The examples on lines 304 and 322 show piping acifragmentgen output to fragment push/attach commands, but the acifragmentgen command is missing the --out-signed-fragment flag that causes it to output the signed fragment to stdout. Without this flag, acifragmentgen will write files to disk and print text output instead of binary fragment data. Add --out-signed-fragment to these example commands.

Copilot uses AI. Check for mistakes.
"""
35 changes: 35 additions & 0 deletions src/confcom/azext_confcom/_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
# pylint: disable=line-too-long

import json
import argparse
import sys
from knack.arguments import CLIArgumentType
from azext_confcom._validators import (
validate_params_file,
Expand Down Expand Up @@ -44,6 +46,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",
Expand Down Expand Up @@ -362,6 +390,13 @@ def load_arguments(self, _):
type=json.loads,
help='Container definitions to include in the policy'
)
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(
Expand Down
Empty file.
46 changes: 46 additions & 0 deletions src/confcom/azext_confcom/command/fragment_attach.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# --------------------------------------------------------------------------------------------
# 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
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()),
Copy link

Copilot AI Dec 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

File path handling issue with relative paths. Using os.path.relpath with start=os.getcwd() can produce incorrect results if the current working directory changes between when the file is created and when oras is invoked. Additionally, if signed_fragment.name is an absolute path, the relpath call may produce unexpected results. Consider either using the absolute path directly or ensuring the working directory is consistent.

Suggested change
os.path.relpath(signed_fragment.name, start=os.getcwd()),
os.path.abspath(signed_fragment.name),

Copilot uses AI. Check for mistakes.
],
check=True,
timeout=120,
)


def fragment_attach(
signed_fragment: BinaryIO,
manifest_tag: str,
) -> None:

if signed_fragment.name == "<stdin>":
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,
)
46 changes: 46 additions & 0 deletions src/confcom/azext_confcom/command/fragment_push.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# --------------------------------------------------------------------------------------------
# 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
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()),
Copy link

Copilot AI Dec 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

File path handling issue with relative paths. Using os.path.relpath with start=os.getcwd() can produce incorrect results if the current working directory changes between when the file is created and when oras is invoked. Additionally, if signed_fragment.name is an absolute path, the relpath call may produce unexpected results. Consider either using the absolute path directly or ensuring the working directory is consistent.

Suggested change
os.path.relpath(signed_fragment.name, start=os.getcwd()),
os.path.abspath(signed_fragment.name),

Copilot uses AI. Check for mistakes.
],
check=True,
timeout=120,
)


def fragment_push(
signed_fragment: BinaryIO,
manifest_tag: str,
) -> None:

if signed_fragment.name == "<stdin>":
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,
)
4 changes: 4 additions & 0 deletions src/confcom/azext_confcom/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
52 changes: 46 additions & 6 deletions src/confcom/azext_confcom/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@

import os
import sys
from typing import Optional
import tempfile
from typing import Optional, BinaryIO

from azext_confcom import oras_proxy, os_util, security_policy
from azext_confcom._validators import resolve_stdio
Expand All @@ -22,6 +23,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

Expand Down Expand Up @@ -255,6 +258,7 @@ def acifragmentgen_confcom(
upload_fragment: bool = False,
no_print: bool = False,
fragments_json: str = "",
out_signed_fragment: bool = False,
):
if container_definitions is None:
container_definitions = []
Expand Down Expand Up @@ -361,24 +365,40 @@ 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(tempfile.gettempdir(), filename)

os_util.write_str_to_file(filename, fragment_text)

if key:
cose_proxy = CoseSignToolProxy()
iss = cose_proxy.create_issuer(chain)
out_path = filename + ".cose"

if out_signed_fragment:
out_path = os.path.join(tempfile.gettempdir(), os.path.basename(out_path))

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 out_signed_fragment:
with open(out_path, "rb") as f:
sys.stdout.buffer.write(f.read())


def katapolicygen_confcom(
Expand Down Expand Up @@ -512,3 +532,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
)
Loading
Loading