Custom Checks
What is it?
tfsec comes with an ever growing number of built in checks, these cover standard AWS, Azure and GCP provider checks, plus several more.
We recognise that there are checks that need performing for an organisation that don't fit with general use cases; for this, there are custom checks.
Custom checks offer an accessible approach to injecting checks that satisfy your organisations compliance and security needs. For example, if you require that all EC2 instances have a CostCentre
tag, that can be achieved with a custom_check
.
How does it work?
Custom checks are defined as json files which sit in the .tfsec
folder in the root check path. any file with the suffix _tfchecks.json
or _tfchecks.yaml
will be parsed and the checks included during the run.
Overriding check directory
The default location for custom checks can be overridden, this is done using the --custom-check-dir
to specify another location to load the checks from instead.
This is useful when global checks are to applied to the terraform under test.
Downloading remote check file
A custom check file can be downloaded from remote locations using the --custom-check-url
. This must be an HTTP location to a file with either a json
or yaml
extension
tfsec --custom-check-url https://github.com/myorg/tfsecconfig/custom_tfchecks.json .
What does a check file look like?
Check files are simply json, this ensures that checks can be put together without requiring Go knowledge or being able to build a new release of tfsec to include your custom code.
Taking the previous example of a required cost centre, the check file might look something like
{
"checks": [
{
"code": "CUS001",
"description": "Custom check to ensure the CostCentre tag is applied to EC2 instances",
"impact": "By not having CostCentre we can't keep track of billing",
"resolution": "Add the CostCentre tag",
"requiredTypes": [
"resource"
],
"requiredLabels": [
"aws_instance"
],
"severity": "ERROR",
"matchSpec": {
"name": "tags",
"action": "contains",
"value": "CostCentre"
},
"errorMessage": "The required CostCentre tag was missing",
"relatedLinks": [
"http://internal.acmecorp.com/standards/aws/tagging.html"
]
}
]
}
or
---
checks:
- code: CUS001
description: Custom check to ensure the CostCentre tag is applied to EC2 instances
impact: By not having CostCentre we can't keep track of billing
resolution: Add the CostCentre tag
requiredTypes:
- resource
requiredLabels:
- aws_instance
severity: ERROR
matchSpec:
name: tags
action: contains
value: CostCentre
errorMessage: The required CostCentre tag was missing
relatedLinks:
- http://internal.acmecorp.com/standards/aws/tagging.html
Using go run ./cmd/tfsec-checkgen generate
, you can generate a skeleton custom check file.
The check contains up of the following attributes;
Attribute | Description |
---|---|
code | The custom code that your check will be known as |
description | A description for the code that will be included in the output |
impact | An optional detail about the consequences of not passing the check |
resolution | An optional brief description of how to satisfy the check |
requiredTypes | The block types to apply the check to - resource, data, module, variable |
requiredLabels | The resource type - aws_ec2_instance for example. This also supports wildcards using * , e.g. aws_* |
severity | How severe is the check |
matchSpec | See below for the MatchSpec attributes |
errorMessage | The error message that should be displayed in cases where the check fails |
relatedLinks | A list of related links for the check to be displayed in cases where the check fails |
The MatchSpec
is the what will define the check itself - this is fairly basic and is made up of the following attributes
Attribute | Description |
---|---|
name | The name of the attribute or block to run the check on |
action | The check type - see below for more information |
value | In cases where a value is required, the value to look for, text matching TFSEC_VAR_{VAR_NAME} will be replaced with the variable value |
ignoreUndefined | If the attribute is undefined, ignore and pass the check |
preConditions | An array of checks, performs the check action defined in action if all preConditions checks passes, passes the whole matchSpec if preConditions are not satisfied |
subMatch | A sub MatchSpec block for nested checking - think looking for enabled value in a logging block, or checking a tag's value in a tag map attribute |
subMatchOne | Same as subMatch, but looks for only exactly 1 match in nested checks, cannot be applied on subMatches for attributes |
predicateMatchSpec | An array of MatchSpec blocks to be logically aggregated by either and or or actions |
assignVariable | The name of the "variable" to store the value of the name attribute in, has to be in uppercase and start with TFSEC_VAR_ |
Check Actions
There are a number of CheckActions
available which should allow you to quickly put together most checks.
inModule
The inModule
check action passes if the resource block is a component of a module. For example, if you're looking to check that an aws_s3_bucket
is only created using a custom module, you could use the following MatchSpec
;
"matchSpec" : {
"action": "inModule"
}
matchSpec:
action: inModule
isPresent
The isPresent
check action passes if the required block or attribute is available in the checked block. For example, if you're looking to check that an acl
is provided and don't care what it is, you can use the following MatchSpec
;
"matchSpec" : {
"name": "acl",
"action": "isPresent"
}
matchSpec:
name: acl
action: isPresent
notPresent
Conversely, the noPresent
check action passes if the specified block or attribute is not found in the checked block. For example, if you explicitly don't want an acl
attribute to be present hou can use the following MatchSpec
"matchSpec" : {
"name": "acl",
"action": "notPresent"
}
matchSpec:
name: acl
action: notPresent
isEmpty
The isEmpty
check action passes if the named block or attribute is defined by empty.
For example, to check that there are not tags you might use the following MatchSpec
"matchSpec" : {
"name": "tags",
"action": "isEmpty",
}
matchSpec:
name: acl
action: isEmpty
startsWith
The startsWith
check action passes if the checked attribute string starts with the specified value. For example, to check that acl
begins with public
you could use the following MatchSpec
"matchSpec" : {
"name": "acl",
"action": "startsWith",
"value": "public"
}
matchSpec:
name: acl
action: startsWith
value: public
endsWith
The endsWith
check action passes if the checked attribute string ends with the specified value. For example, to check that acl
ends with read
you could use the following MatchSpec
;
"matchSpec" : {
"name": "acl",
"action": "endsWith",
"value": "-read"
}
matchSpec:
name: acl
action: endsWith
value: -read
contains
The contains
check action will change depending on the attribute or block it is applied to. If the check is against a string attribute, it will look for the MatchSpec
value in the attribute. If the check is against a list, it will pass if the value item can be found in the list.
If the attribute is an object
or map
it will pass if a key can be found that matches the MatchSpec
value.
For example, if you want to ensure that the CostCentre
exists, you might use the following MatchSpec
;
"matchSpec" : {
"name": "tags",
"action": "contains",
"value": "CostCentre"
}
matchSpec:
name: tags
action: contains
value: CostCentre
notContains
The notContains
check action will change depending on the attribute or block it is applied to. If the check is against a string attribute, it will look for the MatchSpec
value in the attribute. If the check is against a list, it will pass if the value item can be found in the list.
If the attribute is an object
or map
it will pass if a key can be found that matches the MatchSpec
value.
For example, you want to make sure that an action
does not contain kms:*
you might use the following MatchSpec
:
"matchSpec" : {
"name": "action",
"action": "notContains",
"value": "kms:*"
}
matchSpec:
name: tags
action: notContains
value: kms:*
equals
The equals
check action passes if the checked attribute equals specified value.
The core primitive types are supported, if the subject attribute is a Boolean, the MatchSpec
value will attempt to be cast to a Boolean for comparison.
For example, to check that acl
begins with private
you could use the following MatchSpec
;
"matchSpec" : {
"name": "acl",
"action": "equals",
"value": "private"
}
matchSpec:
name: acl
action: equals
value: private
lessThan
The lessThan
check action passes if the checked attribute is numerical and the value is less than the specified value.
For example, if you want to ensure that the cpu_core_count
is less than 8, you might use the following MatchSpec
"matchSpec" : {
"name": "cpu_core_count",
"action": "lessThan",
"value": 8
}
matchSpec:
name: cpu_core_count
action: lessThan
value: 8
lessThanOrEqualTo
The lessThanOrEqualTo
check action passes if the checked attribute is numerical and the value is less than or equal to the specified value.
For example, if you want to ensure that the cpu_core_count
is less than or equal to 4, you might use the following MatchSpec
"matchSpec" : {
"name": "cpu_core_count",
"action": "lessThanOrEqualTo",
"value": 4
}
matchSpec:
name: cpu_core_count
action: lessThanOrEqualTo
value: 4
greaterThan
The greaterThan
check action passes if the checked attribute is numerical and the value is greater than the specified value.
For example, if you want to ensure that the cpu_core_count
is greater than 2, you might use the following MatchSpec
"matchSpec" : {
"name": "cpu_core_count",
"action": "greaterThan",
"value": 2
}
matchSpec:
name: cpu_core_count
action: greaterThan
value: 2
greaterThanOrEqualTo
The greaterThanOrEqualTo
check action passes if the checked attribute is numerical and the value is greater than or equal to the specified value.
For example, if you want to ensure that the cpu_core_count
is greater than or equal to 4, you might use the following MatchSpec
"matchSpec" : {
"name": "cpu_core_count",
"action": "greaterThanOrEqualTo",
"value": 4
}
matchSpec:
name: cpu_core_count
action: greaterThanOrEqualTo
value: 4
regexMatches
The regexMatches
check action passes when the regex is matched to the pattern passed in the value. This is check would generally be used as a top level check to filter whether or not to apply a check.
For example, this check will ensure that the source attribute of a module matches the supplied regex before continuing with the subMatches. This can be used to ensure that checks are targeted to specific modules.
When tackling this specific use case of filtering module blocks by source, the requiredLabels
should be set to "*"
"matchSpec": {
"name": "source",
"action": "regexMatches",
"value": "^modules\\/.*public_.+bucket$",
"subMatch": {
"name": "acl",
"action": "equals",
"value": "public-read"
}
}
matchSpec:
name: source
action: regexMatches
value: "^modules\\/.*public_.+bucket$"
subMatch:
name: acl
action: equals
value: public-read
isAny
The isAny
check action passes when the attribute value can be found in the slice passed as the check value. This check action supports strings and numbers
"matchSpec": {
"name": "acl",
"action": "isAny",
"value": ["private", "log-delivery-write"]
}
matchSpec:
name: acl
action: isAny
value:
- private
- log-delivery-write
isNone
The isNone
check action passes when the attribute value cannot be found in the slice passed as the check value. This check action supports strings and numbers
"matchSpec": {
"name": "acl",
"action": "isNone",
"value": ["authenticated-read", "public-read"]
}
matchSpec:
name: acl
action: isNone
value:
- authenticated-read
- public-read
requiresPresence
The requiresPresence
checks that the resource in name
is also present in the Terraform code.
If you wanted to ensure that aws_vpc_flowlogs
is present if there is a aws_vpc
, you might use the following matchSpec
:
"matchSpec" : {
"action": "requiresPresence",
"name": "aws_vpc_flowlogs"
}
matchSpec:
name: aws_vpc_flowlogs
action: requiresPresence
and
The and
check action passes when all the blocks provided within predicateMatchSpec
evaluate to true
. This action
can be combined with subMatch
to perform composite checks against the contents of nested blocks.
Note that or
and and
actions can be nested as many times as needed.
If you wanted to ensure that device_name
and encrypted
are both present in a nested ebs_block_device
block,
you might use the following matchSpec
:
"matchSpec": {
"action": "isPresent",
"name": "ebs_block_device",
"subMatch": {
"action": "and",
"predicateMatchSpec": [
{
"action": "isPresent",
"name": "device_name"
},
{
"action": "isPresent",
"name": "encrypted"
}
]
}
}
matchSpec:
action: isPresent
name: ebs_block_device
subMatch :
action : and
predicateMatchSpec :
- action : isPresent
name : device_name
- action : isPresent
name : encrypted
or
The or
check action passes when at least one of the blocks provided within predicateMatchSpec
evaluates to true
.
This action can be combined with subMatch
to perform composite checks against the contents of nested blocks.
Note that or
and and
actions can be nested as many times as needed.
If you want to ensure that virtualization_type
is assigned to either hvm
or paravirtual
, while enforcing
that their implicitly linked required attributes are also present, you might use the following matchSpec
:
"matchSpec": {
"action": "or",
"predicateMatchSpec": [
{
"action": "and",
"predicateMatchSpec": [
{
"name": "virtualization_type",
"action": "equals",
"value": "hvm"
}
]
},
{
"action": "and",
"predicateMatchSpec": [
{
"name": "virtualization_type",
"action": "equals",
"value": "paravirtual"
},
{
"name": "image_location",
"action": "isPresent"
},
{
"name": "kernel_id",
"action": "isPresent"
}
]
}
]
}
matchSpec:
action: or
predicateMatchSpec:
- action: and
predicateMatchSpec:
- name: virtualization_type
action: equals
value: hvm
- name: sriov_net_support
action: isPresent
- action: and
predicateMatchSpec:
- name: virtualization_type
action: equals
value: paravirtual
- name: image_location
action: isPresent
- name: kernel_id
action: isPresent
not
The not
check action passes when the predicateMatchSpec
evaluates to false
.
As an example, if you want to represent that a resource
should not be included inModule
you might use the following matchSpec
:
"matchSpec": {
"action": "not",
"predicateMatchSpec": [
{
"action": "inModule"
}
]
}
matchSpec:
action: not
predicateMatchSpec:
- action: inModule
How do I know my JSON is valid?
We have provided the tfsec-checkgen
binary which will validate your check file or help perform tests to ensure that it is valid for use with tfsec
.
tfsec-checkgen validate
Validates the syntax of a custom check.
```shell script go run ./cmd/tfsec-checkgen validate example/custom/.tfsec/custom_checks.json
### `tfsec-checkgen test-check`
Tests custom check against provided test cases. You can pass in multiple `--fail`/`-f`/`--pass`/`-p` flags to perform multiple tests at once on the same custom check.
```shell script
go run ./cmd/tfsec-checkgen test-check ./example/cmd_checkgen_test-check/.tfsec/example_tfchecks.json \
--fail ./example/cmd_checkgen_test-check/fail.tf \
--pass ./example/cmd_checkgen_test-check/pass.tf
Alternatively, you can install the tfsec-checkgen from the releases page
Are there limitations?
At the moment, check MatchSpec
is limited in the number of check types it can perform, these are as shown in the previous table.
Custom defined checks also don't come with the comprehensive tests that the built in ones have. This will be addressed in future releases.