diff --git a/hooks/cascading-scans/hook/hook.test.js b/hooks/cascading-scans/hook/hook.test.js index 4477164309..36528afc6b 100644 --- a/hooks/cascading-scans/hook/hook.test.js +++ b/hooks/cascading-scans/hook/hook.test.js @@ -2,7 +2,6 @@ // // SPDX-License-Identifier: Apache-2.0 -const { getCascadingScanDefinition } = require("./scan-helpers"); const { getCascadingScans } = require("./hook"); let parentScan = undefined; @@ -80,27 +79,39 @@ test("Should create subsequent scans for open HTTPS ports (NMAP findings)", () = expect(cascadedScans).toMatchInlineSnapshot(` Array [ Object { - "cascades": Object {}, - "env": undefined, - "finding": Object { - "attributes": Object { - "hostname": "foobar.com", - "port": 443, - "service": "https", - "state": "open", + "apiVersion": "execution.securecodebox.io/v1", + "kind": "Scan", + "metadata": Object { + "annotations": Object { + "cascading.securecodebox.io/chain": "tls-scans", + "cascading.securecodebox.io/matched-finding": undefined, + "cascading.securecodebox.io/parent-scan": "nmap-foobar.com", + "securecodebox.io/hook": "cascading-scans", }, - "category": "Open Port", - "name": "Port 443 is open", + "generateName": "sslyze-foobar.com-tls-scans-", + "labels": Object {}, + "ownerReferences": Array [ + Object { + "apiVersion": "execution.securecodebox.io/v1", + "blockOwnerDeletion": true, + "controller": true, + "kind": "Scan", + "name": "nmap-foobar.com", + "uid": undefined, + }, + ], + }, + "spec": Object { + "cascades": Object {}, + "env": Array [], + "parameters": Array [ + "--regular", + "foobar.com:443", + ], + "scanType": "sslyze", + "volumeMounts": Array [], + "volumes": Array [], }, - "generatedBy": "tls-scans", - "name": "sslyze-foobar.com-tls-scans", - "parameters": Array [ - "--regular", - "foobar.com:443", - ], - "scanAnnotations": Object {}, - "scanLabels": Object {}, - "scanType": "sslyze", }, ] `); @@ -150,34 +161,7 @@ test("Should not try to do magic to the scan name if its something random", () = sslyzeCascadingRules ); - expect(cascadedScans).toMatchInlineSnapshot(` - Array [ - Object { - "cascades": Object {}, - "env": undefined, - "finding": Object { - "attributes": Object { - "hostname": undefined, - "ip_address": "10.42.42.42", - "port": 443, - "service": "https", - "state": "open", - }, - "category": "Open Port", - "name": "Port 443 is open", - }, - "generatedBy": "tls-scans", - "name": "foobar.com-tls-scans", - "parameters": Array [ - "--regular", - "10.42.42.42:443", - ], - "scanAnnotations": Object {}, - "scanLabels": Object {}, - "scanType": "sslyze", - }, - ] - `); + expect(cascadedScans[0].metadata.generateName).toEqual("foobar.com-tls-scans-"); }); test("Should not start a new scan when the corresponding cascadingRule is already in the chain", () => { @@ -231,27 +215,39 @@ test("Should not crash when the annotations are not set", () => { expect(cascadedScans).toMatchInlineSnapshot(` Array [ Object { - "cascades": Object {}, - "env": undefined, - "finding": Object { - "attributes": Object { - "hostname": "foobar.com", - "port": 443, - "service": "https", - "state": "open", + "apiVersion": "execution.securecodebox.io/v1", + "kind": "Scan", + "metadata": Object { + "annotations": Object { + "cascading.securecodebox.io/chain": "tls-scans", + "cascading.securecodebox.io/matched-finding": undefined, + "cascading.securecodebox.io/parent-scan": "nmap-foobar.com", + "securecodebox.io/hook": "cascading-scans", }, - "category": "Open Port", - "name": "Port 443 is open", + "generateName": "sslyze-foobar.com-tls-scans-", + "labels": Object {}, + "ownerReferences": Array [ + Object { + "apiVersion": "execution.securecodebox.io/v1", + "blockOwnerDeletion": true, + "controller": true, + "kind": "Scan", + "name": "nmap-foobar.com", + "uid": undefined, + }, + ], + }, + "spec": Object { + "cascades": Object {}, + "env": Array [], + "parameters": Array [ + "--regular", + "foobar.com:443", + ], + "scanType": "sslyze", + "volumeMounts": Array [], + "volumes": Array [], }, - "generatedBy": "tls-scans", - "name": "sslyze-foobar.com-tls-scans", - "parameters": Array [ - "--regular", - "foobar.com:443", - ], - "scanAnnotations": Object {}, - "scanLabels": Object {}, - "scanType": "sslyze", }, ] `); @@ -284,40 +280,16 @@ test("Should copy ENV fields from cascadingRule to created scan", () => { sslyzeCascadingRules ); - expect(cascadedScans).toMatchInlineSnapshot(` + expect(cascadedScans[0].spec.env).toMatchInlineSnapshot(` Array [ Object { - "cascades": Object {}, - "env": Array [ - Object { - "name": "FOOBAR", - "valueFrom": Object { - "secretKeyRef": Object { - "key": "token", - "name": "foobar-token", - }, - }, - }, - ], - "finding": Object { - "attributes": Object { - "hostname": "foobar.com", - "port": 443, - "service": "https", - "state": "open", + "name": "FOOBAR", + "valueFrom": Object { + "secretKeyRef": Object { + "key": "token", + "name": "foobar-token", }, - "category": "Open Port", - "name": "Port 443 is open", }, - "generatedBy": "tls-scans", - "name": "sslyze-foobar.com-tls-scans", - "parameters": Array [ - "--regular", - "foobar.com:443", - ], - "scanAnnotations": Object {}, - "scanLabels": Object {}, - "scanType": "sslyze", }, ] `); @@ -373,27 +345,39 @@ test("Should allow wildcards in cascadingRules", () => { expect(cascadedScans).toMatchInlineSnapshot(` Array [ Object { - "cascades": Object {}, - "env": undefined, - "finding": Object { - "attributes": Object { - "hostname": "foobar.com", - "port": 8443, - "service": "https-alt", - "state": "open", + "apiVersion": "execution.securecodebox.io/v1", + "kind": "Scan", + "metadata": Object { + "annotations": Object { + "cascading.securecodebox.io/chain": "tls-scans", + "cascading.securecodebox.io/matched-finding": undefined, + "cascading.securecodebox.io/parent-scan": "nmap-foobar.com", + "securecodebox.io/hook": "cascading-scans", }, - "category": "Open Port", - "name": "Port 8443 is open", + "generateName": "sslyze-foobar.com-tls-scans-", + "labels": Object {}, + "ownerReferences": Array [ + Object { + "apiVersion": "execution.securecodebox.io/v1", + "blockOwnerDeletion": true, + "controller": true, + "kind": "Scan", + "name": "nmap-foobar.com", + "uid": undefined, + }, + ], + }, + "spec": Object { + "cascades": Object {}, + "env": Array [], + "parameters": Array [ + "--regular", + "foobar.com:8443", + ], + "scanType": "sslyze", + "volumeMounts": Array [], + "volumes": Array [], }, - "generatedBy": "tls-scans", - "name": "sslyze-foobar.com-tls-scans", - "parameters": Array [ - "--regular", - "foobar.com:8443", - ], - "scanAnnotations": Object {}, - "scanLabels": Object {}, - "scanType": "sslyze", }, ] `); @@ -427,10 +411,8 @@ test("should not copy labels if inheritLabels is set to false", () => { ); for (const cascadedScan of cascadedScans) { - const cascadingScanDefinition = getCascadingScanDefinition(cascadedScan, parentScan); - expect(Object.entries(parentScan.metadata.labels).every(([label, value]) => - cascadingScanDefinition.metadata.labels[label] === value + cascadedScan.metadata.labels[label] === value )).toBe(false) } }); @@ -462,10 +444,8 @@ test("should copy labels if inheritLabels is not set", () => { ); for (const cascadedScan of cascadedScans) { - const cascadingScanDefinition = getCascadingScanDefinition(cascadedScan, parentScan); - expect(Object.entries(parentScan.metadata.labels).every(([label, value]) => - cascadingScanDefinition.metadata.labels[label] === value + cascadedScan.metadata.labels[label] === value )).toBe(true) } }); @@ -499,10 +479,8 @@ test("should copy labels if inheritLabels is set to true", () => { ); for (const cascadedScan of cascadedScans) { - const cascadingScanDefinition = getCascadingScanDefinition(cascadedScan, parentScan); - expect(Object.entries(parentScan.metadata.labels).every(([label, value]) => - cascadingScanDefinition.metadata.labels[label] === value + cascadedScan.metadata.labels[label] === value )).toBe(true) } }); @@ -534,10 +512,8 @@ test("should not copy annotations if inheritAnnotations is set to false", () => ); for (const cascadedScan of cascadedScans) { - const cascadingScanDefinition = getCascadingScanDefinition(cascadedScan, parentScan); - expect(Object.entries(parentScan.metadata.annotations).every(([label, value]) => - cascadingScanDefinition.metadata.annotations[label] === value + cascadedScan.metadata.annotations[label] === value )).toBe(false) } }); @@ -568,10 +544,8 @@ test("should copy annotations if inheritAnnotations is not set", () => { ); for (const cascadedScan of cascadedScans) { - const cascadingScanDefinition = getCascadingScanDefinition(cascadedScan, parentScan); - expect(Object.entries(parentScan.metadata.annotations).every(([label, value]) => - cascadingScanDefinition.metadata.annotations[label] === value + cascadedScan.metadata.annotations[label] === value )).toBe(true) } }); @@ -603,10 +577,8 @@ test("should copy annotations if inheritAnnotations is set to true", () => { ); for (const cascadedScan of cascadedScans) { - const cascadingScanDefinition = getCascadingScanDefinition(cascadedScan, parentScan); - expect(Object.entries(parentScan.metadata.annotations).every(([label, value]) => - cascadingScanDefinition.metadata.annotations[label] === value + cascadedScan.metadata.annotations[label] === value )).toBe(true) } }); @@ -637,42 +609,8 @@ test("should copy scanLabels from CascadingRule to cascading scan", () => { ); const cascadedScan = cascadedScans[0] - - expect(cascadedScans).toMatchInlineSnapshot(` - Array [ - Object { - "cascades": Object {}, - "env": undefined, - "finding": Object { - "attributes": Object { - "hostname": "foobar.com", - "port": 443, - "service": "https", - "state": "open", - }, - "category": "Open Port", - "name": "Port 443 is open", - }, - "generatedBy": "tls-scans", - "name": "sslyze-foobar.com-tls-scans", - "parameters": Array [ - "--regular", - "foobar.com:443", - ], - "scanAnnotations": Object {}, - "scanLabels": Object { - "k_one": "v_one", - "k_two": "v_two", - }, - "scanType": "sslyze", - }, - ] - `); - - const cascadingScanDefinition = getCascadingScanDefinition(cascadedScan, parentScan); - expect(Object.entries(sslyzeCascadingRules[0].spec.scanLabels).every(([label, value]) => - cascadingScanDefinition.metadata.labels[label] === value + cascadedScan.metadata.labels[label] === value )).toBe(true) }); @@ -702,42 +640,8 @@ test("should copy scanAnnotations from CascadingRule to cascading scan", () => { ); const cascadedScan = cascadedScans[0] - - expect(cascadedScans).toMatchInlineSnapshot(` - Array [ - Object { - "cascades": Object {}, - "env": undefined, - "finding": Object { - "attributes": Object { - "hostname": "foobar.com", - "port": 443, - "service": "https", - "state": "open", - }, - "category": "Open Port", - "name": "Port 443 is open", - }, - "generatedBy": "tls-scans", - "name": "sslyze-foobar.com-tls-scans", - "parameters": Array [ - "--regular", - "foobar.com:443", - ], - "scanAnnotations": Object { - "k_one": "v_one", - "k_two": "v_two", - }, - "scanLabels": Object {}, - "scanType": "sslyze", - }, - ] - `); - - const cascadingScanDefinition = getCascadingScanDefinition(cascadedScan, parentScan); - expect(Object.entries(sslyzeCascadingRules[0].spec.scanAnnotations).every(([label, value]) => - cascadingScanDefinition.metadata.annotations[label] === value + cascadedScan.metadata.annotations[label] === value )).toBe(true) }); @@ -770,21 +674,32 @@ test("should properly parse template values in scanLabels and scanAnnotations", const cascadedScans = getCascadingScans( parentScan, findings, - sslyzeCascadingRules + sslyzeCascadingRules, + sslyzeCascadingRules[0] ); - const { scanLabels, scanAnnotations } = cascadedScans[0] + const { labels, annotations } = cascadedScans[0].metadata; // No snapshots as scanLabels/scanAnnotations can be in any order - const result = { + const labelResults = { "k_one": "nmap-foobar.com", "k_two": "", "k_three": "foobar.com", } - expect(scanLabels).toEqual(result) + expect(labels).toEqual(labelResults) - expect(scanAnnotations).toEqual(result) + const annotationsResults = { + "cascading.securecodebox.io/chain": "tls-scans", + "cascading.securecodebox.io/matched-finding": undefined, + "cascading.securecodebox.io/parent-scan": "nmap-foobar.com", + "securecodebox.io/hook": "cascading-scans", + "k_one": "nmap-foobar.com", + "k_two": "", + "k_three": "foobar.com", + }; + + expect(annotations).toEqual(annotationsResults) }) test("should copy proper finding ID into annotations", () => { @@ -820,42 +735,449 @@ test("should copy proper finding ID into annotations", () => { ); const cascadedScan = cascadedScans[0] + expect(Object.entries(cascadedScan.metadata.annotations).every(([label, value]) => { + if (label === "cascading.securecodebox.io/matched-finding") { + return value === "f0c718bd-9987-42c8-2259-73794e61dd5a"; + } else return true; + } + )).toBe(true) +}); - expect(cascadedScans).toMatchInlineSnapshot(` +test("should merge environment variables into cascaded scan", () => { + parentScan.spec.cascades.inheritEnv = true + const findings = [ + { + name: "Port 443 is open", + category: "Open Port", + attributes: { + state: "open", + hostname: "foobar.com", + port: 443, + service: "https" + } + } + ]; + + parentScan.spec.env = [ + { + "name": "parent_environment_variable_name", + "value": "parent_environment_variable_value" + } + ] + + sslyzeCascadingRules[0].spec.scanSpec.env = [ + { + "name": "rule_environment_variable_name", + "value": "rule_environment_variable_value" + } + ] + + const cascadedScans = getCascadingScans( + parentScan, + findings, + sslyzeCascadingRules + ); + + const cascadedScan = cascadedScans[0] + expect(cascadedScan.spec.env).toMatchInlineSnapshot(` Array [ Object { - "cascades": Object {}, - "env": undefined, - "finding": Object { - "attributes": Object { - "hostname": "foobar.com", - "port": 443, - "service": "https", - "state": "open", - }, - "category": "Open Port", - "id": "f0c718bd-9987-42c8-2259-73794e61dd5a", - "name": "Port 443 is open", + "name": "parent_environment_variable_name", + "value": "parent_environment_variable_value", + }, + Object { + "name": "rule_environment_variable_name", + "value": "rule_environment_variable_value", + }, + ] + `); +}); + +test("should merge volumeMounts into cascaded scan", () => { + parentScan.spec.cascades.inheritVolumes = true + const findings = [ + { + name: "Port 443 is open", + category: "Open Port", + attributes: { + state: "open", + hostname: "foobar.com", + port: 443, + service: "https" + } + } + ]; + + parentScan.spec.volumeMounts = [ + { + "mountPath": "/etc/ssl/certs/ca-cert.cer", + "name": "ca-certificate", + "readOnly": true, + "subPath": "ca-cert.cer" + } + ] + + sslyzeCascadingRules[0].spec.scanSpec.volumeMounts = [ + { + "mountPath": "/etc/ssl/certs/ca-cert-sslyze.cer", + "name": "ca-certificate-sslyze", + "readOnly": true, + "subPath": "ca-cert-sslyze.cer" + } + ] + + const cascadedScans = getCascadingScans( + parentScan, + findings, + sslyzeCascadingRules + ); + + const cascadedScan = cascadedScans[0] + expect(cascadedScan.spec.volumeMounts).toMatchInlineSnapshot(` + Array [ + Object { + "mountPath": "/etc/ssl/certs/ca-cert.cer", + "name": "ca-certificate", + "readOnly": true, + "subPath": "ca-cert.cer", + }, + Object { + "mountPath": "/etc/ssl/certs/ca-cert-sslyze.cer", + "name": "ca-certificate-sslyze", + "readOnly": true, + "subPath": "ca-cert-sslyze.cer", + }, + ] + `); +}); + +test("should merge volumes into cascaded scan", () => { + parentScan.spec.cascades.inheritVolumes = true + const findings = [ + { + name: "Port 443 is open", + category: "Open Port", + attributes: { + state: "open", + hostname: "foobar.com", + port: 443, + service: "https" + } + } + ]; + + parentScan.spec.volumes = [ + { + "name": "ca-certificate", + "configMap": { + "name": "ca-certificate" + } + } + ] + + sslyzeCascadingRules[0].spec.scanSpec.volumes = [ + { + "name": "ca-certificate-sslyze", + "configMap": { + "name": "ca-certificate-sslyze" + } + } + ] + + const cascadedScans = getCascadingScans( + parentScan, + findings, + sslyzeCascadingRules + ); + + const cascadedScan = cascadedScans[0]; + + expect(cascadedScan.spec.volumes).toMatchInlineSnapshot(` + Array [ + Object { + "configMap": Object { + "name": "ca-certificate", }, - "generatedBy": "tls-scans", - "name": "sslyze-foobar.com-tls-scans", - "parameters": Array [ - "--regular", - "foobar.com:443", - ], - "scanAnnotations": Object {}, - "scanLabels": Object {}, - "scanType": "sslyze", + "name": "ca-certificate", + }, + Object { + "configMap": Object { + "name": "ca-certificate-sslyze", + }, + "name": "ca-certificate-sslyze", }, ] `); +}); + +test("should purge cascaded scan spec from parent scan", () => { + parentScan.spec.cascades.inheritEnv = true + parentScan.spec.cascades.inheritVolumes = true + const findings = [ + { + name: "Port 443 is open", + category: "Open Port", + attributes: { + state: "open", + hostname: "foobar.com", + port: 443, + service: "https" + } + } + ]; - const cascadingScanDefinition = getCascadingScanDefinition(cascadedScan, parentScan); + parentScan.spec.volumes = [ + { + "name": "ca-certificate", + "configMap": { + "name": "ca-certificate" + } + } + ] - expect(Object.entries(cascadingScanDefinition.metadata.annotations).every(([label, value]) => { - if (label === "cascading.securecodebox.io/matched-finding") { - return value === "f0c718bd-9987-42c8-2259-73794e61dd5a"; - } else return true; + sslyzeCascadingRules[0].spec.scanSpec.volumes = [ + { + "name": "ca-certificate-sslyze", + "configMap": { + "name": "ca-certificate-sslyze" + } + } + ] + + parentScan.spec.volumeMounts = [ + { + "mountPath": "/etc/ssl/certs/ca-cert.cer", + "name": "ca-certificate", + "readOnly": true, + "subPath": "ca-cert.cer" + } + ] + + sslyzeCascadingRules[0].spec.scanSpec.volumeMounts = [ + { + "mountPath": "/etc/ssl/certs/ca-cert-sslyze.cer", + "name": "ca-certificate-sslyze", + "readOnly": true, + "subPath": "ca-cert-sslyze.cer" + } + ] + + parentScan.spec.env = [ + { + "name": "parent_environment_variable_name", + "value": "parent_environment_variable_value" + } + ] + + sslyzeCascadingRules[0].spec.scanSpec.env = [ + { + "name": "rule_environment_variable_name", + "value": "rule_environment_variable_value" + } + ] + + const cascadedScans = getCascadingScans( + parentScan, + findings, + sslyzeCascadingRules + ); + + const cascadedScan = cascadedScans[0] + + // Create a second cascading rule + sslyzeCascadingRules[1] = { + apiVersion: "cascading.securecodebox.io/v1", + kind: "CascadingRule", + metadata: { + name: "tls-scans-second" + }, + spec: { + matches: { + anyOf: [ + { + category: "Open Port", + attributes: { + port: 443, + service: "https" + } + }, + { + category: "Open Port", + attributes: { + service: "https" + } + } + ] + }, + scanSpec: { + scanType: "sslyze", + parameters: ["--regular", "{{$.hostOrIP}}:{{attributes.port}}"] + } + } } - )).toBe(true) + + cascadedScan.metadata.name = cascadedScan.metadata.generateName + + const secondCascadedScans = getCascadingScans( + cascadedScan, + findings, + sslyzeCascadingRules, + sslyzeCascadingRules[0] // cascaded rule on parent + ); + + const secondCascadedScan = secondCascadedScans[0]; + + expect(secondCascadedScan.spec.env).toMatchInlineSnapshot(` + Array [ + Object { + "name": "parent_environment_variable_name", + "value": "parent_environment_variable_value", + }, + ] + `) + + expect(secondCascadedScan.spec.volumes).toMatchInlineSnapshot(` + Array [ + Object { + "configMap": Object { + "name": "ca-certificate", + }, + "name": "ca-certificate", + }, + ] + `) + + expect(secondCascadedScan.spec.volumeMounts).toMatchInlineSnapshot(` + Array [ + Object { + "mountPath": "/etc/ssl/certs/ca-cert.cer", + "name": "ca-certificate", + "readOnly": true, + "subPath": "ca-cert.cer", + }, + ] + `) + +}); + +test("should not copy cascaded scan spec from parent scan if inheritance is undefined", () => { + const findings = [ + { + name: "Port 443 is open", + category: "Open Port", + attributes: { + state: "open", + hostname: "foobar.com", + port: 443, + service: "https" + } + } + ]; + + parentScan.spec.volumes = [ + { + "name": "ca-certificate", + "configMap": { + "name": "ca-certificate" + } + } + ] + + sslyzeCascadingRules[0].spec.scanSpec.volumes = [ + { + "name": "ca-certificate-sslyze", + "configMap": { + "name": "ca-certificate-sslyze" + } + } + ] + + parentScan.spec.volumeMounts = [ + { + "mountPath": "/etc/ssl/certs/ca-cert.cer", + "name": "ca-certificate", + "readOnly": true, + "subPath": "ca-cert.cer" + } + ] + + sslyzeCascadingRules[0].spec.scanSpec.volumeMounts = [ + { + "mountPath": "/etc/ssl/certs/ca-cert-sslyze.cer", + "name": "ca-certificate-sslyze", + "readOnly": true, + "subPath": "ca-cert-sslyze.cer" + } + ] + + parentScan.spec.env = [ + { + "name": "parent_environment_variable_name", + "value": "parent_environment_variable_value" + } + ] + + sslyzeCascadingRules[0].spec.scanSpec.env = [ + { + "name": "rule_environment_variable_name", + "value": "rule_environment_variable_value" + } + ] + + const cascadedScans = getCascadingScans( + parentScan, + findings, + sslyzeCascadingRules + ); + + const cascadedScan = cascadedScans[0] + // Create a second cascading rule + sslyzeCascadingRules[1] = { + apiVersion: "cascading.securecodebox.io/v1", + kind: "CascadingRule", + metadata: { + name: "tls-scans-second" + }, + spec: { + matches: { + anyOf: [ + { + category: "Open Port", + attributes: { + port: 443, + service: "https" + } + }, + { + category: "Open Port", + attributes: { + service: "https" + } + } + ] + }, + scanSpec: { + scanType: "sslyze", + parameters: ["--regular", "{{$.hostOrIP}}:{{attributes.port}}"] + } + } + } + + cascadedScan.metadata.name = cascadedScan.metadata.generateName + + const secondCascadedScans = getCascadingScans( + cascadedScan, + findings, + sslyzeCascadingRules, + sslyzeCascadingRules[0] // cascaded rule on parent + ); + + const secondCascadedScan = secondCascadedScans[0]; + + expect(secondCascadedScan.spec.env).toMatchInlineSnapshot(`Array []`) + + expect(secondCascadedScan.spec.volumes).toMatchInlineSnapshot(`Array []`) + + expect(secondCascadedScan.spec.volumeMounts).toMatchInlineSnapshot(`Array []`) + }); diff --git a/hooks/cascading-scans/hook/hook.ts b/hooks/cascading-scans/hook/hook.ts index 260826b680..df69548f79 100644 --- a/hooks/cascading-scans/hook/hook.ts +++ b/hooks/cascading-scans/hook/hook.ts @@ -9,12 +9,14 @@ import * as Mustache from "mustache"; import { startSubsequentSecureCodeBoxScan, getCascadingRulesForScan, - getCascadingScanDefinition, // types Scan, Finding, CascadingRule, - ExtendedScanSpec + getCascadedRuleForScan, + purgeCascadedRuleFromScan, + mergeInheritedMap, + mergeInheritedArray } from "./scan-helpers"; interface HandleArgs { @@ -25,12 +27,12 @@ interface HandleArgs { export async function handle({ scan, getFindings }: HandleArgs) { const findings = await getFindings(); const cascadingRules = await getCascadingRules(scan); + const cascadedRuleUsedForParentScan = await getCascadedRuleForScan(scan); - const cascadingScans = getCascadingScans(scan, findings, cascadingRules); + const cascadingScans = getCascadingScans(scan, findings, cascadingRules, cascadedRuleUsedForParentScan); for (const cascadingScan of cascadingScans) { - const cascadingScanDefinition = getCascadingScanDefinition(cascadingScan, scan); - await startSubsequentSecureCodeBoxScan(cascadingScanDefinition); + await startSubsequentSecureCodeBoxScan(cascadingScan); } } @@ -46,11 +48,14 @@ async function getCascadingRules(scan: Scan): Promise> { export function getCascadingScans( parentScan: Scan, findings: Array, - cascadingRules: Array -): Array { - let cascadingScans: Array = []; + cascadingRules: Array, + cascadedRuleUsedForParentScan: CascadingRule +): Array { + let cascadingScans: Array = []; const cascadingRuleChain = getScanChain(parentScan); + parentScan = purgeCascadedRuleFromScan(parentScan, cascadedRuleUsedForParentScan); + for (const cascadingRule of cascadingRules) { // Check if the Same CascadingRule was already applied in the Cascading Chain // If it has already been used skip this rule as it could potentially lead to loops @@ -67,7 +72,7 @@ export function getCascadingScans( return cascadingScans; } -function getScanChain(parentScan: Scan) { +export function getScanChain(parentScan: Scan) { // Get the current Scan Chain (meaning which CascadingRules were used to start this scan and its parents) and convert it to a set, which makes it easier to query. if ( parentScan.metadata.annotations && @@ -81,7 +86,7 @@ function getScanChain(parentScan: Scan) { } function getScansMatchingRule(parentScan: Scan, findings: Array, cascadingRule: CascadingRule) { - const cascadingScans: Array = []; + const cascadingScans: Array = []; for (const finding of findings) { // Check if one (ore more) of the CascadingRule matchers apply to the finding const matches = cascadingRule.spec.matches.anyOf.some(matchesRule => @@ -100,8 +105,79 @@ function getCascadingScan( finding: Finding, cascadingRule: CascadingRule ) { - const { scanType, parameters, env } = cascadingRule.spec.scanSpec; + cascadingRule = templateCascadingRule(parentScan, finding, cascadingRule); + + let { scanType, parameters } = cascadingRule.spec.scanSpec; + + let { annotations, labels, env, volumes, volumeMounts } = mergeCascadingRuleWithScan(parentScan, cascadingRule); + + let cascadingChain: Array = []; + if (parentScan.metadata.annotations && parentScan.metadata.annotations["cascading.securecodebox.io/chain"]) { + cascadingChain = parentScan.metadata.annotations[ + "cascading.securecodebox.io/chain" + ].split(","); + } + + return { + apiVersion: "execution.securecodebox.io/v1", + kind: "Scan", + metadata: { + generateName: `${generateCascadingScanName(parentScan, cascadingRule)}-`, + labels, + annotations: { + "securecodebox.io/hook": "cascading-scans", + "cascading.securecodebox.io/parent-scan": parentScan.metadata.name, + "cascading.securecodebox.io/matched-finding": finding.id, + "cascading.securecodebox.io/chain": [ + ...cascadingChain, + cascadingRule.metadata.name + ].join(","), + ...annotations, + }, + ownerReferences: [ + { + apiVersion: "execution.securecodebox.io/v1", + blockOwnerDeletion: true, + controller: true, + kind: "Scan", + name: parentScan.metadata.name, + uid: parentScan.metadata.uid + } + ] + }, + spec: { + scanType, + parameters, + cascades: parentScan.spec.cascades, + env, + volumes, + volumeMounts, + } + }; +} + +function mergeCascadingRuleWithScan( + scan: Scan, + cascadingRule: CascadingRule +) { + const { scanAnnotations, scanLabels } = cascadingRule.spec; + let { env = [], volumes = [], volumeMounts = [] } = cascadingRule.spec.scanSpec; + let { inheritAnnotations, inheritLabels, inheritEnv, inheritVolumes } = scan.spec.cascades; + return { + annotations: mergeInheritedMap(scan.metadata.annotations, scanAnnotations, inheritAnnotations), + labels: mergeInheritedMap(scan.metadata.labels, scanLabels, inheritLabels), + env: mergeInheritedArray(scan.spec.env, env, inheritEnv), + volumes: mergeInheritedArray(scan.spec.volumes, volumes, inheritVolumes), + volumeMounts: mergeInheritedArray(scan.spec.volumeMounts, volumeMounts, inheritVolumes) + } +} + +function templateCascadingRule( + parentScan: Scan, + finding: Finding, + cascadingRule: CascadingRule +): CascadingRule { const templateArgs = { ...finding, ...parentScan, @@ -112,21 +188,19 @@ function getCascadingScan( } }; - return { - name: generateCascadingScanName(parentScan, cascadingRule), - scanType: Mustache.render(scanType, templateArgs), - parameters: parameters.map(parameter => - Mustache.render(parameter, templateArgs) - ), - cascades: parentScan.spec.cascades, - generatedBy: cascadingRule.metadata.name, - env, - scanLabels: cascadingRule.spec.scanLabels === undefined ? {} : - mapValues(cascadingRule.spec.scanLabels, value => Mustache.render(value, templateArgs)), - scanAnnotations: cascadingRule.spec.scanAnnotations === undefined ? {} : - mapValues(cascadingRule.spec.scanAnnotations, value => Mustache.render(value, templateArgs)), - finding - }; + const { scanSpec, scanAnnotations, scanLabels } = cascadingRule.spec; + const { scanType, parameters } = scanSpec; + + cascadingRule.spec.scanSpec.scanType = + Mustache.render(scanType, templateArgs); + cascadingRule.spec.scanSpec.parameters = + parameters.map(parameter => Mustache.render(parameter, templateArgs)) + cascadingRule.spec.scanAnnotations = + scanAnnotations === undefined ? {} :mapValues(scanAnnotations, value => Mustache.render(value, templateArgs)) + cascadingRule.spec.scanLabels = + scanLabels === undefined ? {} : mapValues(scanLabels, value => Mustache.render(value, templateArgs)) + + return cascadingRule; } function generateCascadingScanName( diff --git a/hooks/cascading-scans/hook/scan-helpers.ts b/hooks/cascading-scans/hook/scan-helpers.ts index 34f3b280af..c777eed87c 100644 --- a/hooks/cascading-scans/hook/scan-helpers.ts +++ b/hooks/cascading-scans/hook/scan-helpers.ts @@ -8,6 +8,8 @@ import { generateSelectorString, LabelSelector } from "./kubernetes-label-selector"; +import {isEqual} from "lodash"; +import {getScanChain} from "./hook"; // configure k8s client const kc = new k8s.KubeConfig(); @@ -57,105 +59,32 @@ export interface ScanSpec { parameters: Array; cascades: LabelSelector & CascadingInheritance; env?: Array; + volumes?: Array; + volumeMounts?: Array; } export interface CascadingInheritance { inheritLabels: boolean, - inheritAnnotations: boolean + inheritAnnotations: boolean, + inheritEnv: boolean, + inheritVolumes: boolean } -export interface ExtendedScanSpec extends ScanSpec { - // This is the name of the scan. Its not "really" part of the scan spec - // But this makes the object smaller - name: string; - - // Indicates which CascadingRule was used to generate the resulting Scan - generatedBy: string; - - // Additional label to be added to the resulting scan - scanLabels: { - [key: string]: string; - }; - - // Additional annotations to be added to the resulting scan - scanAnnotations: { - [key: string]: string; - }; - - // Finding that triggered the scan - finding: Finding -} - -export function getCascadingScanDefinition({ - name, - scanType, - parameters, - generatedBy, - env, - cascades, - scanLabels, - scanAnnotations, - finding - }: ExtendedScanSpec, parentScan: Scan) { - function mergeInherited(parentProps, ruleProps, inherit: boolean = true) { - if (!inherit) { - parentProps = {}; - } - return { - ...parentProps, - ...ruleProps // ruleProps overwrites any duplicate keys from parentProps - } +export function mergeInheritedMap(parentProps, ruleProps, inherit: boolean = true) { + if (!inherit) { + parentProps = {}; } - - let annotations = mergeInherited( - parentScan.metadata.annotations, scanAnnotations, parentScan.spec.cascades.inheritAnnotations); - let labels = mergeInherited( - parentScan.metadata.labels, scanLabels, parentScan.spec.cascades.inheritLabels); - - let cascadingChain: Array = []; - - if (parentScan.metadata.annotations && parentScan.metadata.annotations["cascading.securecodebox.io/chain"]) { - cascadingChain = parentScan.metadata.annotations[ - "cascading.securecodebox.io/chain" - ].split(","); + return { + ...parentProps, + ...ruleProps // ruleProps overwrites any duplicate keys from parentProps } +} - return { - apiVersion: "execution.securecodebox.io/v1", - kind: "Scan", - metadata: { - generateName: `${name}-`, - labels: { - ...labels - }, - annotations: { - "securecodebox.io/hook": "cascading-scans", - "cascading.securecodebox.io/parent-scan": parentScan.metadata.name, - "cascading.securecodebox.io/matched-finding": finding.id, - "cascading.securecodebox.io/chain": [ - ...cascadingChain, - generatedBy - ].join(","), - ...annotations, - }, - ownerReferences: [ - { - apiVersion: "execution.securecodebox.io/v1", - blockOwnerDeletion: true, - controller: true, - kind: "Scan", - name: parentScan.metadata.name, - uid: parentScan.metadata.uid - } - ] - }, - spec: { - scanType, - parameters, - cascades, - env, - } - }; +export function mergeInheritedArray(parentArray, ruleArray, inherit: boolean = false) { + if (!inherit) { + parentArray = []; + } + return (parentArray || []).concat(ruleArray) // CascadingRule's env overwrites scan's env } export async function startSubsequentSecureCodeBoxScan(scan: Scan) { @@ -209,3 +138,57 @@ export async function getCascadingRulesForScan(scan: Scan) { process.exit(1); } } + +// To ensure that the environment variables and volumes from the cascading rule are only applied to the matched scan +// (and not its children), this function purges the cascading rule spec from the parent scan when inheriting them. +export function purgeCascadedRuleFromScan(scan: Scan, cascadedRuleUsedForParentScan?: CascadingRule) : Scan { + // If there was no cascading rule applied to the parent scan, then ignore no purging is necessary. + if (cascadedRuleUsedForParentScan === undefined) return scan; + + if (scan.spec.env !== undefined && cascadedRuleUsedForParentScan.spec.scanSpec.env !== undefined) { + scan.spec.env = scan.spec.env.filter(scanEnv => + !cascadedRuleUsedForParentScan.spec.scanSpec.env.some(ruleEnv => isEqual(scanEnv, ruleEnv)) + ); + } + + if (scan.spec.volumes !== undefined && cascadedRuleUsedForParentScan.spec.scanSpec.volumes !== undefined) { + scan.spec.volumes = scan.spec.volumes.filter(scanVolume => + !cascadedRuleUsedForParentScan.spec.scanSpec.volumes.some(ruleVolume => isEqual(scanVolume, ruleVolume)) + ); + } + + if (scan.spec.volumeMounts !== undefined && cascadedRuleUsedForParentScan.spec.scanSpec.volumeMounts !== undefined) { + scan.spec.volumeMounts = scan.spec.volumeMounts.filter(scanVolumeMount => + !cascadedRuleUsedForParentScan.spec.scanSpec.volumeMounts.some(ruleVolumeMount => isEqual(scanVolumeMount, ruleVolumeMount)) + ); + } + + return scan +} + +export async function getCascadedRuleForScan(scan: Scan) { + const chain = getScanChain(scan) + + if (chain.length === 0) return undefined; + + return await getCascadingRule(chain[chain.length - 1]); +} + +async function getCascadingRule(ruleName) { + try { + const response: any = await k8sApiCRD.getNamespacedCustomObject( + "cascading.securecodebox.io", + "v1", + namespace, + "cascadingrules", + ruleName + ); + + console.log(`Fetched CascadingRule "${ruleName}" that triggered parent scan`); + return response.body; + } catch (err) { + console.error(`Failed to get CascadingRule "${ruleName}" from the kubernetes api`); + console.error(err); + process.exit(1); + } +} diff --git a/hooks/persistence-defectdojo/hook/src/test/java/io/securecodebox/persistence/mapping/DefectDojoFindingToSecureCodeBoxMapperTest.java b/hooks/persistence-defectdojo/hook/src/test/java/io/securecodebox/persistence/mapping/DefectDojoFindingToSecureCodeBoxMapperTest.java index b6d8fcc2a2..0066ae8edb 100644 --- a/hooks/persistence-defectdojo/hook/src/test/java/io/securecodebox/persistence/mapping/DefectDojoFindingToSecureCodeBoxMapperTest.java +++ b/hooks/persistence-defectdojo/hook/src/test/java/io/securecodebox/persistence/mapping/DefectDojoFindingToSecureCodeBoxMapperTest.java @@ -15,6 +15,7 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import java.time.LocalDateTime; import java.util.List; import static org.junit.jupiter.api.Assertions.*; @@ -54,23 +55,23 @@ public void shouldMapBasicFindings(){ var actualFinding = this.mapper.fromDefectDojoFinding(ddFinding); assertEquals( - actualFinding.getName(), - "Content Security Policy (CSP) Header Not Set" + "Content Security Policy (CSP) Header Not Set", + actualFinding.getName() ); assertEquals( - actualFinding.getCategory(), - "DefectDojo Imported Finding" + "DefectDojo Imported Finding", + actualFinding.getCategory() ); assertEquals( - actualFinding.getSeverity(), - SecureCodeBoxFinding.Severities.MEDIUM + SecureCodeBoxFinding.Severities.MEDIUM, + actualFinding.getSeverity() ); assertEquals( - actualFinding.getLocation(), - "http://juice-shop.securecodebox-test.svc:3000" + "http://juice-shop.securecodebox-test.svc:3000", + actualFinding.getLocation() ); } diff --git a/operator/apis/execution/v1/scan_types.go b/operator/apis/execution/v1/scan_types.go index 11a624c276..db0650dffc 100644 --- a/operator/apis/execution/v1/scan_types.go +++ b/operator/apis/execution/v1/scan_types.go @@ -22,6 +22,14 @@ type CascadeSpec struct { // +optional InheritAnnotations bool `json:"inheritAnnotations,omitempty"` + // InheritEnv defines whether cascading scans should inherit environment variables from the parent scan + // +optional + InheritEnv bool `json:"inheritEnv,omitempty"` + + // InheritVolumes defines whether cascading scans should inherit volumes and volume mounts from the parent scan + // +optional + InheritVolumes bool `json:"inheritVolumes,omitempty"` + // matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels // map is equivalent to an element of matchExpressions, whose key field is "key", the // operator is "In", and the values array contains only "value". The requirements are ANDed. diff --git a/operator/config/crd/bases/cascading.securecodebox.io_cascadingrules.yaml b/operator/config/crd/bases/cascading.securecodebox.io_cascadingrules.yaml index 1bd877e6a4..e181c76b28 100644 --- a/operator/config/crd/bases/cascading.securecodebox.io_cascadingrules.yaml +++ b/operator/config/crd/bases/cascading.securecodebox.io_cascadingrules.yaml @@ -108,10 +108,19 @@ spec: description: InheritAnnotations defines whether cascading scans should inherit annotations from the parent scan type: boolean + inheritEnv: + description: InheritEnv defines whether cascading scans should + inherit environment variables from the parent scan + type: boolean inheritLabels: description: InheritLabels defines whether cascading scans should inherit labels from the parent scan type: boolean + inheritVolumes: + description: InheritVolumes defines whether cascading scans + should inherit volumes and volume mounts from the parent + scan + type: boolean matchExpressions: description: matchExpressions is a list of label selector requirements. The requirements are ANDed. diff --git a/operator/config/crd/bases/execution.securecodebox.io_scans.yaml b/operator/config/crd/bases/execution.securecodebox.io_scans.yaml index 68444c933a..cc38bfe839 100644 --- a/operator/config/crd/bases/execution.securecodebox.io_scans.yaml +++ b/operator/config/crd/bases/execution.securecodebox.io_scans.yaml @@ -70,10 +70,18 @@ spec: description: InheritAnnotations defines whether cascading scans should inherit annotations from the parent scan type: boolean + inheritEnv: + description: InheritEnv defines whether cascading scans should + inherit environment variables from the parent scan + type: boolean inheritLabels: description: InheritLabels defines whether cascading scans should inherit labels from the parent scan type: boolean + inheritVolumes: + description: InheritVolumes defines whether cascading scans should + inherit volumes and volume mounts from the parent scan + type: boolean matchExpressions: description: matchExpressions is a list of label selector requirements. The requirements are ANDed. diff --git a/operator/config/crd/bases/execution.securecodebox.io_scheduledscans.yaml b/operator/config/crd/bases/execution.securecodebox.io_scheduledscans.yaml index 9445cd79cf..3264a249a8 100644 --- a/operator/config/crd/bases/execution.securecodebox.io_scheduledscans.yaml +++ b/operator/config/crd/bases/execution.securecodebox.io_scheduledscans.yaml @@ -84,10 +84,19 @@ spec: description: InheritAnnotations defines whether cascading scans should inherit annotations from the parent scan type: boolean + inheritEnv: + description: InheritEnv defines whether cascading scans should + inherit environment variables from the parent scan + type: boolean inheritLabels: description: InheritLabels defines whether cascading scans should inherit labels from the parent scan type: boolean + inheritVolumes: + description: InheritVolumes defines whether cascading scans + should inherit volumes and volume mounts from the parent + scan + type: boolean matchExpressions: description: matchExpressions is a list of label selector requirements. The requirements are ANDed. diff --git a/operator/crds/cascading.securecodebox.io_cascadingrules.yaml b/operator/crds/cascading.securecodebox.io_cascadingrules.yaml index d7884f860d..2982914c46 100644 --- a/operator/crds/cascading.securecodebox.io_cascadingrules.yaml +++ b/operator/crds/cascading.securecodebox.io_cascadingrules.yaml @@ -108,10 +108,19 @@ spec: description: InheritAnnotations defines whether cascading scans should inherit annotations from the parent scan type: boolean + inheritEnv: + description: InheritEnv defines whether cascading scans should + inherit environment variables from the parent scan + type: boolean inheritLabels: description: InheritLabels defines whether cascading scans should inherit labels from the parent scan type: boolean + inheritVolumes: + description: InheritVolumes defines whether cascading scans + should inherit volumes and volume mounts from the parent + scan + type: boolean matchExpressions: description: matchExpressions is a list of label selector requirements. The requirements are ANDed. diff --git a/operator/crds/execution.securecodebox.io_scans.yaml b/operator/crds/execution.securecodebox.io_scans.yaml index dda21a180d..1b27eb2650 100644 --- a/operator/crds/execution.securecodebox.io_scans.yaml +++ b/operator/crds/execution.securecodebox.io_scans.yaml @@ -70,10 +70,18 @@ spec: description: InheritAnnotations defines whether cascading scans should inherit annotations from the parent scan type: boolean + inheritEnv: + description: InheritEnv defines whether cascading scans should + inherit environment variables from the parent scan + type: boolean inheritLabels: description: InheritLabels defines whether cascading scans should inherit labels from the parent scan type: boolean + inheritVolumes: + description: InheritVolumes defines whether cascading scans should + inherit volumes and volume mounts from the parent scan + type: boolean matchExpressions: description: matchExpressions is a list of label selector requirements. The requirements are ANDed. diff --git a/operator/crds/execution.securecodebox.io_scheduledscans.yaml b/operator/crds/execution.securecodebox.io_scheduledscans.yaml index 00af99d3c7..49745f32d6 100644 --- a/operator/crds/execution.securecodebox.io_scheduledscans.yaml +++ b/operator/crds/execution.securecodebox.io_scheduledscans.yaml @@ -84,10 +84,19 @@ spec: description: InheritAnnotations defines whether cascading scans should inherit annotations from the parent scan type: boolean + inheritEnv: + description: InheritEnv defines whether cascading scans should + inherit environment variables from the parent scan + type: boolean inheritLabels: description: InheritLabels defines whether cascading scans should inherit labels from the parent scan type: boolean + inheritVolumes: + description: InheritVolumes defines whether cascading scans + should inherit volumes and volume mounts from the parent + scan + type: boolean matchExpressions: description: matchExpressions is a list of label selector requirements. The requirements are ANDed.