diff --git a/docs/docs/reference/policies.mdx b/docs/docs/reference/policies.mdx index 74586a65d..d5c626604 100644 --- a/docs/docs/reference/policies.mdx +++ b/docs/docs/reference/policies.mdx @@ -23,15 +23,39 @@ In this particular example, we see: * policies have a name (cyclonedx-licenses) * they can be optionally applied to a specific type of material (check [the documentation](./operator/contract#material-schema) for the supported types). If no type is specified, a material name will need to be provided explicitly in the contract. * they have a policy script that it's evaluated against the material (in this case a CycloneDX SBOM report). Currently, only [Rego](https://www.openpolicyagent.org/docs/latest/policy-language/#learning-rego) policies are supported. +* there can be multiple scripts, each associated with a different material type. Policy scripts could also be specified in a detached form: ```yaml ... spec: - type: SBOM_CYCLONEDX_JSON - path: my-script.rego + policies: + - kind: SBOM_CYCLONEDX_JSON + path: my-script.rego ``` +### Supporting multiple material types +Policies can accept multiple material types. This is specially useful when a material can be specified in multiple format types, but from the user perspective, we still want to maintain one single policy. + +For example, this policy would check for vulnerabilities in SARIF, CycloneDX and CSAF formats: +```yaml +... +apiVersion: workflowcontract.chainloop.dev/v1 +kind: Policy +metadata: + name: cve-policy +spec: + policies: + - kind: SBOM_CYCLONEDX_JSON + path: cves-cyclonedx.rego + - kind: CSAF_SECURITY_ADVISORY + path: cves-csaf-sa.rego + - kind: SARIF + path: cves-sarif.rego +``` +In these cases, Chainloop will choose the right script to execute, but externally it would be seen as a single policy. +If more than one path is executed (because they might have the same `kind`), the evaluation result will be the sum of all evaluations. + ## Applying policies to contracts When defining a contract, a new `policies` section can be specified. Policies can be applied to any material, but also to the attestation statement as a whole. ```yaml @@ -86,11 +110,22 @@ There are two ways to attach a policy to a contract: * If preferred, authors could create self-contained contracts **embedding policy specifications**. The main advantage of this method is that it ensures that the policy source cannot be changed, as it's stored and versioned within the contract: - - {PolicyYAML} - + ```yaml + policies: + materials: + - embedded: # (1) + # Put full policy spec here + apiVersion: workflowcontract.chainloop.dev/v1 + kind: Policy + metadata: + name: cve-policy + spec: + policies: + - kind: SBOM_CYCLONEDX_JSON + path: cves-cyclonedx.rego + ``` -In the example above, we can see that, when referenced by the `policy` attribute (1), a full policy can be embedded in the contract. +In the example above, we can see that, when referenced by the `embedded` attribute (1), a full policy can be embedded in the contract. ### Policy arguments Policies may accept arguments to customize its behaviour. See this policy that matches a "quality" score against a "threshold" argument: diff --git a/docs/examples/policies/sbom/cyclonedx-banned-licenses.yaml b/docs/examples/policies/sbom/cyclonedx-banned-licenses.yaml index 0de4ba8bf..868d07074 100644 --- a/docs/examples/policies/sbom/cyclonedx-banned-licenses.yaml +++ b/docs/examples/policies/sbom/cyclonedx-banned-licenses.yaml @@ -20,19 +20,20 @@ metadata: annotations: category: sbom spec: - type: SBOM_CYCLONEDX_JSON - embedded: | - package main - - import rego.v1 - - banned_licenses := ["GPL-2.0", "GPL-3.0"] - - violations contains ref if { - some i - comp := input.components[i] - some j - license := comp.licenses[j].license - license.name == banned_licenses[_] - ref := sprintf("Forbidden license %v for %v (%v)", [license.name, comp.name, comp["bom-ref"]]) - } + policies: + - kind: SBOM_CYCLONEDX_JSON + embedded: | + package main + + import rego.v1 + + banned_licenses := ["GPL-2.0", "GPL-3.0"] + + violations contains ref if { + some i + comp := input.components[i] + some j + license := comp.licenses[j].license + license.name == banned_licenses[_] + ref := sprintf("Forbidden license %v for %v (%v)", [license.name, comp.name, comp["bom-ref"]]) + } diff --git a/docs/examples/policies/sbom/cyclonedx-banned-packages.yaml b/docs/examples/policies/sbom/cyclonedx-banned-packages.yaml index 6ac642e33..11ddb43c5 100644 --- a/docs/examples/policies/sbom/cyclonedx-banned-packages.yaml +++ b/docs/examples/policies/sbom/cyclonedx-banned-packages.yaml @@ -20,36 +20,37 @@ metadata: annotations: category: sbom spec: - type: SBOM_CYCLONEDX_JSON - embedded: | - package main + policies: + - kind: SBOM_CYCLONEDX_JSON + embedded: | + package main + + import rego.v1 + + # It supports packages with version. When specified, requires it to be semver, and would also fail when version is lower + banned_packages := ["log4j@2.14.1"] - import rego.v1 + # all versions + violations contains ref if { + some i + comp := input.components[i] + some j + banned := banned_packages[j] + nv := split(banned, "@") + not nv[1] + comp.name == nv[0] + ref := sprintf("Banned package: %v", [comp.name]) + } - # It supports packages with version. When specified, requires it to be semver, and would also fail when version is lower - banned_packages := ["log4j@2.14.1"] - - # all versions - violations contains ref if { - some i - comp := input.components[i] - some j - banned := banned_packages[j] - nv := split(banned, "@") - not nv[1] - comp.name == nv[0] - ref := sprintf("Banned package: %v", [comp.name]) - } - - # specific versions - violations contains ref if { - some i - comp := input.components[i] - some j - banned := banned_packages[j] - nv := split(banned, "@") - comp.name == nv[0] - result := semver.compare(comp.version, nv[1]) - result <= 0 - ref := sprintf("Banned package: %v %v", [comp.name, comp.version]) - } + # specific versions + violations contains ref if { + some i + comp := input.components[i] + some j + banned := banned_packages[j] + nv := split(banned, "@") + comp.name == nv[0] + result := semver.compare(comp.version, nv[1]) + result <= 0 + ref := sprintf("Banned package: %v %v", [comp.name, comp.version]) + } diff --git a/docs/examples/policies/sbom/cyclonedx-freshness.yaml b/docs/examples/policies/sbom/cyclonedx-freshness.yaml index b4c7cca0b..8b21c92e4 100644 --- a/docs/examples/policies/sbom/cyclonedx-freshness.yaml +++ b/docs/examples/policies/sbom/cyclonedx-freshness.yaml @@ -20,23 +20,24 @@ metadata: annotations: category: sbom spec: - type: SBOM_CYCLONEDX_JSON - embedded: | - package main - - import rego.v1 - - limit := 30 - - nanosecs_per_second = (1000 * 1000) * 1000 - - nanosecs_per_day = ((24 * 60) * 60) * nanosecs_per_second - - maximum_age = limit * nanosecs_per_day - - violations contains msg if { - sbom_ns = time.parse_rfc3339_ns(input.metadata.timestamp) - exceeding = time.now_ns() - (sbom_ns + maximum_age) - exceeding > 0 - msg := sprintf("SBOM created at: %s which is too old (freshness limit set to %d days)", [input.metadata.timestamp, limit]) - } + policies: + - kind: SBOM_CYCLONEDX_JSON + embedded: | + package main + + import rego.v1 + + limit := 30 + + nanosecs_per_second = (1000 * 1000) * 1000 + + nanosecs_per_day = ((24 * 60) * 60) * nanosecs_per_second + + maximum_age = limit * nanosecs_per_day + + violations contains msg if { + sbom_ns = time.parse_rfc3339_ns(input.metadata.timestamp) + exceeding = time.now_ns() - (sbom_ns + maximum_age) + exceeding > 0 + msg := sprintf("SBOM created at: %s which is too old (freshness limit set to %d days)", [input.metadata.timestamp, limit]) + } diff --git a/docs/examples/policies/sbom/cyclonedx-licenses.yaml b/docs/examples/policies/sbom/cyclonedx-licenses.yaml index b2ef62f2c..868bf74c3 100644 --- a/docs/examples/policies/sbom/cyclonedx-licenses.yaml +++ b/docs/examples/policies/sbom/cyclonedx-licenses.yaml @@ -6,21 +6,22 @@ metadata: annotations: category: sbom spec: - type: SBOM_CYCLONEDX_JSON - embedded: | - package main - - import rego.v1 - - violations contains msg if { - count(without_license) > 0 - msg := sprintf("Missing licenses for %s", [components_str]) - } - - components_str := concat(", ", [comp.purl | some comp in without_license]) - - without_license contains comp if { - some comp in input.components - not comp.licenses - } + policies: + - kind: SBOM_CYCLONEDX_JSON + embedded: | + package main + + import rego.v1 + + violations contains msg if { + count(without_license) > 0 + msg := sprintf("Missing licenses for %s", [components_str]) + } + + components_str := concat(", ", [comp.purl | some comp in without_license]) + + without_license contains comp if { + some comp in input.components + not comp.licenses + } diff --git a/docs/examples/policies/sbom/cyclonedx-required-packages.yaml b/docs/examples/policies/sbom/cyclonedx-required-packages.yaml index 01ce9d649..4afabcd37 100644 --- a/docs/examples/policies/sbom/cyclonedx-required-packages.yaml +++ b/docs/examples/policies/sbom/cyclonedx-required-packages.yaml @@ -20,24 +20,25 @@ metadata: annotations: category: sbom spec: - type: SBOM_CYCLONEDX_JSON - embedded: | - package main - - import rego.v1 - - required_packages := {"glibc", "libcrypto3"} - - violations contains msg if { - count(all_matches) != count(required_packages) - missing := required_packages - all_matches - some i - msg := sprintf("missing package: %v", [missing[i]]) - } - - all_matches contains name if { - some i - comp := input.components[i] - comp.name == required_packages[_] - name := comp.name - } + policies: + - kind: SBOM_CYCLONEDX_JSON + embedded: | + package main + + import rego.v1 + + required_packages := {"glibc", "libcrypto3"} + + violations contains msg if { + count(all_matches) != count(required_packages) + missing := required_packages - all_matches + some i + msg := sprintf("missing package: %v", [missing[i]]) + } + + all_matches contains name if { + some i + comp := input.components[i] + comp.name == required_packages[_] + name := comp.name + } diff --git a/docs/examples/policies/sbom/sbom-present.yaml b/docs/examples/policies/sbom/sbom-present.yaml index 2bd72cbe9..1e74c01f1 100644 --- a/docs/examples/policies/sbom/sbom-present.yaml +++ b/docs/examples/policies/sbom/sbom-present.yaml @@ -20,25 +20,26 @@ metadata: annotations: category: sbom spec: - type: ATTESTATION - embedded: | - package main - - # Verifies there is a SBOM material, even if not enforced by contract - - import future.keywords.contains - import future.keywords.in - - violations[msg] { - not has_sbom - msg := "missing SBOM material" - } - - # Collect all material types - kinds contains kind { - some material in input.predicate.materials - kind := material.annotations["chainloop.material.type"] - } + policies: + - kind: ATTESTATION + embedded: | + package main + + # Verifies there is a SBOM material, even if not enforced by contract + + import future.keywords.contains + import future.keywords.in + + violations[msg] { + not has_sbom + msg := "missing SBOM material" + } + + # Collect all material types + kinds contains kind { + some material in input.predicate.materials + kind := material.annotations["chainloop.material.type"] + } has_sbom { values := ["SBOM_SPDX_JSON","SBOM_CYCLONEDX_JSON"] diff --git a/docs/examples/policies/sbom/spdx-sbom-syft.yaml b/docs/examples/policies/sbom/spdx-sbom-syft.yaml index a468bff4b..cdf3124e9 100644 --- a/docs/examples/policies/sbom/spdx-sbom-syft.yaml +++ b/docs/examples/policies/sbom/spdx-sbom-syft.yaml @@ -20,19 +20,20 @@ metadata: annotations: category: sbom spec: - type: SBOM_SPDX_JSON - embedded: | - package main - - import future.keywords.in - - violations[msg] { - not made_with_syft - - msg := "Not made with syft" - } - - made_with_syft { - some creator in input.creationInfo.creators - contains(creator, "syft") - } + policies: + - kind: SBOM_SPDX_JSON + embedded: | + package main + + import future.keywords.in + + violations[msg] { + not made_with_syft + + msg := "Not made with syft" + } + + made_with_syft { + some creator in input.creationInfo.creators + contains(creator, "syft") + }