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
7 changes: 7 additions & 0 deletions src/confcom/HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@
Release History
===============

1.2.7
++++++
* bugfix making it so that oras discover function doesn't error when no fragments are found in the remote repository
* splitting out documentation into command-specific files and adding info about --input flag
* adding standalone fragment support
* bugfix for oras pulling fragments when offline

1.2.6
++++++
* bugfix making it so the fields in the --input format are case-insensitive
Expand Down
12 changes: 12 additions & 0 deletions src/confcom/azext_confcom/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -866,6 +866,18 @@ Using the same command, the default mounts and environment variables used by VN2
az confcom acifragmentgen --input ./fragment_config.json --svn 1 --namespace contoso
```

Example 6: Create an import statement from a signed fragment in a remote repo:

```bash
az confcom acifragmentgen --generate-import --fragment-path contoso.azurecr.io/<my-fragment>:v1 --minimum-svn 1
```

This is assuming there is a standalone fragment present at the specified location of `contoso.azurecr.io/<my-fragment>:v1`. Fragment imports can also be created using local paths to signed fragment files such as:

```bash
az confcom acifragmentgen --generate-import --fragment-path ./contoso.rego.cose --minimum-svn 1
```

## Microsoft Azure CLI 'confcom katapolicygen' Extension Examples

Run `az confcom katapolicygen --help` to see a list of supported arguments along with explanations. The following commands demonstrate the usage of different arguments to generate confidential computing security policies.
Expand Down
2 changes: 1 addition & 1 deletion src/confcom/azext_confcom/_help.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@

- name: --fragment-path -p
type: string
short-summary: 'Path to an existing policy fragment file to be used with --generate-import. This option allows you to create import statements for the specified fragment without needing to pull it from an OCI registry'
short-summary: 'Path to an existing signed policy fragment file to be used with --generate-import. This option allows you to create import statements for the specified fragment without needing to explicitly pull it from an OCI registry. This can either be a local path or an OCI registry reference. For local fragments, the file will remain in the same location. For remote fragments, the file will be downloaded and cleaned up after processing'

- name: --omit-id
type: boolean
Expand Down
3 changes: 1 addition & 2 deletions src/confcom/azext_confcom/_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,6 @@ def load_arguments(self, _):
required=False,
help="Omit the id field in the policy. This is helpful if the image being used will be present in multiple registries and used interchangeably.",
)

c.argument(
"include_fragments",
options_list=("--include-fragments", "-f"),
Expand Down Expand Up @@ -266,7 +265,7 @@ def load_arguments(self, _):
"fragment_path",
options_list=("--fragment-path", "-p"),
required=False,
help="Path to a policy fragment to be used with --generate-import to make import statements without having access to the fragment's OCI registry",
help="Path to a signed policy fragment to be used with --generate-import to make import statements without having access to the fragment's OCI registry. This can either be a local path or a registry address.",
validator=validate_fragment_path,
)
c.argument(
Expand Down
4 changes: 3 additions & 1 deletion src/confcom/azext_confcom/_validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ def validate_image_target(namespace):
def validate_upload_fragment(namespace):
if namespace.upload_fragment and not (namespace.key or namespace.chain):
raise CLIError("Must sign the fragment with --key and --chain to upload it")
if namespace.upload_fragment and not (namespace.image_target or namespace.feed):
raise CLIError("Must either specify an --image-target or --feed to upload a fragment")


def validate_fragment_generate_import(namespace):
Expand All @@ -88,7 +90,7 @@ def validate_fragment_generate_import(namespace):
])) != 1:
raise CLIError(
(
"Must provide either a fragment path, an input file, or "
"Must provide either a fragment path or "
"an image name to generate an import statement"
)
)
Expand Down
2 changes: 2 additions & 0 deletions src/confcom/azext_confcom/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
ACI_FIELD_TEMPLATE_MOUNTS_READONLY = "readOnly"
ACI_FIELD_TEMPLATE_CONFCOM_PROPERTIES = "confidentialComputeProperties"
ACI_FIELD_TEMPLATE_CCE_POLICY = "ccePolicy"
ACI_FIELD_TEMPLATE_STANDALONE_REGO_FRAGMENTS = "standaloneFragments"
ACI_FIELD_CONTAINERS_PRIVILEGED = "privileged"
ACI_FIELD_CONTAINERS_CAPABILITIES = "capabilities"
ACI_FIELD_CONTAINERS_CAPABILITIES_ADD = "add"
Expand Down Expand Up @@ -169,6 +170,7 @@
ACI = "aci"
KATA = "kata"

REGO_SVN_START = "svn := "

CONFIG_FILE = "./data/internal_config.json"

Expand Down
29 changes: 18 additions & 11 deletions src/confcom/azext_confcom/cose_proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,22 @@
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

import subprocess
import os
import stat
import platform
import stat
import subprocess
from typing import List

import requests
from knack.log import get_logger
from azext_confcom.errors import eprint
from azext_confcom.config import (
REGO_CONTAINER_START,
REGO_FRAGMENT_START,
POLICY_FIELD_CONTAINERS,
ACI_FIELD_CONTAINERS_REGO_FRAGMENTS_INCLUDES, POLICY_FIELD_CONTAINERS,
POLICY_FIELD_CONTAINERS_ELEMENTS_REGO_FRAGMENTS,
POLICY_FIELD_CONTAINERS_ELEMENTS_REGO_FRAGMENTS_ISSUER,
POLICY_FIELD_CONTAINERS_ELEMENTS_REGO_FRAGMENTS_FEED,
POLICY_FIELD_CONTAINERS_ELEMENTS_REGO_FRAGMENTS_ISSUER,
POLICY_FIELD_CONTAINERS_ELEMENTS_REGO_FRAGMENTS_MINIMUM_SVN,
ACI_FIELD_CONTAINERS_REGO_FRAGMENTS_INCLUDES,
)
REGO_CONTAINER_START, REGO_FRAGMENT_START)
from azext_confcom.errors import eprint
from knack.log import get_logger

logger = get_logger(__name__)
host_os = platform.system()
Expand Down Expand Up @@ -57,6 +55,8 @@ def download_binaries():
needed_asset_info = [asset for asset in release["assets"] if asset["name"] in needed_assets]
if len(needed_asset_info) == len(needed_assets):
for asset in needed_asset_info:
# say which version we're downloading
print(f"Downloading integrity-vhd version {release['tag_name']}")
# get the download url for the dmverity-vhd file
exe_url = asset["browser_download_url"]
# download the file
Expand Down Expand Up @@ -154,6 +154,12 @@ def generate_import_from_path(self, fragment_path: str, minimum_svn: str) -> str
item = call_cose_sign_tool(arg_list_chain, "Error getting information from signed fragment file")

stdout = item.stdout.decode("utf-8")
# if we don't have a minimum svn, use the one from the fragment
fragment_svn = None
if minimum_svn == -1:
fragment_svn = stdout.split('svn := "')[1].split('"')[0]
if not fragment_svn:
eprint("Must have either a minimum SVN or fragment SVN defined")
# extract issuer, feed, and payload from the fragment
issuer = stdout.split("iss: ")[1].split("\n")[0]
feed = stdout.split("feed: ")[1].split("\n")[0]
Expand All @@ -170,7 +176,8 @@ def generate_import_from_path(self, fragment_path: str, minimum_svn: str) -> str
import_statement = {
POLICY_FIELD_CONTAINERS_ELEMENTS_REGO_FRAGMENTS_ISSUER: issuer,
POLICY_FIELD_CONTAINERS_ELEMENTS_REGO_FRAGMENTS_FEED: feed,
POLICY_FIELD_CONTAINERS_ELEMENTS_REGO_FRAGMENTS_MINIMUM_SVN: minimum_svn,
POLICY_FIELD_CONTAINERS_ELEMENTS_REGO_FRAGMENTS_MINIMUM_SVN:
minimum_svn if minimum_svn != -1 else fragment_svn,
ACI_FIELD_CONTAINERS_REGO_FRAGMENTS_INCLUDES: includes,
}

Expand Down
83 changes: 42 additions & 41 deletions src/confcom/azext_confcom/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,33 +6,22 @@
import os
import sys

from pkg_resources import parse_version
from knack.log import get_logger
from azext_confcom import oras_proxy, os_util, security_policy
from azext_confcom.config import (
DEFAULT_REGO_FRAGMENTS,
POLICY_FIELD_CONTAINERS_ELEMENTS_REGO_FRAGMENTS,
REGO_IMPORT_FILE_STRUCTURE,
)

from azext_confcom import os_util
from azext_confcom.template_util import (
pretty_print_func,
print_func,
str_to_sha256,
inject_policy_into_template,
inject_policy_into_yaml,
print_existing_policy_from_arm_template,
print_existing_policy_from_yaml,
get_image_name,
)
DEFAULT_REGO_FRAGMENTS, POLICY_FIELD_CONTAINERS_ELEMENTS_REGO_FRAGMENTS,
REGO_IMPORT_FILE_STRUCTURE)
from azext_confcom.cose_proxy import CoseSignToolProxy
from azext_confcom.errors import eprint
from azext_confcom.fragment_util import get_all_fragment_contents
from azext_confcom.init_checks import run_initial_docker_checks
from azext_confcom import security_policy
from azext_confcom.security_policy import OutputType
from azext_confcom.kata_proxy import KataPolicyGenProxy
from azext_confcom.cose_proxy import CoseSignToolProxy
from azext_confcom import oras_proxy

from azext_confcom.security_policy import OutputType
from azext_confcom.template_util import (
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 knack.log import get_logger
from pkg_resources import parse_version

logger = get_logger(__name__)

Expand Down Expand Up @@ -96,12 +85,16 @@ def acipolicygen_confcom(
fragments_list = []
# gather information about the fragments being used in the new policy
if include_fragments:
fragments_list = os_util.load_json_from_file(fragments_json or input_path)
if isinstance(fragments_list, dict):
fragments_list = fragments_list.get("fragments", [])

# convert to list if it's just a dict
if not isinstance(fragments_list, list):
fragments_data = os_util.load_json_from_file(fragments_json or input_path)
if isinstance(fragments_data, dict):
fragments_list = fragments_data.get("fragments", [])
# standalone fragments from external file
fragments_list.extend(fragments_data.get("standaloneFragments", []))

# convert to list if it's just a dict. if it's empty, make it an empty list
if not fragments_data:
fragments_list = []
elif not isinstance(fragments_list, list):
fragments_list = [fragments_list]

# telling the user what operation we're doing
Expand Down Expand Up @@ -249,7 +242,15 @@ def acifragmentgen_confcom(
import_statements = []
# images can have multiple fragments attached to them so we need an array to hold the import statements
if fragment_path:
# download and cleanup the fragment from registry if it's not local already
downloaded_fragment = False
if not os.path.exists(fragment_path):
fragment_path = oras_proxy.pull(fragment_path)
downloaded_fragment = True
import_statements = [cose_client.generate_import_from_path(fragment_path, minimum_svn=minimum_svn)]
if downloaded_fragment:
os_util.clean_up_temp_folder(fragment_path)

elif image_name:
import_statements = oras_proxy.generate_imports_from_image_name(image_name, minimum_svn=minimum_svn)

Expand All @@ -260,14 +261,15 @@ def acifragmentgen_confcom(
if os.path.isfile(fragments_json):
fragments_file_contents = os_util.load_json_from_file(fragments_json)
if isinstance(fragments_file_contents, list):
logger.error(
eprint(
"%s %s %s %s",
"Unsupported JSON file format. ",
"Please make sure the outermost structure is not an array. ",
"An empty import file should look like: ",
REGO_IMPORT_FILE_STRUCTURE
REGO_IMPORT_FILE_STRUCTURE,
exit_code=1
)
sys.exit(1)

fragments_list = fragments_file_contents.get(POLICY_FIELD_CONTAINERS_ELEMENTS_REGO_FRAGMENTS, [])

# convert to list if it's just a dict
Expand Down Expand Up @@ -308,14 +310,11 @@ def acifragmentgen_confcom(
individual_image=bool(image_name), tar_mapping=tar_mapping
)

# if no feed is provided, use the first image's feed
# to assume it's an image-attached fragment
if not image_target:
policy_images = policy.get_images()
if not policy_images:
logger.error("No images found in the policy or all images are covered by fragments")
sys.exit(1)
image_target = policy_images[0].containerImage
# make sure we have images to generate a fragment
policy_images = policy.get_images()
if not policy_images:
eprint("No images found in the policy or all images are covered by fragments")

if not feed:
# strip the tag or hash off the image name so there are stable feed names
feed = get_image_name(image_target)
Expand All @@ -336,8 +335,10 @@ def acifragmentgen_confcom(
out_path = filename + ".cose"

cose_proxy.cose_sign(filename, key, chain, feed, iss, algo, out_path)
if upload_fragment:
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)


def katapolicygen_confcom(
Expand Down
2 changes: 1 addition & 1 deletion src/confcom/azext_confcom/data/internal_config.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"version": "1.2.6",
"version": "1.2.7",
"hcsshim_config": {
"maxVersion": "1.0.0",
"minVersion": "0.0.1"
Expand Down
Loading
Loading