Skip to content

Commit f2fc2d5

Browse files
committed
Add fragment references from_image command
1 parent 1901333 commit f2fc2d5

File tree

9 files changed

+249
-0
lines changed

9 files changed

+249
-0
lines changed

src/confcom/azext_confcom/_params.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,14 @@ def load_arguments(self, _):
235235
required=False,
236236
help='Container definitions to include in the policy'
237237
)
238+
c.argument(
239+
"fragment_definitions",
240+
options_list=['--with-fragments'],
241+
action='append',
242+
type=json.loads,
243+
required=False,
244+
help='Fragment definitions to include in the policy'
245+
)
238246

239247
with self.argument_context("confcom acifragmentgen") as c:
240248
c.argument(
@@ -484,3 +492,16 @@ def load_arguments(self, _):
484492
type=str,
485493
help="Platform to create container definition for",
486494
)
495+
496+
with self.argument_context("confcom fragment references from_image") as c:
497+
c.positional(
498+
"image",
499+
type=str,
500+
help="Image to create container definition from",
501+
)
502+
c.argument(
503+
"minimum_svn",
504+
required=False,
505+
type=str,
506+
help="Minimum Allowed Software Version Number for Fragment",
507+
)
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# --------------------------------------------------------------------------------------------
2+
# Copyright (c) Microsoft Corporation. All rights reserved.
3+
# Licensed under the MIT License. See License.txt in the project root for license information.
4+
# --------------------------------------------------------------------------------------------
5+
6+
import json
7+
8+
from typing import Optional
9+
10+
from azext_confcom.lib.fragment_references_from_image import fragment_references_from_image as lib_fragment_references_from_image
11+
12+
13+
def fragment_references_from_image(image: str, minimum_svn: Optional[str]) -> str:
14+
return print(json.dumps(list(lib_fragment_references_from_image(image, minimum_svn))))

src/confcom/azext_confcom/commands.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,6 @@ def load_command_table(self, _):
2020

2121
with self.command_group("confcom containers") as g:
2222
g.custom_command("from_image", "containers_from_image")
23+
24+
with self.command_group("confcom fragment references") as g:
25+
g.custom_command("from_image", "fragment_references_from_image")

src/confcom/azext_confcom/custom.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from azext_confcom.command.fragment_attach import fragment_attach as _fragment_attach
2626
from azext_confcom.command.fragment_push import fragment_push as _fragment_push
2727
from azext_confcom.command.containers_from_image import containers_from_image as _containers_from_image
28+
from azext_confcom.command.fragment_references_from_image import fragment_references_from_image as _fragment_references_from_image
2829
from knack.log import get_logger
2930
from pkg_resources import parse_version
3031

@@ -57,6 +58,7 @@ def acipolicygen_confcom(
5758
include_fragments: bool = False,
5859
fragments_json: str = None,
5960
exclude_default_fragments: bool = False,
61+
fragment_definitions: Optional[list] = None,
6062
):
6163
if print_existing_policy or outraw or outraw_pretty_print:
6264
logger.warning(
@@ -165,6 +167,10 @@ def acipolicygen_confcom(
165167
container_definitions=container_definitions,
166168
)
167169

170+
if fragment_definitions:
171+
for fragment_definition in fragment_definitions:
172+
container_group_policies._fragments.extend(fragment_definition) # pylint: disable=protected-access
173+
168174
exit_code = 0
169175

170176
# standardize the output so we're only operating on arrays
@@ -561,3 +567,13 @@ def containers_from_image(
561567
image=image,
562568
platform=platform,
563569
)
570+
571+
572+
def fragment_references_from_image(
573+
image: str,
574+
minimum_svn: Optional[str],
575+
) -> None:
576+
_fragment_references_from_image(
577+
image=image,
578+
minimum_svn=minimum_svn,
579+
)
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# --------------------------------------------------------------------------------------------
2+
# Copyright (c) Microsoft Corporation. All rights reserved.
3+
# Licensed under the MIT License. See License.txt in the project root for license information.
4+
# --------------------------------------------------------------------------------------------
5+
6+
import hashlib
7+
import platform
8+
import re
9+
import requests
10+
import subprocess
11+
12+
from typing import Iterable
13+
from pathlib import Path
14+
15+
from azext_confcom.lib.paths import get_binaries_dir
16+
17+
18+
_binaries_dir = get_binaries_dir()
19+
_cosesign1_binaries = {
20+
"Linux": {
21+
"path": _binaries_dir / "sign1util",
22+
"url": "https://github.com/microsoft/cosesign1go/releases/download/v1.4.0/sign1util",
23+
"sha256": "526b54aeb6293fc160e8fa1f81be6857300aba9641d45955f402f8b082a4d4a5",
24+
},
25+
"Windows": {
26+
"path": _binaries_dir / "sign1util.exe",
27+
"url": "https://github.com/microsoft/cosesign1go/releases/download/v1.4.0/sign1util.exe",
28+
"sha256": "f33cccf2b1bb8c3a495c730984b47d0f0715678981dbfe712248a2452dd53303",
29+
},
30+
}
31+
32+
33+
def cose_get():
34+
for binary_info in _cosesign1_binaries.values():
35+
cosesign1_fetch_resp = requests.get(binary_info["url"], verify=True)
36+
cosesign1_fetch_resp.raise_for_status()
37+
38+
assert hashlib.sha256(cosesign1_fetch_resp.content).hexdigest() == binary_info["sha256"]
39+
40+
with open(binary_info["path"], "wb") as f:
41+
f.write(cosesign1_fetch_resp.content)
42+
43+
44+
def cose_run(args: Iterable[str]) -> subprocess.CompletedProcess:
45+
return subprocess.run(
46+
[_cosesign1_binaries[platform.system()]["path"], *args],
47+
check=True,
48+
stdout=subprocess.PIPE,
49+
text=True,
50+
)
51+
52+
53+
def cose_print(file_path: Path):
54+
return cose_run([
55+
"print",
56+
"--in", file_path.as_posix(),
57+
]).stdout.strip()
58+
59+
60+
def cose_get_properties(file_path: Path):
61+
cose_print_output = cose_print(file_path)
62+
return {
63+
"iss": re.search(r"^iss:\s*(.*)$", cose_print_output, re.MULTILINE).group(1),
64+
"feed": re.search(r"^feed:\s*(.*)$", cose_print_output, re.MULTILINE).group(1),
65+
"payload": re.search(r"^payload:\s*(.*)", cose_print_output, re.MULTILINE | re.DOTALL).group(1),
66+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# --------------------------------------------------------------------------------------------
2+
# Copyright (c) Microsoft Corporation. All rights reserved.
3+
# Licensed under the MIT License. See License.txt in the project root for license information.
4+
# --------------------------------------------------------------------------------------------
5+
6+
import tempfile
7+
8+
from pathlib import Path
9+
from typing import Optional
10+
11+
from azext_confcom.lib.cose import cose_get_properties
12+
from azext_confcom.lib.fragments import get_fragments_from_image
13+
from azext_confcom.lib.opa import opa_eval
14+
15+
16+
def fragment_references_from_image(image: str, minimum_svn: Optional[str]):
17+
18+
for signed_fragment in get_fragments_from_image(image):
19+
20+
package_name = signed_fragment.name.split(".")[0]
21+
cose_properties = cose_get_properties(signed_fragment)
22+
23+
with tempfile.NamedTemporaryFile("w+b") as payload:
24+
payload.write(cose_properties["payload"].encode("utf-8"))
25+
payload.flush()
26+
fragment_properties = opa_eval(
27+
Path(payload.name),
28+
f"data.{package_name}",
29+
)["result"][0]["expressions"][0]["value"]
30+
31+
yield {
32+
"feed": cose_properties["feed"],
33+
"includes": sorted(list(set(fragment_properties.keys()).intersection({
34+
"containers",
35+
"fragmnents",
36+
"namespace",
37+
"external_processes",
38+
}))),
39+
"issuer": cose_properties["iss"],
40+
"minimum_svn": minimum_svn or fragment_properties["svn"],
41+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# --------------------------------------------------------------------------------------------
2+
# Copyright (c) Microsoft Corporation. All rights reserved.
3+
# Licensed under the MIT License. See License.txt in the project root for license information.
4+
# --------------------------------------------------------------------------------------------
5+
6+
import tempfile
7+
8+
from pathlib import Path
9+
10+
from azext_confcom.lib.images import sanitize_image_reference
11+
from azext_confcom.lib.oras import get_artifact_references, pull
12+
13+
14+
def get_fragments_from_image(image_reference: str):
15+
16+
for reference in get_artifact_references(image_reference):
17+
18+
fragment_path = Path(tempfile.gettempdir()) / sanitize_image_reference(reference)
19+
pull(reference, fragment_path)
20+
21+
for fragment_file in fragment_path.glob("*.rego.cose"):
22+
yield fragment_file

src/confcom/azext_confcom/lib/images.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import functools
77
import os
8+
import re
89
import subprocess
910
import docker
1011

@@ -62,3 +63,7 @@ def get_image_config(image: str) -> dict:
6263
config["working_dir"] = image_config.get("WorkingDir")
6364

6465
return config
66+
67+
68+
def sanitize_image_reference(image_reference: str) -> str:
69+
return re.sub(f"[{re.escape(r'<>:"/\\|?*@\0')}]", "-", image_reference)
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# --------------------------------------------------------------------------------------------
2+
# Copyright (c) Microsoft Corporation. All rights reserved.
3+
# Licensed under the MIT License. See License.txt in the project root for license information.
4+
# --------------------------------------------------------------------------------------------
5+
6+
import shutil
7+
import json
8+
import subprocess
9+
10+
from pathlib import Path
11+
from typing import Iterable
12+
13+
from azext_confcom.errors import eprint
14+
15+
16+
def oras_run(args: Iterable[str]) -> subprocess.CompletedProcess:
17+
18+
# Maintain existing behaviour of requiring the user to install ORAS themselves
19+
if not shutil.which("oras"):
20+
eprint("ORAS CLI not installed. Please install ORAS CLI: https://oras.land/docs/installation")
21+
22+
return subprocess.run(
23+
["oras", *args],
24+
check=True,
25+
stdout=subprocess.PIPE,
26+
stderr=subprocess.DEVNULL,
27+
text=True,
28+
)
29+
30+
31+
def discover(reference: str):
32+
return json.loads(oras_run([
33+
"discover",
34+
"--format",
35+
"json",
36+
reference,
37+
]).stdout.strip())
38+
39+
40+
def pull(reference: str, destination: Path):
41+
return oras_run([
42+
"pull",
43+
"--output",
44+
destination.as_posix(),
45+
reference,
46+
])
47+
48+
49+
def get_artifact_references(reference: str):
50+
51+
def get_references(discover_result):
52+
if "artifactType" in discover_result:
53+
yield discover_result["reference"]
54+
for field in discover_result.values():
55+
if isinstance(field, list):
56+
for item in field:
57+
yield from get_references(item)
58+
if isinstance(field, dict):
59+
yield from get_references(field)
60+
61+
return list(get_references(discover(reference)))

0 commit comments

Comments
 (0)