The CloudGraph rules engine validates compliance checks for the various benchmarks we currently support. It is responsible for executing each rule using various operators that allow for the building of the required conditions necessary to validate. If you are interested in writing your own compliance checks (or any kind of. checks for that matter) then this section is for you.
Basic Operators
daysAgo
Calculates how many days have passed by giving a specific date. Returns an integer that represents the number of days between the provided date and the current one.
Example
In the following rule, you can see how to use the daysAgo operator. In this case, passwordLastUsed is a valid date that will be converted to days before comparing with another operator to match a result.
JS
|
exportdefault{id:'rule-id',description:'Password should have been used in the last 90 days',gql:`{
queryawsIamUser {
id
__typename
passwordLastUsed
}
}`,resource:'queryawsIamUser[*]',severity:'warning',conditions:{value:{daysAgo:{},path:'@.passwordLastUsed'},lessThanInclusive:90,},}
daysDiff
Calculates the difference in days between a given date and today. Returns an integer that represents the number of days between the provided date and the current one.
Example
In the following rule, you can see how to use the daysDiff operator. In this case, nextRotationTimeisa valid date that will be converted to days before comparing with another operator to match a result.
JS
|
exportdefault{id:'rule-id',title:'Ensure nextRotationTime is <= 90 days from current date',gql:`{
querygcpKmsKeyRing {
id
__typename
kmsCryptoKeys {
nextRotationTime
}
}
}`,resource:'querygcpKmsKeyRing[*]',severity:'unknown',conditions:{path:'@.kmsCryptoKeys',array_any:{value:{daysDiff:{},path:'[*].nextRotationTime'},lessThanInclusive:90,},},}
equal
Compares that two elements are equal. Returns a boolean that determines when the comparison matches or not.
Example
The next rule shows how to use the equal operator. Notice that the mfaActive field is a boolean.
JS
|
exportdefault{id:'rule-id',description:"Ensure MFA is enabled for the 'root' account",gql:`{
queryawsIamUser(filter: { name: { eq: "root" } }) {
id
__typename
name
mfaActive
}
}`,resource:'queryawsIamUser[*]',severity:'danger',conditions:{path:'@.mfaActive',equal:true,},}
notEqual
Compares that two elements aren't equal. Returns a boolean that determines when the comparison matches or not.
Example
The next rule shows how to use the notEqual operator. Notice that the mfaActive field is a boolean.
JS
|
exportdefault{id:'rule-id',description:"Ensure MFA is disabled for the 'root' account",gql:`{
queryawsIamUser(filter: { name: { eq: "root" } }) {
id
__typename
name
mfaActive
}
}`,resource:'queryawsIamUser[*]',severity:'danger',conditions:{path:'@.mfaActive',notEqual:true,},}
lessThan
Indicates when the provided data is less than the value to compare. Returns a boolean that represents when the condition matches or not.
Example
The following rule indicates that the field maxPasswordAge should be less than 30.
JS
|
exportdefault{id:'rule-id',description:'Ensure IAM password policy expires passwords within 90 days or less',gql:`{
queryawsIamPasswordPolicy {
id
__typename
maxPasswordAge
}
}`,resource:'queryawsIamPasswordPolicy[*]',severity:'warning',conditions:{path:'@.maxPasswordAge',lessThan:30,},}
lessThanInclusive
Indicates when the provided data is equal or less than the value to compare. Returns a boolean that represents when the condition matches or not.
Example
The following rule indicates that the field maxPasswordAge should be less than or equal to 90.
JS
|
exportdefault{id:'rule-id',description:'Ensure IAM password policy expires passwords within 90 days or less',gql:`{
queryawsIamPasswordPolicy {
id
__typename
maxPasswordAge
}
}`,resource:'queryawsIamPasswordPolicy[*]',severity:'warning',conditions:{path:'@.maxPasswordAge',lessThanInclusive:90,},}
greaterThan
Indicates when the provided data is greater than the value to compare. Returns a boolean that represents when the condition matches or not.
Example
The following rule indicates that the field minimumPasswordLength should be greater than 24.
JS
|
exportdefault{id:'rule-id',description:'Ensure IAM password policy prevents password reuse',gql:`{
queryawsIamPasswordPolicy {
id
__typename
passwordReusePrevention
}
}`,resource:'queryawsIamPasswordPolicy[*]',severity:'warning',conditions:{path:'@.passwordReusePrevention',greaterThan:24,},}
greaterThanInclusive
Indicates when the provided data is equal or greater than the value to compare. Returns a boolean that represents when the condition matches or not.
Example
The following rule indicates that the field minimumPasswordLength should be equal to or greater than 14.
JS
|
exportdefault{id:'rule-id',description:
"Ensure IAM password policy requires minimum length of14 or greater',gql:`{
queryawsIamPasswordPolicy {
id
__typename
minimumPasswordLength
}
}`,resource:'queryawsIamPasswordPolicy[*]',severity:'warning',conditions:{path:'@.minimumPasswordLength',greaterThanInclusive:14,},}
in
Search for a group of elements is present in an Array. Returns a boolean that represents when the condition matches or not.
Example
The next rule says that the field source should be between the range between 0.0.0.0/0 and::/0.
JS
|
exportdefault{id:'rule-id',description:'Ensure security groups allow ingress from 0.0.0.0/0 to port 22 (Scored)',gql:`{
queryawsSecurityGroup{
id
__typename
inboundRules{
source
toPort
fromPort
}
}
}`,resource:'queryawsSecurityGroup[*]',severity:'danger',conditions:{path:'@.inboundRules',array_any:[{path:'[*].source',in:['0.0.0.0/0','::/0'],},],},}
notIn
Search for a group of elements that isn't present in an Array. Returns a boolean that represents when the condition matches or not.
Example
The next rule says that the field source shouldn't be between the range between 0.0.0.0/0 and::/0.
JS
|
exportdefault{id:'rule-id',description:'Ensure no security groups allow ingress from 0.0.0.0/0 to port 22 (Scored)',gql:`{
queryawsSecurityGroup{
id
__typename
inboundRules{
source
toPort
fromPort
}
}
}`,resource:'queryawsSecurityGroup[*]',severity:'danger',conditions:{path:'@.inboundRules',array_any:[{path:'[*].source',notIn:['0.0.0.0/0','::/0'],},],},}
isEmpty
Verify that a specific Array is empty or not depending on the given parameter. Returns a boolean that represents when the condition matches or not.
Example
The following rule demonstrates the use of the isEmpty operator. When isEmtpy is false it expects the field, in this case, flowLogs, to be a non-empty array. Conversely, if isEmpty was true, that indicates that the compared array should be empty.
JS
|
exportdefault{id:'rule-id',description:'Ensure VPC flow logging is enabled in all VPCs (Scored)',gql:`{
queryawsVpc {
id
__typename
flowLogs {
resourceId
}
}
}`,resource:'queryawsVpc[*]',severity:'warning',conditions:{path:'@.flowLogs',isEmpty:false,},}
match
Verifies that the field doesn't follow the given regular expression. Returns true when the condition matches and false when it doesn't.
Example
The next condition validates when a user is using a valid email or not with the regular expression below
JS
|
exportdefault{id:'rule-id',description:"Ensure that the user has a valid email",gql:`{
queryawsIamUser(filter: { name: { eq: "root" } }) {
id
__typename
email
}
}`,resource:'queryawsIamUser[*]',severity:'danger',conditions:{path:'@.email',match:/[\w\d.-]+@[\w\d.-]+\.[\w\d.-]+/,},}
mismatch
Verifies that the field doesn't follow the given regular expression. Returns false when the condition matches and true when it doesn't.
Example
The next condition validates when a user is using a valid email with the regular expression below
JS
|
exportdefault{id:'rule-id',description:"Ensure that the user has not a valid email",gql:`{
queryawsIamUser(filter: { name: { eq: "root" } }) {
id
__typename
email
}
}`,resource:'queryawsIamUser[*]',severity:'danger',conditions:{path:'@.email',mismatch:/[\w\d.-]+@[\w\d.-]+\.[\w\d.-]+/,},}
Nested Operators
and
Joins two or more conditions. When they are all true it returns true, if one of them is false the whole clause is false.
Example
The rule will pass when both conditions inside the and array are true.
JS
|
exportdefault{id:'rule-id',description:"Avoid the use of 'root' account. Show used in last 30 days (Scored)",gql:`{
queryawsIamUser(filter: { name: { eq: "root" } }) {
id
__typename
passwordLastUsed
passwordEnabled
}
}`,resource:'queryawsIamUser[*]',severity:'danger',conditions:{and:[{path:'@.passwordEnabled',equal:true,},{value:{daysAgo:{},path:'@.passwordLastUsed'},greaterThan:30,},],},}
or
Joins two or more conditions. When they are all false it returns false, if one of them is true the whole clause is true.
Example
The rule should pass if at least one of the conditions within the or array is true.
JS
|
exportdefault{id:'rule-id',description:'Ensure rotation for customer created CMKs is enabled (Scored)',gql:`{
queryawsKms {
id
__typename
keyManager
keyRotationEnabled
}
}`,resource:'queryawsKms[*]',severity:'warning',conditions:{or:[{and:[{path:'@.keyManager',equal:'AWS',},{path:'@.keyRotationEnabled',equal:'Yes',},],},{and:[{path:'@.keyManager',equal:'CUSTOMER',},{path:'@.keyRotationEnabled',equal:'Yes',},],},],},}
array_any
This operator detects when at least one of the elements of the given array match one of the provided conditions. It can be combined with other nested operators like and/or
Example
The following rule verifies that the access keys should satisfy the condition that all S3 buckets should have logging enabled.
JS
|
exportdefault{id:'rule-id',description:'Ensure S3 bucket access logging is enabled on the CloudTrail S3 bucket',gql:`{
queryawsCloudtrail {
id
__typename
s3 {
logging
}
}
}`,resource:'queryawsCloudtrail[*]',severity:'warning',conditions:{path:'@.s3',array_any:{path:'[*].logging',equal:'Enabled',},},}
Evaluators
jq
This evaluator uses node-jq wrapper for jq. jq is a lightweight and flexible command-line JSON processor. You can slice, filter, map and transform structured data in a simple and powerful way.
The following rule verifies that IAM have hardware MFA enabled for the root account.
If any MFA Serial Number is queal to the following SerialNumber: arn:aws:iam::_<aws_account_number>_:mfa/root-account-mfa-device, it means the MFA is virtual, not hardware.
JS
|
exportdefault{id:'rule-id',description:'IAM should have hardware MFA enabled for the root account',gql:`{
queryawsIamUser(filter: { name: { eq: "root" } }) {
id
arn
accountId
__typename
name
mfaActive
virtualMfaDevices {
serialNumber
}
}
}`,resource:'queryawsIamUser[*]',severity:'high',conditions:{and:[{path:'@.mfaActive',equal:true,},{jq:'[select("arn:aws:iam::" + .accountId + ":mfa/root-account-mfa-device" == .virtualMfaDevices[].serialNumber)] | { "mfaIsVirtual" : (length > 0) }',path:'@',and:[{path:'@.mfaIsVirtual',notEqual:true,},],},],},}
The following rule verifies a log metric filter and alarm exist for unauthorized API calls
The jq query makes a join between cloudwatch and metricFilters by the metric name to get the metricFilters of each cloudwatch.
JS
|
exportdefault{id:'rule-id',description:'Ensure a log metric filter and alarm exist for unauthorized API calls',gql:`{
queryawsAccount {
id
__typename
cloudtrail {
cloudwatchLog {
cloudwatch {
metric
}
metricFilters {
filterPattern
metricTransformations {
metricName
}
}
}
}
}
}`,resource:'queryawsAccount[*]',severity:'medium',conditions:{path:'@.cloudtrail',array_any:{and:[{path:'[*].cloudwatchLog',jq:'[.[].metricFilters[] + .[].cloudwatch[] | select(.metricTransformations[].metricName == .metric)]',array_any:{and:[{path:'[*].filterPattern',match:/(\$.errorCode)\s*=\s*"UnauthorizedOperation"/,},{path:'[*].filterPattern',match:/(\$.errorCode)\s*=\s*"AccessDenied"/,},],},},],},},}