cfn-check 0.6.2__tar.gz → 0.7.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. {cfn_check-0.6.2 → cfn_check-0.7.1}/PKG-INFO +58 -4
  2. {cfn_check-0.6.2 → cfn_check-0.7.1}/README.md +57 -3
  3. {cfn_check-0.6.2 → cfn_check-0.7.1}/cfn_check/cli/render.py +27 -26
  4. {cfn_check-0.6.2 → cfn_check-0.7.1}/cfn_check/cli/utils/files.py +0 -1
  5. cfn_check-0.7.1/cfn_check/cli/utils/stdout.py +18 -0
  6. {cfn_check-0.6.2 → cfn_check-0.7.1}/cfn_check/cli/validate.py +12 -22
  7. {cfn_check-0.6.2 → cfn_check-0.7.1}/cfn_check/evaluation/evaluator.py +12 -3
  8. {cfn_check-0.6.2 → cfn_check-0.7.1}/cfn_check/evaluation/validate.py +6 -1
  9. cfn_check-0.7.1/cfn_check/rendering/cidr_solver.py +66 -0
  10. {cfn_check-0.6.2 → cfn_check-0.7.1}/cfn_check/rendering/renderer.py +497 -151
  11. {cfn_check-0.6.2 → cfn_check-0.7.1}/cfn_check.egg-info/PKG-INFO +58 -4
  12. {cfn_check-0.6.2 → cfn_check-0.7.1}/cfn_check.egg-info/SOURCES.txt +2 -0
  13. {cfn_check-0.6.2 → cfn_check-0.7.1}/cfn_check.egg-info/requires.txt +1 -1
  14. {cfn_check-0.6.2 → cfn_check-0.7.1}/pyproject.toml +2 -2
  15. {cfn_check-0.6.2 → cfn_check-0.7.1}/LICENSE +0 -0
  16. {cfn_check-0.6.2 → cfn_check-0.7.1}/cfn_check/__init__.py +0 -0
  17. {cfn_check-0.6.2 → cfn_check-0.7.1}/cfn_check/cli/__init__.py +0 -0
  18. {cfn_check-0.6.2 → cfn_check-0.7.1}/cfn_check/cli/root.py +0 -0
  19. {cfn_check-0.6.2 → cfn_check-0.7.1}/cfn_check/cli/utils/__init__.py +0 -0
  20. {cfn_check-0.6.2 → cfn_check-0.7.1}/cfn_check/cli/utils/attributes.py +0 -0
  21. {cfn_check-0.6.2 → cfn_check-0.7.1}/cfn_check/collection/__init__.py +0 -0
  22. {cfn_check-0.6.2 → cfn_check-0.7.1}/cfn_check/collection/collection.py +0 -0
  23. {cfn_check-0.6.2 → cfn_check-0.7.1}/cfn_check/evaluation/__init__.py +0 -0
  24. {cfn_check-0.6.2 → cfn_check-0.7.1}/cfn_check/evaluation/errors.py +0 -0
  25. {cfn_check-0.6.2 → cfn_check-0.7.1}/cfn_check/evaluation/parsing/__init__.py +0 -0
  26. {cfn_check-0.6.2 → cfn_check-0.7.1}/cfn_check/evaluation/parsing/query_parser.py +0 -0
  27. {cfn_check-0.6.2 → cfn_check-0.7.1}/cfn_check/evaluation/parsing/token.py +0 -0
  28. {cfn_check-0.6.2 → cfn_check-0.7.1}/cfn_check/evaluation/parsing/token_type.py +0 -0
  29. {cfn_check-0.6.2 → cfn_check-0.7.1}/cfn_check/logging/__init__.py +0 -0
  30. {cfn_check-0.6.2 → cfn_check-0.7.1}/cfn_check/logging/models.py +0 -0
  31. {cfn_check-0.6.2 → cfn_check-0.7.1}/cfn_check/rendering/__init__.py +0 -0
  32. {cfn_check-0.6.2 → cfn_check-0.7.1}/cfn_check/rendering/utils.py +0 -0
  33. {cfn_check-0.6.2 → cfn_check-0.7.1}/cfn_check/rules/__init__.py +0 -0
  34. {cfn_check-0.6.2 → cfn_check-0.7.1}/cfn_check/rules/rule.py +0 -0
  35. {cfn_check-0.6.2 → cfn_check-0.7.1}/cfn_check/shared/__init__.py +0 -0
  36. {cfn_check-0.6.2 → cfn_check-0.7.1}/cfn_check/shared/types.py +0 -0
  37. {cfn_check-0.6.2 → cfn_check-0.7.1}/cfn_check/validation/__init__.py +0 -0
  38. {cfn_check-0.6.2 → cfn_check-0.7.1}/cfn_check/validation/validator.py +0 -0
  39. {cfn_check-0.6.2 → cfn_check-0.7.1}/cfn_check.egg-info/dependency_links.txt +0 -0
  40. {cfn_check-0.6.2 → cfn_check-0.7.1}/cfn_check.egg-info/entry_points.txt +0 -0
  41. {cfn_check-0.6.2 → cfn_check-0.7.1}/cfn_check.egg-info/top_level.txt +0 -0
  42. {cfn_check-0.6.2 → cfn_check-0.7.1}/example/multitag.py +0 -0
  43. {cfn_check-0.6.2 → cfn_check-0.7.1}/example/pydantic_rules.py +0 -0
  44. {cfn_check-0.6.2 → cfn_check-0.7.1}/example/renderer_test.py +0 -0
  45. {cfn_check-0.6.2 → cfn_check-0.7.1}/example/rules.py +0 -0
  46. {cfn_check-0.6.2 → cfn_check-0.7.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cfn-check
3
- Version: 0.6.2
3
+ Version: 0.7.1
4
4
  Summary: Validate Cloud Formation
5
5
  Author-email: Ada Lundhe <adalundhe@lundhe.audio>
6
6
  License: MIT License
@@ -34,7 +34,7 @@ Requires-Python: >=3.12
34
34
  Description-Content-Type: text/markdown
35
35
  License-File: LICENSE
36
36
  Requires-Dist: pydantic
37
- Requires-Dist: pyyaml
37
+ Requires-Dist: ruamel.yaml
38
38
  Requires-Dist: hyperlight-cocoa
39
39
  Requires-Dist: async-logging
40
40
  Dynamic: license-file
@@ -50,7 +50,7 @@ Dynamic: license-file
50
50
 
51
51
  | Package | cfn-check |
52
52
  | ----------- | ----------- |
53
- | Version | 0.3.3 |
53
+ | Version | 0.7.1 |
54
54
  | Download | https://pypi.org/project/cfn-check/ |
55
55
  | Source | https://github.com/adalundhe/cfn-check |
56
56
  | Keywords | cloud-formation, testing, aws, cli |
@@ -70,15 +70,20 @@ problems inherint to `cfn-lint` more than `cfn-guard`, primarily:
70
70
  - Inability to parse non-resource wildcards
71
71
  - Inability to validate non-resource template data
72
72
  - Inabillity to use structured models to validate input
73
+ - Poor ability to parse and render CloudFormation Refs/Functions
73
74
 
74
75
  In comparison to `cfn-guard`, `cfn-check` is pure Python, thus
75
76
  avoiding YADSL (Yet Another DSL) headaches. It also proves
76
77
  significantly more configurable/modular/hackable as a result.
78
+ `cfn-check` can resolve _some_ (not all) CloudFormation Intrinsic
79
+ Functions and Refs.
77
80
 
78
81
  CFN-Check uses a combination of simple depth-first-search tree
79
82
  parsing, friendly `cfn-lint` like query syntax, `Pydantic` models,
80
83
  and `pytest`-like assert-driven checks to make validating your
81
84
  Cloud Formation easy while offering both CLI and Python API interfaces.
85
+ CFN-Check also uses a lightning-fast AST-parser to render your templates,
86
+ allowing you to validate policy, not just a YAML document.
82
87
 
83
88
  <br/>
84
89
 
@@ -447,7 +452,7 @@ Resources::*::Type
447
452
  Selects all `Resource` objects. If we convert the Wildcard Token in the query to a Wildcard Range Token:
448
453
 
449
454
  ```
450
- Resources::*::Type
455
+ Resources::[*]::Type
451
456
  ```
452
457
 
453
458
  The Rule will fail as below:
@@ -585,3 +590,52 @@ class ValidateResourceType(Collection):
585
590
  ```
586
591
 
587
592
  By deferring type and existence assertions to `Pydantic` models, you can focus your actual assertion logic on business/security policy checks.
593
+
594
+ <br/>
595
+
596
+ # The Rendering Engine
597
+
598
+ ### Overview
599
+
600
+ In Version 0.6.X, CFN-Check introduced a rendering engine, which allows it
601
+ to parse and execute Refs and all CloudFormation intrinsic functions via
602
+ either the CloudFormation document or user-supplied values. This additional
603
+ also resulted in the:
604
+
605
+ ```bash
606
+ cfn-check render <TEMPLATE_PATH >
607
+ ```
608
+
609
+ command being added, allowing you to effectively "dry run" render your
610
+ CloudFormation templates akin to the `helm template` command for Helm.
611
+
612
+ By default, `cfn-check render` outputs to stdout, however you can easily
613
+ save rendered output to a file via the `-o/--output-file` flag. For example:
614
+
615
+ ```bash
616
+ cfn-check render template.yml -o rendered.yml
617
+ ```
618
+
619
+ The `cfn-check render` command also offers the following options:
620
+
621
+ - `-a/--attributes`: A list of <key>=<value> input `!GetAtt` attributes to use
622
+ - `-m/--mappings`: A list of <key>=<value> input `Mappings` to use
623
+ - `-p/--parameters`: A list of <key>=<value> input `Parameters` to use
624
+ - `-l/--log-level`: The log level to use
625
+
626
+ ### The Rendering Engine during Checks
627
+
628
+ By default rendering is enabled when running `cfn-check` validation. You can
629
+ disable it by supplying `no-render` to the `-F/--flags` option as below:
630
+
631
+ ```bash
632
+ cfn-check validate -F no-render -r rules.py template.yaml
633
+ ```
634
+
635
+ Disabling rendering means CFN-Check will validate your template as-is, with
636
+ no additional pre-processing and no application of user input values.
637
+
638
+ > [!WARNING]
639
+ > CloudFormation documents are <b>not</b> "plain yaml" and disabling
640
+ > rendering means any dynamically determined values will likely fail
641
+ > to pass validation, resulting in false positives for failures!
@@ -9,7 +9,7 @@
9
9
 
10
10
  | Package | cfn-check |
11
11
  | ----------- | ----------- |
12
- | Version | 0.3.3 |
12
+ | Version | 0.7.1 |
13
13
  | Download | https://pypi.org/project/cfn-check/ |
14
14
  | Source | https://github.com/adalundhe/cfn-check |
15
15
  | Keywords | cloud-formation, testing, aws, cli |
@@ -29,15 +29,20 @@ problems inherint to `cfn-lint` more than `cfn-guard`, primarily:
29
29
  - Inability to parse non-resource wildcards
30
30
  - Inability to validate non-resource template data
31
31
  - Inabillity to use structured models to validate input
32
+ - Poor ability to parse and render CloudFormation Refs/Functions
32
33
 
33
34
  In comparison to `cfn-guard`, `cfn-check` is pure Python, thus
34
35
  avoiding YADSL (Yet Another DSL) headaches. It also proves
35
36
  significantly more configurable/modular/hackable as a result.
37
+ `cfn-check` can resolve _some_ (not all) CloudFormation Intrinsic
38
+ Functions and Refs.
36
39
 
37
40
  CFN-Check uses a combination of simple depth-first-search tree
38
41
  parsing, friendly `cfn-lint` like query syntax, `Pydantic` models,
39
42
  and `pytest`-like assert-driven checks to make validating your
40
43
  Cloud Formation easy while offering both CLI and Python API interfaces.
44
+ CFN-Check also uses a lightning-fast AST-parser to render your templates,
45
+ allowing you to validate policy, not just a YAML document.
41
46
 
42
47
  <br/>
43
48
 
@@ -406,7 +411,7 @@ Resources::*::Type
406
411
  Selects all `Resource` objects. If we convert the Wildcard Token in the query to a Wildcard Range Token:
407
412
 
408
413
  ```
409
- Resources::*::Type
414
+ Resources::[*]::Type
410
415
  ```
411
416
 
412
417
  The Rule will fail as below:
@@ -543,4 +548,53 @@ class ValidateResourceType(Collection):
543
548
  assert value is not None
544
549
  ```
545
550
 
546
- By deferring type and existence assertions to `Pydantic` models, you can focus your actual assertion logic on business/security policy checks.
551
+ By deferring type and existence assertions to `Pydantic` models, you can focus your actual assertion logic on business/security policy checks.
552
+
553
+ <br/>
554
+
555
+ # The Rendering Engine
556
+
557
+ ### Overview
558
+
559
+ In Version 0.6.X, CFN-Check introduced a rendering engine, which allows it
560
+ to parse and execute Refs and all CloudFormation intrinsic functions via
561
+ either the CloudFormation document or user-supplied values. This additional
562
+ also resulted in the:
563
+
564
+ ```bash
565
+ cfn-check render <TEMPLATE_PATH >
566
+ ```
567
+
568
+ command being added, allowing you to effectively "dry run" render your
569
+ CloudFormation templates akin to the `helm template` command for Helm.
570
+
571
+ By default, `cfn-check render` outputs to stdout, however you can easily
572
+ save rendered output to a file via the `-o/--output-file` flag. For example:
573
+
574
+ ```bash
575
+ cfn-check render template.yml -o rendered.yml
576
+ ```
577
+
578
+ The `cfn-check render` command also offers the following options:
579
+
580
+ - `-a/--attributes`: A list of <key>=<value> input `!GetAtt` attributes to use
581
+ - `-m/--mappings`: A list of <key>=<value> input `Mappings` to use
582
+ - `-p/--parameters`: A list of <key>=<value> input `Parameters` to use
583
+ - `-l/--log-level`: The log level to use
584
+
585
+ ### The Rendering Engine during Checks
586
+
587
+ By default rendering is enabled when running `cfn-check` validation. You can
588
+ disable it by supplying `no-render` to the `-F/--flags` option as below:
589
+
590
+ ```bash
591
+ cfn-check validate -F no-render -r rules.py template.yaml
592
+ ```
593
+
594
+ Disabling rendering means CFN-Check will validate your template as-is, with
595
+ no additional pre-processing and no application of user input values.
596
+
597
+ > [!WARNING]
598
+ > CloudFormation documents are <b>not</b> "plain yaml" and disabling
599
+ > rendering means any dynamically determined values will likely fail
600
+ > to pass validation, resulting in false positives for failures!
@@ -3,45 +3,30 @@ from async_logging import LogLevelName, Logger, LoggingConfig
3
3
  from cocoa.cli import CLI
4
4
 
5
5
  from cfn_check.cli.utils.files import load_templates, write_to_file
6
+ from cfn_check.cli.utils.stdout import write_to_stdout
6
7
  from cfn_check.rendering import Renderer
7
8
  from cfn_check.logging.models import InfoLog
8
9
 
9
10
 
10
- @CLI.command(
11
- display_help_on_error=False
12
- )
11
+ @CLI.command()
13
12
  async def render(
14
13
  path: str,
15
- output_file: str = 'rendered.yml',
14
+ output_file: str | None = None,
15
+ attributes: list[str] | None = None,
16
+ mappings: list[str] | None = None,
16
17
  parameters: list[str] | None = None,
17
18
  references: list[str] | None = None,
18
- tags: list[str] = [
19
- 'Ref',
20
- 'Sub',
21
- 'Join',
22
- 'Select',
23
- 'Split',
24
- 'GetAtt',
25
- 'GetAZs',
26
- 'ImportValue',
27
- 'Equals',
28
- 'If',
29
- 'Not',
30
- 'And',
31
- 'Or',
32
- 'Condition',
33
- 'FindInMap',
34
- ],
35
19
  log_level: LogLevelName = 'info',
36
20
  ):
37
21
  """
38
22
  Render a Cloud Formation template
39
23
 
40
24
  @param output_file Path to output the rendered CloudFormation template to
25
+ @param attributes A list of <key>=<value> input !GetAtt attributes to use
26
+ @param mappings A list of <key>=<value> input Mappings to use
41
27
  @param parameters A list of <key>=<value> input Parameters to use
42
28
  @param references A list of <key>=<value> input !Ref values to use
43
- @param tags List of CloudFormation intrinsic function tags
44
- @param log_level The log level to use
29
+ @param log-level The log level to use
45
30
  """
46
31
  logging_config = LoggingConfig()
47
32
  logging_config.update(
@@ -49,6 +34,18 @@ async def render(
49
34
  log_output='stderr',
50
35
  )
51
36
 
37
+ parsed_attributes: dict[str, str] | None = None
38
+ if attributes:
39
+ parsed_attributes = dict([
40
+ attribute.split('=', maxsplit=1) for attribute in attributes if len(attribute.split('=', maxsplit=1)) > 0
41
+ ])
42
+
43
+ parsed_mappings: dict[str, str] | None = None
44
+ if mappings:
45
+ parsed_mappings = dict([
46
+ mapping.split('=', maxsplit=1) for mapping in mappings if len(mapping.split('=', maxsplit=1)) > 0
47
+ ])
48
+
52
49
  parsed_parameters: dict[str, str] | None = None
53
50
  if parameters:
54
51
  parsed_parameters = dict([
@@ -65,7 +62,6 @@ async def render(
65
62
 
66
63
  templates = await load_templates(
67
64
  path,
68
- tags,
69
65
  )
70
66
 
71
67
  assert len(templates) == 1 , '❌ Can only render one file'
@@ -74,10 +70,15 @@ async def render(
74
70
  renderer = Renderer()
75
71
  rendered = renderer.render(
76
72
  template,
73
+ attributes=parsed_attributes,
74
+ mappings=parsed_mappings,
77
75
  parameters=parsed_parameters,
78
76
  references=parsed_references,
79
77
  )
80
78
 
81
- await write_to_file(output_file, rendered)
79
+ if output_file is False:
80
+ await write_to_file(output_file, rendered)
81
+ await logger.log(InfoLog(message=f'✅ {path} template rendered'))
82
82
 
83
- await logger.log(InfoLog(message=f'✅ {path} template rendered'))
83
+ else:
84
+ await write_to_stdout(rendered)
@@ -67,7 +67,6 @@ async def localize_path(path: str, loop: asyncio.AbstractEventLoop):
67
67
 
68
68
  async def load_templates(
69
69
  path: str,
70
- tags: list[str],
71
70
  file_pattern: str | None = None,
72
71
  ):
73
72
 
@@ -0,0 +1,18 @@
1
+ import asyncio
2
+ import sys
3
+ from ruamel.yaml import YAML
4
+ from ruamel.yaml.comments import CommentedBase
5
+
6
+ async def write_to_stdout(data: CommentedBase):
7
+ loop = asyncio.get_event_loop()
8
+
9
+ yaml = YAML(typ=['rt'])
10
+ yaml.preserve_quotes = True
11
+ yaml.width = 4096
12
+ yaml.indent(mapping=2, sequence=4, offset=2)
13
+ await loop.run_in_executor(
14
+ None,
15
+ yaml.dump,
16
+ data,
17
+ sys.stdout,
18
+ )
@@ -11,36 +11,24 @@ from cfn_check.collection.collection import Collection
11
11
  from cfn_check.validation.validator import Validator
12
12
 
13
13
 
14
- @CLI.command()
14
+ @CLI.command(
15
+ shortnames={
16
+ 'flags': 'F'
17
+ }
18
+ )
15
19
  async def validate(
16
20
  path: str,
17
21
  file_pattern: str | None = None,
18
22
  rules: ImportType[Collection] = None,
19
- tags: list[str] = [
20
- 'Ref',
21
- 'Sub',
22
- 'Join',
23
- 'Select',
24
- 'Split',
25
- 'GetAtt',
26
- 'GetAZs',
27
- 'ImportValue',
28
- 'Equals',
29
- 'If',
30
- 'Not',
31
- 'And',
32
- 'Or',
33
- 'Condition',
34
- 'FindInMap',
35
- ],
23
+ flags: list[str] | None = None,
36
24
  log_level: LogLevelName = 'info',
37
25
  ):
38
26
  '''
39
27
  Validate Cloud Foundation
40
28
 
41
- @param rules Path to a file containing Collections
29
+ @param disabled A list of string features to disable during checks
42
30
  @param file_pattern A string pattern used to find template files
43
- @param tags List of CloudFormation intrinsic function tags
31
+ @param rules Path to a file containing Collections
44
32
  @param log_level The log level to use
45
33
  '''
46
34
 
@@ -52,9 +40,11 @@ async def validate(
52
40
 
53
41
  logger = Logger()
54
42
 
43
+ if flags is None:
44
+ flags = []
45
+
55
46
  templates = await load_templates(
56
47
  path,
57
- tags,
58
48
  file_pattern=file_pattern,
59
49
  )
60
50
 
@@ -71,7 +61,7 @@ async def validate(
71
61
  for rule in rules.data.values()
72
62
  for _, validation in inspect.getmembers(rule)
73
63
  if isinstance(validation, Validator)
74
- ])
64
+ ], flags=flags)
75
65
 
76
66
  if validation_error := validation_set.validate([
77
67
  template_data for _, template_data in templates
@@ -12,7 +12,14 @@ from .parsing import QueryParser
12
12
 
13
13
  class Evaluator:
14
14
 
15
- def __init__(self):
15
+ def __init__(
16
+ self,
17
+ flags: list[str] | None = None
18
+ ):
19
+ if flags is None:
20
+ flags = []
21
+
22
+ self.flags = flags
16
23
  self._query_parser = QueryParser()
17
24
  self._renderer = Renderer()
18
25
 
@@ -22,9 +29,11 @@ class Evaluator:
22
29
  path: str,
23
30
  ):
24
31
  items: Items = deque()
32
+
33
+ if 'no-render' not in self.flags:
34
+ resources = self._renderer.render(resources)
25
35
 
26
- rendered = self._renderer.render(resources)
27
- items.append(rendered)
36
+ items.append(resources)
28
37
 
29
38
  segments = path.split("::")[::-1]
30
39
  # Queries can be multi-segment,
@@ -9,8 +9,13 @@ class ValidationSet:
9
9
  def __init__(
10
10
  self,
11
11
  validators: list[Validator],
12
+ flags: list[str] | None = None
12
13
  ):
13
- self._evaluator = Evaluator()
14
+
15
+ if flags is None:
16
+ flags = []
17
+
18
+ self._evaluator = Evaluator(flags=flags)
14
19
  self._validators = validators
15
20
 
16
21
  @property
@@ -0,0 +1,66 @@
1
+ class IPv4CIDRSolver:
2
+
3
+ def __init__(
4
+ self,
5
+ host: str,
6
+ desired: int,
7
+ bits: int,
8
+ ):
9
+ self.host = host
10
+ self.subnets_desired = desired
11
+ self.subnet_bits = bits
12
+
13
+ host_ip, mask = self.host.split('/', maxsplit=1)
14
+
15
+ self.host_ip = host_ip
16
+ self._host_mask_string = f'/{mask}'
17
+ self.host_mask = int(mask)
18
+
19
+ self.subnet_mask = 32 - bits
20
+
21
+ self._host_octets = [
22
+ int(octet) for octet in self.host.strip(self._host_mask_string).split('.')
23
+ ]
24
+
25
+ def provision_subnets(self):
26
+ subnet_requested_ips = 2**self.subnet_bits
27
+ host_available_ips = 2**(32 - self.host_mask)
28
+
29
+ total_ips_requested = subnet_requested_ips * self.subnets_desired
30
+ if host_available_ips < total_ips_requested:
31
+ return []
32
+
33
+ return [
34
+ self._provision_subnet(
35
+ subnet_requested_ips,
36
+ idx
37
+ ) for idx in range(self.subnets_desired)
38
+ ]
39
+
40
+
41
+ def _provision_subnet(
42
+ self,
43
+ requested_ips: int,
44
+ idx: int,
45
+ ):
46
+ increment = requested_ips
47
+ octet_idx = -1
48
+ if requested_ips > 255:
49
+ increment /= 256
50
+ octet_idx -= 1
51
+
52
+ increment *= idx
53
+
54
+ subnet = list(self._host_octets)
55
+
56
+ subnet[octet_idx] += increment
57
+
58
+ subnet_base_ip = '.'.join([
59
+ str(octet) for octet in subnet
60
+ ])
61
+
62
+ return f'{subnet_base_ip}/{self.subnet_mask}'
63
+
64
+
65
+
66
+