As of version 3.0.0 guard now supplies some builtin functions, allowing for stateful rules.
Built-in functions are supported only through assignment to a variable at the moment.
There are some limitations with the current implementation of functions. We do not support inline usage yet. Please read through more about this limitation here.
NOTE: all examples are operating off the following yaml template
Resources:
newServer:
Type: AWS::New::Service
Properties:
Policy: |
{
"Principal": "*",
"Actions": ["s3*", "ec2*"]
}
Arn: arn:aws:newservice:us-west-2:123456789012:Table/extracted
Encoded: This%20string%20will%20be%20URL%20encoded
Collection:
- a
- b
- c
BucketPolicy:
PolicyText: '{"Version":"2012-10-17","Statement":[{"Sid":"DenyReducedReliabilityStorage","Effect":"Deny","Principal":"*","Action":"s3:*","Resource":"arn:aws:s3:::s3-test-123/*","Condition":{"StringEquals":{"s3:x-amz-storage-class-123":["ONEZONE_IA","REDUCED_REDUNDANCY"]}}}]}'
s3:
Type: AWS::S3::Bucket
Properties:
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true
bucket:
Type: AWS::S3::Bucket
Properties:
PublicAccessBlockConfiguration:
BlockPublicAcls: false
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true
SecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
SecurityGroupIngress:
String: "true"
Char: "1"
Int: 1
Float: 1.5
The following functions all operate on queries that resolve to string values
The json_parse
function adds support for parsing inline JSON strings from a given template. After parsing the string into an object,
you can now evaluate certain properties of this struct just like with a normal JSON/YAML object
json_string
: Either be a query that resolves to a string or a string literal. Example,'{"a": "basic", "json": "object"}'
Query of JSON value(s) corresponding to every string literal resolved from input query
The following example shows how you could parse 2 fields on the above template and then write clauses on the results:
let template = Resources.*[ Type == 'AWS::New::Service']
let expected = {
"Principal": "*",
"Actions": ["s3*", "ec2*"]
}
rule TEST_JSON_PARSE when %template !empty {
let policy = %template.Properties.Policy
let res = json_parse(%policy)
%res !empty
%res == %expected
<<
Violation: the IAM policy does not match with the recommended policy
>>
let policy_text = %template.BucketPolicy.PolicyText
let res2 = json_parse(%policy_text)
%res2.Statement[*]
{
Effect == "Deny"
Resource == "arn:aws:s3:::s3-test-123/*"
}
}
The regex_replace
function adds support for replacing one regular expression with another
base_string
: A query, each string that is resolved from this query will be operated on. Example,%s3_resource.Properties.BucketName
regex_to_extract
: A regular expression that we are looking for to extract from thebase_string
- Note: if this string does not resolve to a valid regular expression an error will occur
regex_replacement
A regular expression that will replace the part we extracted, also supports capture groups
A query where each string from the input has gone through the replacements
In this simple example, we will re-format an ARN by moving around some sections in it.
We will start with a normal ARN that has the following pattern: arn:<Partition>:<Service>:<Region>:<AccountID>:<ResourceType>/<ResourceID>
and we will try to convert it to: <Partition>/<AccountID>/<Region>/<Service>-<ResourceType>/<ResourceID>
let template = Resources.*[ Type == 'AWS::New::Service']
rule TEST_REGEX_REPLACE when %template !empty {
%template.Properties.Arn exists
let arn = %template.Properties.Arn
let arn_partition_regex = "^arn:(\w+):(\w+):([\w0-9-]+):(\d+):(.+)$"
let capture_group_reordering = "${1}/${4}/${3}/${2}-${5}"
let res = regex_replace(%arn, %arn_partition_regex, %capture_group_reordering)
%res == "aws/123456789012/us-west-2/newservice-Table/extracted"
<< Violation: Resulting reformatted ARN does not match the expected format >>
}
The join
function adds support to collect a query, and then join their values using the provided delimiter.
collection
: A query, all string values resolved from this query are candidates of elements to be joineddelimiter
: A query or a literal value that resolves to a string or character to be used as delimiter
Query where each string that was resolved from the input is joined with the provided delimiter
The following example queries the template for a Collection field on a given resource, it then provides a join on ONLY the string values that this query resolves to with a ,
delimiter
let template = Resources.*[ Type == 'AWS::New::Service']
rule TEST_COLLECTION when %template !empty {
let collection = %template.Collection.*
let res = join(%collection, ",")
%res == "a,b,c"
<< Violation: The joined value does not match the expected result >>
}
This function can be used to change the casing of the all characters in the string passed to all lowercase.
base_string
: A query that resolves to string(s)
Returns the base_string
in all lowercase
let type = Resources.newServer.Type
rule STRING_MANIPULATION when %type !empty {
let lower = to_lower(%type)
%lower == /aws::new::service/
<< Violation: expected a value to be all lowercase >>
}
This function can be used to change the casing of the all characters in the string passed to all uppercase.
base_string
: A query that resolves to string(s)
Returns capitalized version of the base_string
let type = Resources.newServer.Type
rule STRING_MANIPULATION when %type !empty {
let upper = to_upper(%type)
%upper == "AWS::NEW::SERVICE"
<< Violation: expected a value to be all uppercase >>
}
The substring
function allows to extract a part of string(s) resolved from a query
base_string
: A query that resolves to string(s)start_index
: A query that resolves to an int or a literal int, this is the starting index for the substring (inclusive)end_index
: A query that resolves to an int or a literal int, this is the ending index for the substring (exclusive)
A result of substrings for each base_string
passed as input
- Note: Any string that would result in an index out of bounds from the 2nd or 3rd argument is skipped
let template = Resources.*[ Type == 'AWS::New::Service']
rule TEST_SUBSTRING when %template !empty {
%template.Properties.Arn exists
let arn = %template.Properties.Arn
let res = substring(%arn, 0, 3)
%res == "arn"
<< Violation: Substring extracted does not match with the expected outcome >>
}
This function can be used to transform URL encoded strings into their decoded versions
base_string
: A query that resolves to a string or a string literal
A query containing URL decoded version of every string value from base_string
The following rule shows how you could url_decode
the string This%20string%20will%20be%20URL%20encoded
let template = Resources.*[ Type == 'AWS::New::Service']
rule SOME_RULE when %template !empty {
%template.Properties.Encoded exists
let encoded = %template.Properties.Encoded
let res = url_decode(%encoded)
%res == "This string will be URL encoded"
<<
Violation: The result of URL decoding does not
match with the expected outcome
>>
}
This function can be used to count the number of items that a query resolves to
collection
: A query that can resolves to any type
The number of resolved values from collection
is returned as the result
The following rules show different ways we can use the count function.
- One queries a struct, and counts the number of properties.
- The second queries a list object, and counts the elements in the list
- The third queries for all resources that are s3 buckets and have a PublicAccessBlockConfiguration property
let template = Resources.*[ Type == 'AWS::New::Service' ]
rule SOME_RULE when %template !empty {
let props = %template.Properties.*
let res = count(%props)
%res >= 3
<< Violation: There must be at least 3 properties set for this service >>
let collection = %template.Collection.*
let res2 = count(%collection)
%res2 >= 3
<< Violation: Collection should contain at least 3 items >>
let buckets = Resources.*[ Type == 'AWS::S3::Bucket' ]
let b = %buckets[ Properties.PublicAccessBlockConfiguration exists ]
let res3 = count(%b)
%res3 >= 2
<< Violation: At least 2 buckets should have PublicAccessBlockConfiguration set >>
}
It's important to note that if the the argument passed to any of the converter functions is a list, any element in the list that is of a type not supported for the conversion function, is skipped and left out of the final result.
This means the resulting list would contain N - M elements where N is the number of items in the original list, and M are the number of elements whose type were not supported for this conversion
This function converts strings, ints, and chars to floating numbers
NOTE: Strings that cannot be represented as a number will cause this function to error
args
: A query that resolves to either a literal or variable
let security_group = Resources.*[ Type == "AWS::EC2::SecurityGroup" ]
rule check when %security_group !EMPTY {
let converted = parse_float(%security_group.Properties.Int)
1.0 == %converted
}
This function converts strings, floats, and chars to integers
NOTE: floating point numbers are truncated to their integer representation, chars/strings that cannot be represented as an int will cause this function to error
args
: A query that resolves to either a literal or variable
let security_group = Resources.*[ Type == "AWS::EC2::SecurityGroup" ]
rule check when %security_group !EMPTY {
let converted = parse_int(%security_group.Properties.Float)
1 == %converted
}
This function converts strings to their boolean equivalent
NOTE: this function is not case sensitive meaning tRuE
, true
, TRUE
, True
, etc will all be successfully converted
args
: A query that resolves to either a literal or variable
let security_group = Resources.*[ Type == "AWS::EC2::SecurityGroup" ]
rule check when %security_group !EMPTY {
let converted = parse_boolean(%security_group.Properties.String)
%converted == true
}
This function converts floats, booleans, chars, and ints to their String equivalents
args
: A query that resolves to either a literal or variable
let security_group = Resources.*[ Type == "AWS::EC2::SecurityGroup" ]
rule check when %security_group !EMPTY {
let converted = parse_string(%security_group.Properties.Float)
%converted == "1.5"
}
This function converts strings, and ints to their char equivalents
NOTE: this function will cause an error if the int is not 0 <= n <= 9, it will also error out if a string has a length > 1
args
: A query that resolves to either a literal or variable
let security_group = Resources.*[ Type == "AWS::EC2::SecurityGroup" ]
rule check when %security_group !EMPTY {
let converted = parse_char(%security_group.Properties.Char)
%converted == '1'
}