Skip to content

Latest commit

 

History

History
409 lines (306 loc) · 14.2 KB

CLAUSES.md

File metadata and controls

409 lines (306 loc) · 14.2 KB

Guard: Clauses

Clauses are the foundational underpinning of Guard rules. Clauses are boolean statements which evaluate to a true (PASS)/ false (FAIL) and take the following format:

  <query> <operator> [query|value literal] 

You must specify a query and an operator in the clause section:

  • query in its most simple form is a decimal dot (.) formatted expressions written to traverse hierarchical data. More information on this section of the clause can be found in the Guard: Query and Filtering document.

  • operator can use unary or binary operators. Both of these operators will be discussed in-depth later in this document:

    • Unary Operators: exists, empty, is_string, is_list, is_struct, not(!)
    • Binary Operators: ==, !=, >, >=, <, <=, IN

The query|value literal section of the clause is optional:

  • query|value literal the third section of a clause is needed when a binary operator is used. It can be a query as defined above or it can be any supported value literal like string, integer(64), etc.

Below are some examples for clauses:

  • Clauses using unary operator:
# The collection TcpBlockedPorts cannot be empty
InputParameters.TcpBlockedPorts not empty  
# ExecutionRoleArn must be a string
Properties.ExecutionRoleArn is_string
  • Clauses using binary operator:
# BucketName must not contain the string "encrypt" irrespective of casing
Properties.BucketName != /(?i)encrypted/
# ReadCapacityUnits must be less than or equal to 5000
Properties.ProvisionedThroughtput.ReadCapacityUnits <= 5000

Operators

Let us look at Guard supported operators in-depth. Information on each operator will be accompanied by examples based on the following example CloudFormation template to help you understand the various operators. All examples in this section will be based on the following template:

# Template-1
Resources:
  S3Bucket:
    Type: "AWS::S3::Bucket"
    Properties:
      BucketName: "MyServiceS3Bucket"
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: 'aws:kms'
              KMSMasterKeyID: 'arn:aws:kms:us-east-1:123456789:key/056ea50b-1013-3907-8617-c93e474e400'
      Tags:
        - Key: "stage"
          Value: "prod"
        - Key: "service"
          Value: "myService"

Unary Operators

empty and exists operators

Guard introduces empty and exists operators to assist authors verify states of the query in a clause. You can use the not(!) operator with both these operators to check the inverse state.

empty- Checks if a collection is empty. It can also be used to check if a query has values in a hierarchical data as all queries result in a collection, more about queries is mentioned in the Guard: Query and Filtering document.

# Checks if the template has resources defined
Resources !empty
# Checks if one or more tags defined
Resources.S3Bucket.Properties.Tags !empty

The above two clauses will PASS for the example template.

  • The first clause checks to see if the template has resources defined under Resources , and the template has one resource with S3Bucket as the logical resource ID.
  • The second clause checks to see if one or more tags are defined in S3Bucket. It has two tags defined under Tags.

empty can’t be used to check if string value queries have an empty string ("") defined.

exists - Checks if each occurrence of the query has a value and can be used in place of != null.

# Checks if BucketEncryption is defined
Resources.S3Bucket.Properties.BucketEncryption exists

The above clause will PASS for the example template as BucketEncryption is defined for S3Bucket.

IMPORTANT: empty and not exists checks evaluate to true for missing property keys when traversing the input data. E.g. if we check Resources.S3Bucket.Properties.Tags empty if Properties was not present in the template for S3Bucket, then empty evaluates to true.

is_string, is_list, and is_struct operators

is_string - Checks if each occurrence of the query is of string type.

# Checks if BucketName is defined as a string
Resources.S3Bucket.Properties.BucketName is_string

is_list - Checks if each occurrence of the query is of list type.

# Checks if Tags is defined as a list
Resources.S3Bucket.Properties.Tags is_list

is_struct - Checks if each occurrence of the query is a structured data.

# Checks if BucketEncryption is defined as a structured data
Resources.S3Bucket.Properties.BucketEncryption is_struct

All the above example clauses will PASS for the example template. You can use the not(!) operator with the above operators to check the inverse state.

Binary Operators

Below are binary operators supported in Guard. The left-hand side (LHS) to the operator as mentioned above must always be a query and the right-hand side (RHS) can be a query or a value literal:

  ==    Equal
  !=    Not Equal
  >     Greater Than
  >=    Greater Than Or Equal To
  <     Less Than
  <=    Less Than Or Equal To
  IN    In a list of form [x, y, z]

A value literal can be from any of the following supported categories,

  • all primitives string, integer(64), float(64), bool, char, regex
  • a specialized range type for expressing integer, float, char ranges, expressed as,
    • r[<lower_limit>, <upper_limit>], which translates to any value k that satisfies the following expression: lower_limit <= k <= upper_limit
    • r[<lower_limit>, <upper_limit>), which translates to any value k that satisfies the following expression: lower_limit <= k < upper_limit
    • r(<lower_limit>, <upper_limit>], which translates to any value k that satisfies the following expression: lower_limit < k <= upper_limit
    • r(<lower_limit>, <upper_limit>), which translates to any value k that satisfies the following expression: lower_limit < k < upper_limit
  • associative arrays (a.k.a map) for nested key value structured data like:
{ "my-map": { "nested-maps": [ { "key": 10, "value": 20 } ] } }
  • arrays of primitive/associative array types

Below are a couple of examples of clauses using binary operators:

  • Based on the Template-1 example template:
# Checks if BucketName does not contain the string "encrypt" irrespective of casing
Resources.S3Bucket.Properties.BucketName != /(?i)encrypt/
  • Consider the following CloudFormation template:
# Template-2
Resources:
  NewVolume:
    Type: AWS::EC2::Volume
    Properties: 
      Size: 100
      VolumeType: io1
      Iops: 100
      AvailabilityZone:
        Fn::Select:
          - 0
          - Fn::GetAZs: us-east-1
      Tags:
        - Key: environment
          Value: test
    DeletionPolicy: Snapshot

Below are a few Guard clauses based on the above template:

# Checks Size of the EC2 volume is within a specific range, 50<= Size <= 200
Resources.NewVolume.Properties.Size IN r[50,200]
# Checks VolumeType is one of io1, io2 or gp3
Resources.NewVolume.Properties.VolumeType IN [ 'io1','io2','gp3' ]

While these examples illustrate using S3Bucket, NewVolume in the query, often these are user defined and can be arbitrarily named in an IaC template. To write a rule that is generic and applies to all AWS::S3::Bucket resources defined in the template the most common form of query used is Resources.*[ Type == ‘AWS::S3::Bucket’ ] to select them. See Guard: Query and Filtering for details on usage and explore the examples directory.

Custom Message

You can add a custom message to a clause. A custom message is added at the end of a clause as follows:

  <query> <operator> [query|value literal] [custom message]

custom message is expressed as <<message>> where message is any string which ideally provides information regarding the clause preceding it. This message is displayed in the verbose outputs of the validate (provide links) and test (provide links) commands and can be used to supply information helpful for understanding/debugging rules file evaluation on hierarchical data. The Template-2 example template clauses with custom message will look as follows:

Resources.NewVolume.Properties.Size IN r[50,200] 
<<
    EC2Volume size must be between 50 and 200, 
    not including 50 and 200
>>
Resources.NewVolume.Properties.VolumeType IN [ 'io1','io2','gp3' ] <<Allowed Volume Types are io1, io2, and gp3>>

Combining Clauses

Now that we have a complete picture of what constitutes a clause, let us learn to combine clauses. In Guard, each clause written on a new line is combined implicitly with the next clause using conjunction (boolean and logic):

# clause_A ^ clause_B ^ clause_C
clause_A
clause_B
clause_C

You can also combine a clause with the next clause using disjunction by specifying or|OR at the end of the first clause.

  <query> <operator> [query|value literal] [custom message] [or|OR]

Disjunctions are evaluated first followed by conjunctions, and hence Guard rules can be defined as a conjunction of disjunction of clauses that evaluate to a true (PASS) / false (FAIL). This can be best explained with a few examples:

# (clause_E v clause_F) ^ clause_G
clause_E OR clause_F
clause_G

# (clause_H v clause_I) ^ (clause_J v clause_K)
clause_H OR
clause_I
clause_J OR
clause_K

# (clause_L v clause_M v clause_N) ^ clause_O
clause_L OR
clause_M OR
clause_N 
clause_O

This is similar to the Conjunctive Normal Form (CNF).

All clauses written based on the Template-1 example template can be combined as follows:

Resources.S3Bucket.Properties.BucketName is_string
Resources.S3Bucket.Properties.BucketName != /(?i)encrypt/
Resources.S3Bucket.Properties.BucketEncryption exists
Resources.S3Bucket.Properties.BucketEncryption is_struct
Resources.S3Bucket.Properties.Tags is_list
Resources.S3Bucket.Properties.Tags !empty

All clauses above are combined using conjunction. As you can see, there is repetition of part of the query expression in every clause. You can improve composability and remove verbosity and repetition from a set of related clauses with the same initial query path using a query block.

Blocks

Query blocks

The above set of clauses can be written in block format as follows:

Resources.S3Bucket.Properties {
    BucketName is_string
    BucketName != /(?i)encrypt/
    BucketEncryption exists
    BucketEncryption is_struct
    Tags is_list
    Tags !empty
}

The above composition is referred to as query blocks as the query preceding the block sets the context for clauses inside the block. This improves composability and removes verbosity and repetition while writing multiple related clauses with the same initial query path.

When blocks - When condition for conditional evaluation

Blocks can be evaluated conditionally using when blocks; when blocks take the following form:

  when <condition> {
      Guard_rule_1
      Guard_rule_2
      ...
  }
  • The when keyword designates the start of the when block.

  • condition can be any Guard rule. The block is evaluated only if the evaluation of the condition results in true (PASS).

Below is an example using the Template-1 example template:

when Resources.S3Bucket.Properties.BucketName is_string {
    Resources.S3Bucket.Properties.BucketName != /(?i)encrypt/
}

The clause within the when block will only execute if BucketName is a string. If the value for BucketName was being referenced from a Parameter as shown below, the clause within the when block will not be executed.

Parameters:
  S3BucketName:
    Type: String

Resources:
  S3Bucket:
    Type: "AWS::S3::Bucket"
    Properties:
      BucketName: 
        Ref: S3BucketName
    ...

Named rule blocks

Named rule blocks allow for re-usability, improved composition and remove verbosity and repetition. They take the following form:

  rule <rule name> [when <condition>] {
      Guard_rule_1
      Guard_rule_2
      ...
  }
  • The rule keyword designates the start of a named rule block.

  • rule name can be any string that is human-readable and ideally should uniquely identify a named rule block. It can be thought of as a label to the set of Guard rules it encapsulates where Guard rules is an umbrella term for clauses, query blocks, when blocks and named rule blocks. The rule name can be used to refer to the evaluation result of the set of Guard rules it encapsulates, this is what makes named rule blocks re-usable. It also helps provide context in the validate (provide links) and test (provide links) command output as to what exactly failed. The rule name is displayed along with it block’s evaluation status - PASS, FAIL, or SKIP, in the evaluation output of the rules file:

# Sample output of an evaluation where check1, check2, and check3 are rule names.
_Summary__ __Report_ Overall File Status = **FAIL**
**PASS/****SKIP** **rules**
check1 **SKIP**
check2 **PASS**
**FAILED rules**
check3 **FAIL**
  • Named rule blocks can also be evaluated conditionally by specifying the when keyword followed with a condition after the rule name.

Reiterating the when block example below:

rule checkBucketNameStringValue when Resources.S3Bucket.Properties.BucketName is_string {
    Resources.S3Bucket.Properties.BucketName != /(?i)encrypt/
}

# The above can also be written as follows
rule checkBucketNameIsString {
    Resources.S3Bucket.Properties.BucketName is_string
}
rule checkBucketNameStringValue when checkBucketNameIsString {
    Resources.S3Bucket.Properties.BucketName != /(?i)encrypt/
}

Named rule blocks can be re-used and grouped together with other Guard rules. Below are a few examples:

rule rule_name_A {
    Guard_rule_1 OR
    Guard_rule_2
    ...
}

rule rule_name_B {
    Guard_rule_3
    Guard_rule_4
    ...
}

rule rule_name_C {
    rule_name_A OR rule_name_B
}

rule rule_name_D {
    rule_name_A
    rule_name_B
}

rule rule_name_E when rule_name_D {
    Guard_rule_5
    Guard_rule_6
    ...
}

The above styles of compositions will be discussed in-depth in the Guard: Complex Composition document.