OCL

Object Control List (OCL)

An Object Control List (OCL) is a list of statements that determine whether an item is allowed to exist. Using OCL, you can create custom rules that describe what configurations are acceptable or unacceptable for any yaml or json object. Turbot Guardrails leverages OCL in many Approved Controls to provide a flexible, user-definable mechanism for defining what objects are approved.

OCL Structure

An OCL is a list of filter rule statements to determine whether an item is allowed to exist.

  - APPROVE {rule}
  - REJECT {rule}
  - REJECT {rule}


Aim Rule
Approved if the port is 443 AND the cidr is in the 10.0.0.0/8 range APPROVE $.turbot.ports.+:443 $.turbot.cidr:<=10.0.0.0/8
Approved if the port 80 or 443 APPROVE $.turbot.ports.+:80,443
Unapproved if not enabled REJECT $.enabled:!true
Unapproved if the portRangeSize is -1 or is greater than 4 REJECT $.turbot.portRangeSize:>4,-1
Unapproved if not AWS account 268085402721 or 525041748188 REJECT !$.Principal.AWS.*.AWS:/arn:aws:iam::268085402721/,/arn:aws:iam::525041748188/

Canonical Form for complex objects

Resources such as IAM policies and security groups are complex, and often have variable forms. In addition, the corrective action sometimes requires modifying an item instead of deleting it. To facilitate searching and matching against these objects in a consistent way, Guardrails first transforms them into a single, standard form. The OCL rules should be written against this canonical form, as that is the format that Approved controls will match against.

Note that resources are stored in their native form in the Guardrails CMDB, not the canonical form used for OCL processing. Controls that use the canonical form will output the canonical version in the control logs to assist in troubleshooting.

The canonical form requires knowledge of the underlying schema and implementation for that specific object type, and the form varies by type.

When a list is canonicalized:

Example - Security Group Rules

A security group will have ingress rules stored in the CMDB in the native format:
IpPermissions:
  - FromPort: 0
    IpProtocol: icmp
    IpRanges:
      - CidrIp: 0.0.0.0/0
        Description: ""
    Ipv6Ranges: [ ]
    PrefixListIds: [ ]
    ToPort: -1
    UserIdGroupPairs: [ ]
  - FromPort: 22
    IpProtocol: tcp
    IpRanges: [ ]
    Ipv6Ranges: [ ]
    PrefixListIds: [ ]
    ToPort: 22
    UserIdGroupPairs:
      - GroupId: sg-0e2a05296f27460d5
        UserId: "876515858155"
  - FromPort: 20
    IpProtocol: tcp
    IpRanges:
      - CidrIp: 192.168.0.0/24
        Description: ""
    Ipv6Ranges: [ ]
    PrefixListIds: [ ]
    ToPort: 21
    UserIdGroupPairs: [ ]
  - FromPort: 443
    IpProtocol: tcp
    IpRanges:
      - CidrIp: 0.0.0.0/0
        Description: ""
      - CidrIp: 10.0.0.0/8
        Description: ""
      - CidrIp: 172.16.0.0/16
        Description: ""
    Ipv6Ranges: [ ]
    PrefixListIds: [ ]
    ToPort: 443
    UserIdGroupPairs: [ ]
These rules will be transformed into a canonical form:
data:
  - ToPort: -1
    turbot:
      cidr: 0.0.0.0/0
      ports: [ ]
      bitmaskLength: "0"
    FromPort: 0
    IpRanges:
      CidrIp: 0.0.0.0/0
      Description: ""
    IpProtocol: icmp
    Ipv6Ranges: [ ]
    PrefixListIds: [ ]
    UserIdGroupPairs: [ ]
  - ToPort: 22
    turbot:
      ports:
        - 22
      portRangeSize: 1
    FromPort: 22
    IpRanges: [ ]
    IpProtocol: tcp
    Ipv6Ranges: [ ]
    PrefixListIds: [ ]
    UserIdGroupPairs:
      UserId: "876515858155"
      GroupId: sg-0e2a05296f27460d5
  - ToPort: 21
    turbot:
      cidr: 192.168.0.0/24
      ports:
        - 20
        - 21
      bitmaskLength: "24"
      portRangeSize: 2
    FromPort: 20
    IpRanges:
      CidrIp: 192.168.0.0/24
      Description: ""
    IpProtocol: tcp
    Ipv6Ranges: [ ]
    PrefixListIds: [ ]
    UserIdGroupPairs: [ ]
  - ToPort: 443
    turbot:
      cidr: 0.0.0.0/0
      ports:
        - 443
      bitmaskLength: "0"
      portRangeSize: 1
    FromPort: 443
    IpRanges:
      CidrIp: 0.0.0.0/0
      Description: ""
    IpProtocol: tcp
    Ipv6Ranges: [ ]
    PrefixListIds: [ ]
    UserIdGroupPairs: [ ]
  - ToPort: 443
    turbot:
      cidr: 10.0.0.0/8
      ports:
        - 443
      bitmaskLength: "8"
      portRangeSize: 1
    FromPort: 443
    IpRanges:
      CidrIp: 10.0.0.0/8
      Description: ""
    IpProtocol: tcp
    Ipv6Ranges: [ ]
    PrefixListIds: [ ]
    UserIdGroupPairs: [ ]
  - ToPort: 443
    turbot:
      cidr: 172.16.0.0/16
      ports:
        - 443
      bitmaskLength: "16"
      portRangeSize: 1
    FromPort: 443
    IpRanges:
      CidrIp: 172.16.0.0/16
      Description: ""
    IpProtocol: tcp
    Ipv6Ranges: [ ]
    PrefixListIds: [ ]
    UserIdGroupPairs: [ ]
Matching rules become intuitive
 Reject prohibited ports
 REJECT $.turbot.ports.+:20,21,25

# Reject bitmasks below minimum
REJECT $.turbot.bitmaskLength:<8

# Reject port range sizes above maximum (including -1 for all)
REJECT $.turbot.portRangeSize:>4,-1

# Reject CIDR ranges not in approved list
REJECT \
  $.turbot.cidr:>10.0.0.0/8 \
  $.turbot.cidr:>172.168.0.0/16 \
  $.turbot.cidr:>192.168.0.0/24

# Approve unmatched rules
APPROVE *

List Approved Policies

Most Approved controls that apply to lists (ingress rules, egress rules, policy statements, attachments, etc) leverage OCL. These are composite policy controls - their behavior is defined by multiple policies. The policy types share a common structure:

  # Standard OCL Approved Policies
  AWS > VPC > Security Group > Ingress Rules > Approved
  AWS > VPC > Security Group > Ingress Rules > Approved > Rules
  AWS > VPC > Security Group > Ingress Rules > Approved > Compiled Rules
  ## "Friendly" Policies 
  AWS > VPC > Security Group > Ingress Rules > Approved > CIDR Ranges
  AWS > VPC > Security Group > Ingress Rules > Approved > Maximum Port Range
  AWS > VPC > Security Group > Ingress Rules > Approved > Minimum Bitmask
  AWS > VPC > Security Group > Ingress Rules > Approved > Prohibited Ports