Skip to content

Commit bca2ea1

Browse files
[confcom] Adding standalone fragment support (#9097)
* ensure that oras discover doesn't error when the remote image doesn't exist * updating version * adding print for binary version * commenting out some tests due to docker incompatibility * pull image before saving to tar --------- Co-authored-by: Heather Garvison <[email protected]>
1 parent d7a207d commit bca2ea1

31 files changed

+3214
-346
lines changed

src/confcom/HISTORY.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,13 @@
33
Release History
44
===============
55

6+
1.2.7
7+
++++++
8+
* bugfix making it so that oras discover function doesn't error when no fragments are found in the remote repository
9+
* splitting out documentation into command-specific files and adding info about --input flag
10+
* adding standalone fragment support
11+
* bugfix for oras pulling fragments when offline
12+
613
1.2.6
714
++++++
815
* bugfix making it so the fields in the --input format are case-insensitive

src/confcom/azext_confcom/README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -866,6 +866,18 @@ Using the same command, the default mounts and environment variables used by VN2
866866
az confcom acifragmentgen --input ./fragment_config.json --svn 1 --namespace contoso
867867
```
868868

869+
Example 6: Create an import statement from a signed fragment in a remote repo:
870+
871+
```bash
872+
az confcom acifragmentgen --generate-import --fragment-path contoso.azurecr.io/<my-fragment>:v1 --minimum-svn 1
873+
```
874+
875+
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:
876+
877+
```bash
878+
az confcom acifragmentgen --generate-import --fragment-path ./contoso.rego.cose --minimum-svn 1
879+
```
880+
869881
## Microsoft Azure CLI 'confcom katapolicygen' Extension Examples
870882

871883
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.

src/confcom/azext_confcom/_help.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@
165165
166166
- name: --fragment-path -p
167167
type: string
168-
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'
168+
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'
169169
170170
- name: --omit-id
171171
type: boolean

src/confcom/azext_confcom/_params.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,6 @@ def load_arguments(self, _):
172172
required=False,
173173
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.",
174174
)
175-
176175
c.argument(
177176
"include_fragments",
178177
options_list=("--include-fragments", "-f"),
@@ -266,7 +265,7 @@ def load_arguments(self, _):
266265
"fragment_path",
267266
options_list=("--fragment-path", "-p"),
268267
required=False,
269-
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",
268+
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.",
270269
validator=validate_fragment_path,
271270
)
272271
c.argument(

src/confcom/azext_confcom/_validators.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ def validate_image_target(namespace):
7979
def validate_upload_fragment(namespace):
8080
if namespace.upload_fragment and not (namespace.key or namespace.chain):
8181
raise CLIError("Must sign the fragment with --key and --chain to upload it")
82+
if namespace.upload_fragment and not (namespace.image_target or namespace.feed):
83+
raise CLIError("Must either specify an --image-target or --feed to upload a fragment")
8284

8385

8486
def validate_fragment_generate_import(namespace):
@@ -88,7 +90,7 @@ def validate_fragment_generate_import(namespace):
8890
])) != 1:
8991
raise CLIError(
9092
(
91-
"Must provide either a fragment path, an input file, or "
93+
"Must provide either a fragment path or "
9294
"an image name to generate an import statement"
9395
)
9496
)

src/confcom/azext_confcom/config.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@
7272
ACI_FIELD_TEMPLATE_MOUNTS_READONLY = "readOnly"
7373
ACI_FIELD_TEMPLATE_CONFCOM_PROPERTIES = "confidentialComputeProperties"
7474
ACI_FIELD_TEMPLATE_CCE_POLICY = "ccePolicy"
75+
ACI_FIELD_TEMPLATE_STANDALONE_REGO_FRAGMENTS = "standaloneFragments"
7576
ACI_FIELD_CONTAINERS_PRIVILEGED = "privileged"
7677
ACI_FIELD_CONTAINERS_CAPABILITIES = "capabilities"
7778
ACI_FIELD_CONTAINERS_CAPABILITIES_ADD = "add"
@@ -169,6 +170,7 @@
169170
ACI = "aci"
170171
KATA = "kata"
171172

173+
REGO_SVN_START = "svn := "
172174

173175
CONFIG_FILE = "./data/internal_config.json"
174176

src/confcom/azext_confcom/cose_proxy.py

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,22 @@
33
# Licensed under the MIT License. See License.txt in the project root for license information.
44
# --------------------------------------------------------------------------------------------
55

6-
import subprocess
76
import os
8-
import stat
97
import platform
8+
import stat
9+
import subprocess
1010
from typing import List
11+
1112
import requests
12-
from knack.log import get_logger
13-
from azext_confcom.errors import eprint
1413
from azext_confcom.config import (
15-
REGO_CONTAINER_START,
16-
REGO_FRAGMENT_START,
17-
POLICY_FIELD_CONTAINERS,
14+
ACI_FIELD_CONTAINERS_REGO_FRAGMENTS_INCLUDES, POLICY_FIELD_CONTAINERS,
1815
POLICY_FIELD_CONTAINERS_ELEMENTS_REGO_FRAGMENTS,
19-
POLICY_FIELD_CONTAINERS_ELEMENTS_REGO_FRAGMENTS_ISSUER,
2016
POLICY_FIELD_CONTAINERS_ELEMENTS_REGO_FRAGMENTS_FEED,
17+
POLICY_FIELD_CONTAINERS_ELEMENTS_REGO_FRAGMENTS_ISSUER,
2118
POLICY_FIELD_CONTAINERS_ELEMENTS_REGO_FRAGMENTS_MINIMUM_SVN,
22-
ACI_FIELD_CONTAINERS_REGO_FRAGMENTS_INCLUDES,
23-
)
19+
REGO_CONTAINER_START, REGO_FRAGMENT_START)
20+
from azext_confcom.errors import eprint
21+
from knack.log import get_logger
2422

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

156156
stdout = item.stdout.decode("utf-8")
157+
# if we don't have a minimum svn, use the one from the fragment
158+
fragment_svn = None
159+
if minimum_svn == -1:
160+
fragment_svn = stdout.split('svn := "')[1].split('"')[0]
161+
if not fragment_svn:
162+
eprint("Must have either a minimum SVN or fragment SVN defined")
157163
# extract issuer, feed, and payload from the fragment
158164
issuer = stdout.split("iss: ")[1].split("\n")[0]
159165
feed = stdout.split("feed: ")[1].split("\n")[0]
@@ -170,7 +176,8 @@ def generate_import_from_path(self, fragment_path: str, minimum_svn: str) -> str
170176
import_statement = {
171177
POLICY_FIELD_CONTAINERS_ELEMENTS_REGO_FRAGMENTS_ISSUER: issuer,
172178
POLICY_FIELD_CONTAINERS_ELEMENTS_REGO_FRAGMENTS_FEED: feed,
173-
POLICY_FIELD_CONTAINERS_ELEMENTS_REGO_FRAGMENTS_MINIMUM_SVN: minimum_svn,
179+
POLICY_FIELD_CONTAINERS_ELEMENTS_REGO_FRAGMENTS_MINIMUM_SVN:
180+
minimum_svn if minimum_svn != -1 else fragment_svn,
174181
ACI_FIELD_CONTAINERS_REGO_FRAGMENTS_INCLUDES: includes,
175182
}
176183

src/confcom/azext_confcom/custom.py

Lines changed: 42 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -6,33 +6,22 @@
66
import os
77
import sys
88

9-
from pkg_resources import parse_version
10-
from knack.log import get_logger
9+
from azext_confcom import oras_proxy, os_util, security_policy
1110
from azext_confcom.config import (
12-
DEFAULT_REGO_FRAGMENTS,
13-
POLICY_FIELD_CONTAINERS_ELEMENTS_REGO_FRAGMENTS,
14-
REGO_IMPORT_FILE_STRUCTURE,
15-
)
16-
17-
from azext_confcom import os_util
18-
from azext_confcom.template_util import (
19-
pretty_print_func,
20-
print_func,
21-
str_to_sha256,
22-
inject_policy_into_template,
23-
inject_policy_into_yaml,
24-
print_existing_policy_from_arm_template,
25-
print_existing_policy_from_yaml,
26-
get_image_name,
27-
)
11+
DEFAULT_REGO_FRAGMENTS, POLICY_FIELD_CONTAINERS_ELEMENTS_REGO_FRAGMENTS,
12+
REGO_IMPORT_FILE_STRUCTURE)
13+
from azext_confcom.cose_proxy import CoseSignToolProxy
14+
from azext_confcom.errors import eprint
2815
from azext_confcom.fragment_util import get_all_fragment_contents
2916
from azext_confcom.init_checks import run_initial_docker_checks
30-
from azext_confcom import security_policy
31-
from azext_confcom.security_policy import OutputType
3217
from azext_confcom.kata_proxy import KataPolicyGenProxy
33-
from azext_confcom.cose_proxy import CoseSignToolProxy
34-
from azext_confcom import oras_proxy
35-
18+
from azext_confcom.security_policy import OutputType
19+
from azext_confcom.template_util import (
20+
get_image_name, inject_policy_into_template, inject_policy_into_yaml,
21+
pretty_print_func, print_existing_policy_from_arm_template,
22+
print_existing_policy_from_yaml, print_func, str_to_sha256)
23+
from knack.log import get_logger
24+
from pkg_resources import parse_version
3625

3726
logger = get_logger(__name__)
3827

@@ -96,12 +85,16 @@ def acipolicygen_confcom(
9685
fragments_list = []
9786
# gather information about the fragments being used in the new policy
9887
if include_fragments:
99-
fragments_list = os_util.load_json_from_file(fragments_json or input_path)
100-
if isinstance(fragments_list, dict):
101-
fragments_list = fragments_list.get("fragments", [])
102-
103-
# convert to list if it's just a dict
104-
if not isinstance(fragments_list, list):
88+
fragments_data = os_util.load_json_from_file(fragments_json or input_path)
89+
if isinstance(fragments_data, dict):
90+
fragments_list = fragments_data.get("fragments", [])
91+
# standalone fragments from external file
92+
fragments_list.extend(fragments_data.get("standaloneFragments", []))
93+
94+
# convert to list if it's just a dict. if it's empty, make it an empty list
95+
if not fragments_data:
96+
fragments_list = []
97+
elif not isinstance(fragments_list, list):
10598
fragments_list = [fragments_list]
10699

107100
# telling the user what operation we're doing
@@ -249,7 +242,15 @@ def acifragmentgen_confcom(
249242
import_statements = []
250243
# images can have multiple fragments attached to them so we need an array to hold the import statements
251244
if fragment_path:
245+
# download and cleanup the fragment from registry if it's not local already
246+
downloaded_fragment = False
247+
if not os.path.exists(fragment_path):
248+
fragment_path = oras_proxy.pull(fragment_path)
249+
downloaded_fragment = True
252250
import_statements = [cose_client.generate_import_from_path(fragment_path, minimum_svn=minimum_svn)]
251+
if downloaded_fragment:
252+
os_util.clean_up_temp_folder(fragment_path)
253+
253254
elif image_name:
254255
import_statements = oras_proxy.generate_imports_from_image_name(image_name, minimum_svn=minimum_svn)
255256

@@ -260,14 +261,15 @@ def acifragmentgen_confcom(
260261
if os.path.isfile(fragments_json):
261262
fragments_file_contents = os_util.load_json_from_file(fragments_json)
262263
if isinstance(fragments_file_contents, list):
263-
logger.error(
264+
eprint(
264265
"%s %s %s %s",
265266
"Unsupported JSON file format. ",
266267
"Please make sure the outermost structure is not an array. ",
267268
"An empty import file should look like: ",
268-
REGO_IMPORT_FILE_STRUCTURE
269+
REGO_IMPORT_FILE_STRUCTURE,
270+
exit_code=1
269271
)
270-
sys.exit(1)
272+
271273
fragments_list = fragments_file_contents.get(POLICY_FIELD_CONTAINERS_ELEMENTS_REGO_FRAGMENTS, [])
272274

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

311-
# if no feed is provided, use the first image's feed
312-
# to assume it's an image-attached fragment
313-
if not image_target:
314-
policy_images = policy.get_images()
315-
if not policy_images:
316-
logger.error("No images found in the policy or all images are covered by fragments")
317-
sys.exit(1)
318-
image_target = policy_images[0].containerImage
313+
# make sure we have images to generate a fragment
314+
policy_images = policy.get_images()
315+
if not policy_images:
316+
eprint("No images found in the policy or all images are covered by fragments")
317+
319318
if not feed:
320319
# strip the tag or hash off the image name so there are stable feed names
321320
feed = get_image_name(image_target)
@@ -336,8 +335,10 @@ def acifragmentgen_confcom(
336335
out_path = filename + ".cose"
337336

338337
cose_proxy.cose_sign(filename, key, chain, feed, iss, algo, out_path)
339-
if upload_fragment:
338+
if upload_fragment and image_target:
340339
oras_proxy.attach_fragment_to_image(image_target, out_path)
340+
elif upload_fragment:
341+
oras_proxy.push_fragment_to_registry(feed, out_path)
341342

342343

343344
def katapolicygen_confcom(

src/confcom/azext_confcom/data/internal_config.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"version": "1.2.6",
2+
"version": "1.2.7",
33
"hcsshim_config": {
44
"maxVersion": "1.0.0",
55
"minVersion": "0.0.1"

0 commit comments

Comments
 (0)