Enforce compliance policy by using Open Policy Agent

This topic describes how you can use Open Policy Agent to enforce compliance policy for Supply Chain Security Tools (SCST) - Scan.

Note

This topic assumes that you use SCST - Scan 1.0 because, although it is deprecated, it is still the default option in Supply Chain with Testing in this version of Tanzu Application Platform. For more information, see Add testing and scanning to your application.

VMware recommends using SCST - Scan 2.0 instead because SCST - Scan 1.0 will be removed from future versions of Tanzu Application Platform. For more information, see SCST - Scan versions.

Writing a policy template

The Scan Policy custom resource (CR) allows you to define a Rego file for policy enforcement that you can reuse across image-scan and source-scan CRs.

The Scan Controller supports policy enforcement by using an Open Policy Agent (OPA) engine with Rego files. This allows you to validate scan results for company policy compliance and can prevent the building of source code or the deployment of images.

Rego file contract

To define a Rego file for an image scan or source scan, you must comply with the requirements defined for every Rego file for the policy verification to work. For information about how to write Rego, see the Open Policy Agent documentation.

  • Package main:

    The Rego file must define a package in its body called main. The system searches for this package to verify the scan results compliance.

  • Input match:

    The Rego file evaluates one vulnerability match at a time, iterating as many times as the Rego file finds vulnerabilities in the scan. The match structure is accessed in the input.currentVulnerability object inside the Rego file and has the CycloneDX format.

  • deny rule:

    The Rego file must define a deny rule inside its body. deny is a set of error messages that are returned to the user. Each rule you write adds to that set of error messages. If the conditions in the body of the deny statement are true then the user is handed an error message. If false, the vulnerability is allowed in the source or image scan.

Define a Rego file for policy enforcement

Follow these steps to define a Rego file for policy enforcement that you can reuse across image scan and source scan CRs that output in the CycloneDX XML format.

Note

The Snyk Scanner outputs SPDX JSON. For an example of a ScanPolicy formatted for SPDX JSON output, see Sample ScanPolicy for Snyk in SPDX JSON format.

  1. Create a scan policy with a Rego file. The following is an example scan policy resource:

    ---
    apiVersion: scanning.apps.tanzu.vmware.com/v1beta1
    kind: ScanPolicy
    metadata:
      name: scan-policy
      labels:
        app.kubernetes.io/part-of: enable-in-gui
    spec:
      regoFile: |
        package main
    
        # Accepted Values: "Critical", "High", "Medium", "Low", "Negligible", "UnknownSeverity"
        notAllowedSeverities := ["Critical", "High", "UnknownSeverity"]
        ignoreCves := []
    
        contains(array, elem) = true {
          array[_] = elem
        } else = false { true }
    
        isSafe(match) {
          severities := { e | e := match.ratings.rating.severity } | { e | e := match.ratings.rating[_].severity }
          some i
          fails := contains(notAllowedSeverities, severities[i])
          not fails
        }
    
        isSafe(match) {
          ignore := contains(ignoreCves, match.id)
          ignore
        }
    
        deny[msg] {
          comps := { e | e := input.bom.components.component } | { e | e := input.bom.components.component[_] }
          some i
          comp := comps[i]
          vulns := { e | e := comp.vulnerabilities.vulnerability } | { e | e := comp.vulnerabilities.vulnerability[_] }
          some j
          vuln := vulns[j]
          ratings := { e | e := vuln.ratings.rating.severity } | { e | e := vuln.ratings.rating[_].severity }
          not isSafe(vuln)
          msg = sprintf("CVE %s %s %s", [comp.name, vuln.id, ratings])
        }
    

    You can edit the following text boxes of the Rego file as part of the CVE triage workflow:

    • notAllowedSeverities, which contains the categories of CVEs that cause the SourceScan or ImageScan to fail policy enforcement. The following example shows an app-operator blocking only Critical, High, and UnknownSeverity CVEs:

      ...
      spec:
      regoFile: |
       package main
      
       # Accepted Values: "Critical", "High", "Medium", "Low", "Negligible", "UnknownSeverity"
       notAllowedSeverities := ["Critical", "High", "UnknownSeverity"]
       ignoreCves := []
      ...
      
    • ignoreCves contains individual ignored CVEs when determining policy enforcement. In the following example, an app-operator ignores CVE-2018-14643 and GHSA-f2jv-r9rf-7988 if they are false positives. For more information, see Vulnerability Scanner limitations.

      ...
      spec:
      regoFile: |
       package main
      
       notAllowedSeverities := []
       ignoreCves := ["CVE-2018-14643", "GHSA-f2jv-r9rf-7988"]
      ...
      
  2. Deploy the scan policy to the cluster by running:

    kubectl apply -f <path_to_scan_policy>/<scan_policy_filename>.yaml -n <desired_namespace>
    

For information about how scan policies are used in the CVE triage workflow, see Triage and Remediate CVEs for SCST - Scan.

Further refine the scan policy for use

The scan policy earlier demonstrates how vulnerabilities are ignored during a compliance check. It is not possible to audit why a vulnerability is ignored. You might want to allow an exception where a build with a failing vulnerability is allowed to progress through a supply chain. You can allow this exception for a period of time, which requires an expiration date. Vulnerability Exploitability Exchange (VEX) documents are gaining popularity to capture security advisory information pertaining to vulnerabilities. You can use Rego for these use cases.

For example, the following scan policy includes an additional text box to capture comments regarding why the scan ignores a vulnerability. The notAllowedSeverities array remains an array of strings, but the ignoreCves array updates from an array of strings to an array of objects. This causes a change to the contains function, splitting it into separate functions for each array.

---
apiVersion: scanning.apps.tanzu.vmware.com/v1beta1
kind: ScanPolicy
metadata:
  name: scan-policy
  labels:
    app.kubernetes.io/part-of: enable-in-gui
spec:
  regoFile: |
    package main

    # Accepted Values: "Critical", "High", "Medium", "Low", "Negligible", "UnknownSeverity"
    notAllowedSeverities := ["Critical", "High", "UnknownSeverity"]

    # List of known vulnerabilities to ignore when deciding whether to fail compliance. Example:
    # ignoreCves := [
    #   {
    #     "id": "CVE-2018-14643",
    #     "detail": "Determined affected code is not in the execution path."
    #   }
    # ]
    ignoreCves := []

    containsSeverity(array, elem) = true {
      array[_] = elem
    } else = false { true }

    isSafe(match) {
      severities := { e | e := match.ratings.rating.severity } | { e | e := match.ratings.rating[_].severity }
      some i
      fails := containsSeverity(notAllowedSeverities, severities[i])
      not fails
    }

    containsCve(array, elem) = true {
      array[_].id = elem
    } else = false { true }

    isSafe(match) {
      ignore := containsCve(ignoreCves, match.id)
      ignore
    }

    deny[msg] {
      comps := { e | e := input.bom.components.component } | { e | e := input.bom.components.component[_] }
      some i
      comp := comps[i]
      vulns := { e | e := comp.vulnerabilities.vulnerability } | { e | e := comp.vulnerabilities.vulnerability[_] }
      some j
      vuln := vulns[j]
      ratings := { e | e := vuln.ratings.rating.severity } | { e | e := vuln.ratings.rating[_].severity }
      not isSafe(vuln)
      msg = sprintf("CVE %s %s %s", [comp.name, vuln.id, ratings])
    }

The following example includes an expiration text box and only allows the vulnerability to be ignored for a period of time:

---
apiVersion: scanning.apps.tanzu.vmware.com/v1beta1
kind: ScanPolicy
metadata:
  name: scan-policy
  labels:
    app.kubernetes.io/part-of: enable-in-gui
spec:
  regoFile: |
    package main

    # Accepted Values: "Critical", "High", "Medium", "Low", "Negligible", "UnknownSeverity"
    notAllowedSeverities := ["Critical", "High", "UnknownSeverity"]

    # List of known vulnerabilities to ignore when deciding whether to fail compliance. Example:
    # ignoreCves := [
    #   {
    #     "id": "CVE-2018-14643",
    #     "detail": "Determined affected code is not in the execution path.",
    #     "expiration": "2022-Dec-31"
    #   }
    # ]
    ignoreCves := []

    containsSeverity(array, elem) = true {
      array[_] = elem
    } else = false { true }

    isSafe(match) {
      severities := { e | e := match.ratings.rating.severity } | { e | e := match.ratings.rating[_].severity }
      some i
      fails := containsSeverity(notAllowedSeverities, severities[i])
      not fails
    }

    containsCve(array, elem) = true {
      array[_].id = elem
      curr_time := time.now_ns()
      date_format := "2006-Jan-02"
      expire_time := time.parse_ns(date_format, array[_].expiration)
      curr_time < expire_time
    } else = false { true }

    isSafe(match) {
      ignore := containsCve(ignoreCves, match.id)
      ignore
    }

    deny[msg] {
      comps := { e | e := input.bom.components.component } | { e | e := input.bom.components.component[_] }
      some i
      comp := comps[i]
      vulns := { e | e := comp.vulnerabilities.vulnerability } | { e | e := comp.vulnerabilities.vulnerability[_] }
      some j
      vuln := vulns[j]
      ratings := { e | e := vuln.ratings.rating.severity } | { e | e := vuln.ratings.rating[_].severity }
      not isSafe(vuln)
      msg = sprintf("CVE %s %s %s", [comp.name, vuln.id, ratings])
    }

Troubleshooting Rego files (Scan Policy)

To troubleshoot or confirm that any modifications made to the Rego file in the provided sample scan policy are functioning as intended, see Troubleshooting Rego Files.

Enable Tanzu Developer Portal to view the ScanPolicy resource

For the Tanzu Developer Portal to view the ScanPolicy resource, it must have a matching kubernetes-label-selector with a part-of prefix.

The following example is a portion of a ScanPolicy that Tanzu Developer Portal can display:

---
apiVersion: scanning.apps.tanzu.vmware.com/v1beta1
kind: ScanPolicy
metadata:
  name: scan-policy
  labels:
    app.kubernetes.io/part-of: enable-in-gui
spec:
  regoFile: |
    ...
Note

Anything van be a value for the label. Tanzu Developer Portal searches for the existence of the part-of prefix string and does not match for anything else specific.

Deprecated Rego file definition

Before Scan Controller v1.2.0, you must use the following format where the Rego file differences are:

  • The package name must be package policies instead of package main.
  • The deny rule is the Boolean isCompliant instead of deny[msg].
  • The Rego file must define an isCompliant rule inside its body. This must be a Boolean type containing the result, whether the vulnerability violates the security policy or not. If isCompliant is true, the vulnerability is allowed in the source or image scan. Otherwise, false is considered. Any scan that finds at least one vulnerability that evaluates to isCompliant=false sets the PolicySucceeded condition as false.

The following is an example scan policy resource:

apiVersion: scanning.apps.tanzu.vmware.com/v1alpha1
kind: ScanPolicy
metadata:
  name: v1alpha1-scan-policy
  labels:
    app.kubernetes.io/part-of: enable-in-gui
spec:
  regoFile: |
    package policies

    default isCompliant = false

    ignoreSeverities := ["Critical", "High"]

    contains(array, elem) = true {
      array[_] = elem
    } else = false { true }

    isCompliant {
      ignore := contains(ignoreSeverities, input.currentVulnerability.Ratings.Rating[_].Severity)
      ignore
    }
check-circle-line exclamation-circle-line close-line
Scroll to top icon