Skip to content

Commit 3ca4ce5

Browse files
authored
[confcom] Add a --with-containers flag to policy and fragment gen (#9409)
* Add a --with-containers flag to policy and fragment gen * Style fixes * Update HISTORY.rst * Bump confcom version * Support more kinds of container defs * Update version * Cleanup * Remove unused array * Fix existing tests * Add new basic tests * Fix running without --with-containers * Fix acifragmentgen with no containers
1 parent c7f8eff commit 3ca4ce5

File tree

9 files changed

+240
-8
lines changed

9 files changed

+240
-8
lines changed

src/confcom/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,5 @@ azext_confcom/bin/*
3636
**/.coverage
3737

3838
**/htmlcov
39+
40+
!lib/

src/confcom/HISTORY.rst

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

6+
1.4.0
7+
++++++
8+
* Add --with-containers flag to acipolicygen and acifragmentgen to allow passing container policy definitions directly
9+
610
1.3.1
711
++++++
812
* bugfix for --exclude-default-fragments flag not working as intended

src/confcom/azext_confcom/_params.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
# --------------------------------------------------------------------------------------------
55
# pylint: disable=line-too-long
66

7+
import json
78
from knack.arguments import CLIArgumentType
89
from azext_confcom._validators import (
910
validate_params_file,
@@ -198,6 +199,14 @@ def load_arguments(self, _):
198199
required=False,
199200
help="Exclude default fragments in the generated policy",
200201
)
202+
c.argument(
203+
"container_definitions",
204+
options_list=['--with-containers'],
205+
action='append',
206+
type=json.loads,
207+
required=False,
208+
help='Container definitions to include in the policy'
209+
)
201210

202211
with self.argument_context("confcom acifragmentgen") as c:
203212
c.argument(
@@ -345,6 +354,14 @@ def load_arguments(self, _):
345354
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",
346355
validator=validate_fragment_json,
347356
)
357+
c.argument(
358+
"container_definitions",
359+
options_list=['--with-containers'],
360+
action='append',
361+
required=False,
362+
type=json.loads,
363+
help='Container definitions to include in the policy'
364+
)
348365

349366
with self.argument_context("confcom katapolicygen") as c:
350367
c.argument(

src/confcom/azext_confcom/_validators.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ def validate_aci_source(namespace):
3838
namespace.input_path,
3939
namespace.arm_template,
4040
namespace.image_name,
41-
namespace.virtual_node_yaml_path
41+
namespace.virtual_node_yaml_path,
42+
namespace.container_definitions is not None,
4243
])) != 1:
4344
raise CLIError("Can only generate CCE policy from one source at a time")
4445

@@ -71,7 +72,11 @@ def validate_fragment_key_and_chain(namespace):
7172

7273

7374
def validate_fragment_source(namespace):
74-
if not namespace.generate_import and sum(map(bool, [namespace.image_name, namespace.input_path])) != 1:
75+
if not namespace.generate_import and sum(map(bool, [
76+
namespace.image_name,
77+
namespace.input_path,
78+
namespace.container_definitions is not None,
79+
])) != 1:
7580
raise CLIError("Must provide either an image name or an input file to generate a fragment")
7681

7782

src/confcom/azext_confcom/custom.py

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,13 @@
1111
from azext_confcom._validators import resolve_stdio
1212
from azext_confcom.config import (
1313
DEFAULT_REGO_FRAGMENTS, POLICY_FIELD_CONTAINERS_ELEMENTS_REGO_FRAGMENTS,
14-
REGO_IMPORT_FILE_STRUCTURE)
14+
REGO_IMPORT_FILE_STRUCTURE, ACI_FIELD_VERSION, ACI_FIELD_CONTAINERS)
1515
from azext_confcom.cose_proxy import CoseSignToolProxy
1616
from azext_confcom.errors import eprint
1717
from azext_confcom.fragment_util import get_all_fragment_contents
1818
from azext_confcom.init_checks import run_initial_docker_checks
1919
from azext_confcom.kata_proxy import KataPolicyGenProxy
20-
from azext_confcom.security_policy import OutputType
20+
from azext_confcom.security_policy import AciPolicy, OutputType
2121
from azext_confcom.template_util import (
2222
get_image_name, inject_policy_into_template, inject_policy_into_yaml,
2323
pretty_print_func, print_existing_policy_from_arm_template,
@@ -37,6 +37,7 @@ def acipolicygen_confcom(
3737
virtual_node_yaml_path: str,
3838
infrastructure_svn: str,
3939
tar_mapping_location: str,
40+
container_definitions: Optional[list] = None,
4041
approve_wildcards: str = False,
4142
outraw: bool = False,
4243
outraw_pretty_print: bool = False,
@@ -64,6 +65,9 @@ def acipolicygen_confcom(
6465
"For additional information, see http://aka.ms/clisecrets. \n",
6566
)
6667

68+
if container_definitions is None:
69+
container_definitions = []
70+
6771
stdio_enabled = resolve_stdio(enable_stdio, disable_stdio)
6872

6973
if print_existing_policy and arm_template:
@@ -147,6 +151,16 @@ def acipolicygen_confcom(
147151
exclude_default_fragments=exclude_default_fragments,
148152
infrastructure_svn=infrastructure_svn,
149153
)
154+
elif container_definitions:
155+
container_group_policies = AciPolicy(
156+
{
157+
ACI_FIELD_VERSION: "1.0",
158+
ACI_FIELD_CONTAINERS: [],
159+
},
160+
debug_mode=debug_mode,
161+
disable_stdio=disable_stdio,
162+
container_definitions=container_definitions,
163+
)
150164

151165
exit_code = 0
152166

@@ -227,6 +241,7 @@ def acifragmentgen_confcom(
227241
key: str,
228242
chain: str,
229243
minimum_svn: str,
244+
container_definitions: Optional[list] = None,
230245
image_target: str = "",
231246
algo: str = "ES384",
232247
fragment_path: str = None,
@@ -241,6 +256,8 @@ def acifragmentgen_confcom(
241256
no_print: bool = False,
242257
fragments_json: str = "",
243258
):
259+
if container_definitions is None:
260+
container_definitions = []
244261

245262
stdio_enabled = resolve_stdio(enable_stdio, disable_stdio)
246263

@@ -299,13 +316,27 @@ def acifragmentgen_confcom(
299316
policy = security_policy.load_policy_from_image_name(
300317
image_name, debug_mode=debug_mode, disable_stdio=(not stdio_enabled)
301318
)
302-
else:
319+
elif input_path:
303320
# this is using --input
304321
if not tar_mapping:
305322
tar_mapping = os_util.load_tar_mapping_from_config_file(input_path)
306323
policy = security_policy.load_policy_from_json_file(
307324
input_path, debug_mode=debug_mode, disable_stdio=(not stdio_enabled)
308325
)
326+
elif container_definitions:
327+
policy = AciPolicy(
328+
{
329+
ACI_FIELD_VERSION: "1.0",
330+
ACI_FIELD_CONTAINERS: [],
331+
},
332+
debug_mode=debug_mode,
333+
disable_stdio=disable_stdio,
334+
container_definitions=container_definitions,
335+
)
336+
else:
337+
eprint("Either --image-name, --input, or --container-definitions must be provided", exit_code=2)
338+
return
339+
309340
# get all of the fragments that are being used in the policy
310341
# and associate them with each container group
311342
fragment_policy_list = []
@@ -321,7 +352,7 @@ def acifragmentgen_confcom(
321352

322353
# make sure we have images to generate a fragment
323354
policy_images = policy.get_images()
324-
if not policy_images:
355+
if not policy_images and not container_definitions:
325356
eprint("No images found in the policy or all images are covered by fragments")
326357

327358
if not feed:
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
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+
from dataclasses import dataclass, field, is_dataclass
7+
import inspect
8+
import sys
9+
from typing import Literal, Optional
10+
11+
12+
def get_default_capabilities():
13+
return [
14+
"CAP_AUDIT_WRITE",
15+
"CAP_CHOWN",
16+
"CAP_DAC_OVERRIDE",
17+
"CAP_FOWNER",
18+
"CAP_FSETID",
19+
"CAP_KILL",
20+
"CAP_MKNOD",
21+
"CAP_NET_BIND_SERVICE",
22+
"CAP_NET_RAW",
23+
"CAP_SETFCAP",
24+
"CAP_SETGID",
25+
"CAP_SETPCAP",
26+
"CAP_SETUID",
27+
"CAP_SYS_CHROOT"
28+
]
29+
30+
31+
@dataclass
32+
class ContainerCapabilities:
33+
ambient: list[str] = field(default_factory=list)
34+
bounding: list[str] = field(default_factory=get_default_capabilities)
35+
effective: list[str] = field(default_factory=get_default_capabilities)
36+
inheritable: list[str] = field(default_factory=list)
37+
permitted: list[str] = field(default_factory=get_default_capabilities)
38+
39+
40+
@dataclass
41+
class ContainerRule:
42+
pattern: str
43+
strategy: str
44+
required: Optional[bool] = False
45+
46+
47+
@dataclass
48+
class ContainerExecProcesses:
49+
command: list[str]
50+
signals: Optional[list[str]] = None
51+
allow_stdio_access: bool = True
52+
53+
54+
@dataclass
55+
class ContainerMount:
56+
destination: str
57+
source: str
58+
type: str
59+
options: list[str] = field(default_factory=list)
60+
61+
62+
@dataclass
63+
class ContainerUser:
64+
group_idnames: list[ContainerRule] = field(default_factory=lambda: [ContainerRule(pattern="", strategy="any")])
65+
umask: str = "0022"
66+
user_idname: ContainerRule = field(default_factory=lambda: ContainerRule(pattern="", strategy="any"))
67+
68+
69+
@dataclass
70+
class FragmentReference:
71+
feed: str
72+
issuer: str
73+
minimum_svn: str
74+
includes: list[Literal["containers", "fragments", "namespace", "external_processes"]]
75+
path: Optional[str] = None
76+
77+
78+
@dataclass
79+
class Container:
80+
allow_elevated: bool = False
81+
allow_stdio_access: bool = True
82+
capabilities: ContainerCapabilities = field(default_factory=ContainerCapabilities)
83+
command: Optional[list[str]] = None
84+
env_rules: list[ContainerRule] = field(default_factory=list)
85+
exec_processes: list[ContainerExecProcesses] = field(default_factory=list)
86+
id: Optional[str] = None
87+
layers: list[str] = field(default_factory=list)
88+
mounts: list[ContainerMount] = field(default_factory=list)
89+
name: Optional[str] = None
90+
no_new_privileges: bool = False
91+
seccomp_profile_sha256: str = ""
92+
signals: list[str] = field(default_factory=list)
93+
user: ContainerUser = field(default_factory=ContainerUser)
94+
working_dir: str = "/"
95+
96+
97+
@dataclass
98+
class Policy:
99+
package: str = "policy"
100+
api_version: str = "0.10.0"
101+
framework_version: str = "0.2.3"
102+
fragments: list[FragmentReference] = field(default_factory=list)
103+
containers: list[Container] = field(default_factory=list)
104+
allow_properties_access: bool = True
105+
allow_dump_stacks: bool = False
106+
allow_runtime_logging: bool = False
107+
allow_environment_variable_dropping: bool = True
108+
allow_unencrypted_scratch: bool = False
109+
allow_capability_dropping: bool = True
110+
111+
112+
@dataclass
113+
class Fragment:
114+
package: str = "fragment"
115+
svn: str = "0"
116+
framework_version: str = "0.2.3"
117+
fragments: list[FragmentReference] = field(default_factory=list)
118+
containers: list[Container] = field(default_factory=list)

src/confcom/azext_confcom/security_policy.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@
44
# --------------------------------------------------------------------------------------------
55

66
import copy
7+
from dataclasses import asdict
78
import json
89
import warnings
910
from enum import Enum, auto
10-
from typing import Any, Dict, List, Tuple, Union
11+
from typing import Any, Dict, List, Optional, Tuple, Union
1112

1213
import deepdiff
1314
from azext_confcom import config, os_util
15+
from azext_confcom.lib.policy import Container
1416
from azext_confcom.container import ContainerImage, UserContainerImage
1517
from azext_confcom.errors import eprint
1618
from azext_confcom.fragment_util import sanitize_fragment_fields
@@ -65,6 +67,7 @@ def __init__(
6567
disable_stdio: bool = False,
6668
is_vn2: bool = False,
6769
fragment_contents: Any = None,
70+
container_definitions: Optional[list] = None,
6871
) -> None:
6972
self._rootfs_proxy = None
7073
self._policy_str = None
@@ -74,6 +77,15 @@ def __init__(
7477
self._existing_fragments = existing_rego_fragments
7578
self._api_version = config.API_VERSION
7679
self._fragment_contents = fragment_contents
80+
self._container_definitions = container_definitions or []
81+
82+
self._container_definitions = []
83+
if container_definitions:
84+
for container_definition in container_definitions:
85+
if isinstance(container_definition, list):
86+
self._container_definitions.extend(container_definition)
87+
else:
88+
self._container_definitions.append(container_definition)
7789

7890
if debug_mode:
7991
self._allow_properties_access = config.DEBUG_MODE_SETTINGS.get(
@@ -399,6 +411,8 @@ def _policy_serialization(self, pretty_print=False, include_sidecars: bool = Tru
399411
for container in policy:
400412
container[config.POLICY_FIELD_CONTAINERS_ELEMENTS_ALLOW_STDIO_ACCESS] = False
401413

414+
policy += [asdict(Container(**c)) for c in self._container_definitions]
415+
402416
if pretty_print:
403417
return pretty_print_func(policy)
404418
return print_func(policy)

src/confcom/azext_confcom/tests/latest/test_confcom_acipolicygen_arm.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,4 +252,45 @@ def test_acipolicygen_arm_diff_with_allow_all():
252252
}
253253

254254

255+
@pytest.mark.parametrize(
256+
"container_definitions",
257+
[
258+
["{}"], # Single empty container definition (use all default values)
259+
["{}", "{}"], # Two empty container definitions
260+
["[{}]", "{}"], # Two empty container definitions, one in subarray
261+
["[{}, {}]", "{}"], # Three empty container definitions, two in subarray
262+
['{"id": "test"}'], # Single container definition a field changed
263+
]
264+
)
265+
def test_acipolicygen_with_containers(container_definitions):
266+
267+
acipolicygen_confcom(
268+
input_path=None,
269+
arm_template=None,
270+
arm_template_parameters=None,
271+
image_name=None,
272+
virtual_node_yaml_path=None,
273+
infrastructure_svn=None,
274+
tar_mapping_location=None,
275+
outraw=True,
276+
container_definitions=[json.loads(c) for c in container_definitions]
277+
)
255278

279+
280+
def test_acipolicygen_with_containers_field_changed():
281+
282+
buffer = io.StringIO()
283+
with contextlib.redirect_stdout(buffer):
284+
acipolicygen_confcom(
285+
input_path=None,
286+
arm_template=None,
287+
arm_template_parameters=None,
288+
image_name=None,
289+
virtual_node_yaml_path=None,
290+
infrastructure_svn=None,
291+
tar_mapping_location=None,
292+
outraw=True,
293+
container_definitions=[json.loads('{"id": "test"}')]
294+
)
295+
actual_policy = buffer.getvalue()
296+
assert '"id":"test"' in actual_policy

0 commit comments

Comments
 (0)