Skip to content

Latest commit

 

History

History
288 lines (210 loc) · 10.8 KB

QUERY_PROJECTION_AND_INTERPOLATION.md

File metadata and controls

288 lines (210 loc) · 10.8 KB

Guard: Query, Projection and Interpolation

Recommended Readings

AWS CloudFormation Guard

Guard: Clauses

Guard: Query and Filtering

What are variables?

Variables in Guard are used to store information that needs to be referenced in an easy and repeatable fashion while authoring simple and complex rules. Variables are defined by keeping the concept of “immutability” in mind and therefore allow only a single shot assignment. Variables are evaluated lazily, meaning that the Guard engine only evaluates variables when it encounters such during the rule execution. Variables can store both static literals and dynamic properties resulting from Guard queries. Queries are often assigned to variables, so that they can be written once and referenced everywhere else.

Let’s take a look at variables in action in the upcoming sections.

Variable Assignment

The let keyword is used to initialize and assign a variable. As a best practice, variable names conform to snake case. A variable defined using the let keyword can be referenced by using the % prefix. One exception to this is that Guard currently does not support referencing variables inside custom error messages. Some examples of variable assignments follow in sections below.

Static assignment

Sample CloudFormation template:

Resources:
  EcsTask:
    Type: 'AWS::ECS::TaskDefinition'
    Properties:
      TaskRoleArn: 'arn:aws:iam::123456789012:role/my-role-name'

Sample Guard rule:

rule check_ecs_task_definition_task_role_arn
{
    let ecs_task_definition_task_role_arn = 'arn:aws:iam::123456789012:role/my-role-name'
    Resources.*.Properties.TaskRoleArn == %ecs_task_definition_task_role_arn
}

In the example above, you are using the variable ecs_task_definition_task_role_arn to store a static string value. You author a rule that ensures that the Amazon Resource Name (ARN) of the role for the Amazon ECS task defined in the CloudFormation template is equal to the arn:aws:iam::123456789012:role/my-role-name string value.

Query assignment

Sample CloudFormation template:

Resources:
  EcsTask:
    Type: 'AWS::ECS::TaskDefinition'
    Properties:
      TaskRoleArn: 'arn:aws:iam::123456789012:role/my-role-name'

Sample Guard rule:

let ecs_tasks = Resources.*[
    Type == 'AWS::ECS::TaskDefinition'
]

rule check_ecs_task_definition_task_role_arn
{
    %ecs_tasks.Properties.TaskRoleArn == 'arn:aws:iam::123456789012:role/my-role-name'
}

In the example above, you are querying all resources of type AWS::ECS::TaskDefinition in the input template, and storing them in the ecs_tasks variable. The rule check_ecs_task_definition_task_role_arn then asserts that all resources of type AWS::ECS::TaskDefinition have TaskRoleArn set to arn:aws:iam::123456789012:role/my-role-name.

Variable Referencing

Variables can also be referenced as a part of a query, e.g. %ecs_tasks.Properties. Guard would first evaluate the variable ecs_tasks and use values returned to traverse the hierarchy. If the variable ecs_tasks resolves to non-string values, then it is an error.

Sample CloudFormation template:

Resources:
  EcsTask:
    Type: 'AWS::ECS::TaskDefinition'
    Properties:
      TaskRoleArn: 'arn:aws:iam::123456789012:role/my-role-name'

Sample Guard rule:

let ecs_tasks = Resources.*[
    Type == 'AWS::ECS::TaskDefinition'
]

rule check_ecs_task_definition_task_role_arn when %ecs_tasks !empty
{
    %ecs_tasks.Properties.TaskRoleArn == 'arn:aws:iam::123456789012:role/my-role-name'
}

In the example above, you are using the variable ecs_tasks to store the information for all resources of the type AWS::ECS::TaskDefinition from the sample CloudFormation template. Then, the rule check_ecs_task_definition_task_role_arn, which is evaluated when the variable ecs_tasks is not empty - that is, at least one resource of type AWS::ECS::TaskDefinition exists in the template - asserts that the TaskRoleArn of all AWS::ECS::TaskDefinition types in the template is arn:aws:iam::123456789012:role/my-role-name.

Variable Scope

Scope refers to the visibility of variables defined in a rules file. As pointed out earlier, variables in Guard are single shot assignments. Also, there can only be one same named variable defined within the context of a scope. Broadly speaking, there are three places where a variable can be declared: file level, rule level and block level. Let’s take a look at those in sections below.

File level

When a variable is initialized at a file level, usually at the top of the file, it can be used pretty much in all the rules within that file. It is visible (hence accessible) to the entire file.

The example below illustrates the use of a file-scoped variable.

Sample CloudFormation template:

# Template-1
--- 
Resources: 
  EcsTask:
    Type: "AWS::ECS::TaskDefinition"
    Properties: 
      ExecutionRoleArn: "arn:aws:iam::123456789012:role/my-execution-role-name"
      TaskRoleArn: "arn:aws:iam::123456789012:role/my-task-role-name"

and a Guard rule set for the template, with rules that assert the TaskRoleArn and ExecutionRoleArn in the template as:

let ecs_task_definition_task_role_arn = 'arn:aws:iam::123456789012:role/my-task-role-name'
let ecs_task_definition_execution_role_arn = 'arn:aws:iam::123456789012:role/my-execution-role-name'

rule check_ecs_task_definition_task_role_arn
{
    Resources.*.Properties.TaskRoleArn == %ecs_task_definition_task_role_arn
}

rule check_ecs_task_definition_execution_role_arn
{
    Resources.*.Properties.ExecutionRoleArn == %ecs_task_definition_execution_role_arn
}

In the example above, ecs_task_definition_task_role_arn and ecs_task_definition_execution_role_arn are variables defined with file-level scope, and can be used across different rules in the rule set.

Rule level

When a variable is initialized within a rule, it is visible only to the particular rule. Any references outside the rule will result in an error.

The example below illustrates the use of a rule scope variable.

Consider the template named Template-1 in the example above. The rule set for the template can be rewritten to use variables having rule level scopes as:

rule check_ecs_task_definition_task_role_arn
{
    let ecs_task_definition_task_role_arn = 'arn:aws:iam::123456789012:role/my-task-role-name'
    Resources.*.Properties.TaskRoleArn == %ecs_task_definition_task_role_arn
}

rule check_ecs_task_definition_execution_role_arn
{
    let ecs_task_definition_execution_role_arn = 'arn:aws:iam::123456789012:role/my-execution-role-name'
    Resources.*.Properties.ExecutionRoleArn == %ecs_task_definition_execution_role_arn
}

In the rule set above, the variables ecs_task_definition_task_role_arn and ecs_task_definition_execution_role_arn have been moved to the individual rules where they are used, changing their scope from being file-level to being rule-level. Variables can now be accessed only in rules within which they have been defined, and not anywhere else.

Block level

When a variable is initialized within a block, such as a when clause, it is only visible to the block. The outer rule or the file is unable to reference this variable.

Consider the template named Template-1 above. The rule set for the template can be rewritten to use variables having block-level scopes as:

rule check_ecs_task_definition_task_role_arn
{
    AWS::ECS::TaskDefinition
    {
        let ecs_task_definition_task_role_arn = 'arn:aws:iam::123456789012:role/my-task-role-name'
        Properties.TaskRoleArn == %ecs_task_definition_task_role_arn
    }
}

rule check_ecs_task_definition_execution_role_arn
{
    AWS::ECS::TaskDefinition
    {
        let ecs_task_definition_execution_role_arn = 'arn:aws:iam::123456789012:role/my-execution-role-name'
        Properties.ExecutionRoleArn == %ecs_task_definition_execution_role_arn
    }
}

In the rule set above, the variables ecs_task_definition_task_role_arn and ecs_task_definition_execution_role_arn have been moved to a type block definition for AWS::ECS::TaskDefinition in their individual rules. Variables will be visible only in their individual type blocks and nowhere outside of them.

Example

Let’s walk through a more complex example of a production use case that allows for users to author Guard rules to ensure stricter controls on how their ECS tasks are defined.

In the example below, you will write rules to ensure that each task definition conforms to the following:

  1. Has a task and execution role attached
  2. Both these roles are IAM roles
  3. Both roles are described in the CloudFormation template
  4. A permission boundary exists for these roles

Sample CloudFormation template:

Resources:
  EcsTask:
    Type: 'AWS::ECS::TaskDefinition'
    Properties:
      TaskRoleArn: 
        'Fn::GetAtt': [TaskIamRole, Arn]
      ExecutionRoleArn:
        'Fn::GetAtt': [ExecutionIamRole, Arn]

  TaskIamRole:
    Type: 'AWS::IAM::Role'
    Properties:
      PermissionsBoundary: 'arn:aws:iam::123456789012:policy/MyExamplePolicy'

  ExecutionIamRole:
    Type: 'AWS::IAM::Role'
    Properties:
      PermissionsBoundary: 'arn:aws:iam::123456789012:policy/MyExamplePolicy'

Sample Guard rule:

# Select as ECS TaskDefinitions from the template
let ecs_tasks = Resources.*[
    Type == 'AWS::ECS::TaskDefinition'
]

# Select a subset of TaskDefinitions whose TaskRoleArn is a Fn::Gett Ref
let task_role_refs = some %ecs_tasks.Properties.TaskRoleArn.'Fn::GetAtt'[0]

# Select a subset of TaskDefinitions whose ExecutionRoleArn is a Fn::Gett Ref
let execution_role_refs = some %ecs_tasks.Properties.ExecutionRoleArn.'Fn::GetAtt'[0]

# Verifies #1 defined requirement
rule all_ecs_tasks_must_have_task_end_execution_roles 
    when %ecs_tasks !empty 
{
    %ecs_tasks.Properties {
        TaskRoleArn exists
        ExecutionRoleArn exists
    }
}

# Verifies requirement #2 and #3
rule all_roles_are_local_and_type_IAM
    when all_ecs_tasks_must_have_task_end_execution_roles
{
    let task_iam_references = Resources.%task_role_refs
    let execution_iam_reference = Resources.%execution_role_refs

    when %task_iam_references !empty {
        %task_iam_references.Type == 'AWS::IAM::Role'
    }

    when %execution_iam_reference !empty {
        %execution_iam_reference.Type == 'AWS::IAM::Role'
    }
}

# Verifies requirement #4
rule check_role_have_permissions_boundary
    when all_ecs_tasks_must_have_task_end_execution_roles
{
    let task_iam_references = Resources.%task_role_refs
    let execution_iam_reference = Resources.%execution_role_refs

    when %task_iam_references !empty {
        %task_iam_references.Properties.PermissionsBoundary exists
    }

    when %execution_iam_reference !empty {
        %execution_iam_reference.Properties.PermissionsBoundary exists
    }
}