Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
499c077
Cascading Scans: merge environment variables from parent scan and cas…
EndPositive Jun 24, 2021
0f224fa
Cascading Scans: merge volumes and volumeMounts from parent scan and …
EndPositive Jun 24, 2021
3391d74
Cascading Scans: CascadingRule's spec overwrites scan's spec
EndPositive Jun 25, 2021
b7b1856
Cascading Scans: purge cascaded rules spec from parent scan
EndPositive Jun 25, 2021
5307a1a
Cascading Scans: fix error when cascadingRule has no spec
EndPositive Jun 30, 2021
dd5f864
Merge branch 'main' into cascading-scan-spec-merge-inherit
EndPositive Jul 12, 2021
94bed3a
Cascading Scans: fix tests when running with ts-jest
EndPositive Jul 12, 2021
8312973
Cascading Scans: add inheritEnv & inheritVolumes (disabled by default)
EndPositive Jul 12, 2021
1979c3a
Operator: add inheritVolumes and inheritEnv to CRDs
EndPositive Jul 12, 2021
d15f02e
Cascading Scans: extract functions to make codeclimate happy
EndPositive Jul 12, 2021
efed9f8
Merge branch 'main' into cascading-scan-spec-merge-inherit
JohannesZahn Jul 13, 2021
8501b73
Cascading Scans: updates from code reviews
EndPositive Jul 21, 2021
2ded643
Cascading Scans: tests remove intermediate assertions on cascadedScans
EndPositive Jul 21, 2021
ffa294f
Cascading Scans: create scan definition in `getCascadingScans` and up…
EndPositive Jul 21, 2021
c538866
Cascading Scans: update getCascadedRuleForScan to use existing chain …
EndPositive Jul 21, 2021
934941b
Cascading Scans: get rid of redundant ExtendedScanSpec
EndPositive Jul 21, 2021
c99094d
Operator: update Helm CRD's
EndPositive Jul 21, 2021
e410e6a
Fix incorrect expected / actual ordering in assertions
J12934 Jul 22, 2021
fc3b806
Merge branch 'main' into pr/EndPositive/538
J12934 Jul 22, 2021
d4856d8
Trigger CI
J12934 Jul 22, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
796 changes: 559 additions & 237 deletions hooks/cascading-scans/hook/hook.test.js

Large diffs are not rendered by default.

126 changes: 100 additions & 26 deletions hooks/cascading-scans/hook/hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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);
}
}

Expand All @@ -46,11 +48,14 @@ async function getCascadingRules(scan: Scan): Promise<Array<CascadingRule>> {
export function getCascadingScans(
parentScan: Scan,
findings: Array<Finding>,
cascadingRules: Array<CascadingRule>
): Array<ExtendedScanSpec> {
let cascadingScans: Array<ExtendedScanSpec> = [];
cascadingRules: Array<CascadingRule>,
cascadedRuleUsedForParentScan: CascadingRule
): Array<Scan> {
let cascadingScans: Array<Scan> = [];
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
Expand All @@ -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 &&
Expand All @@ -81,7 +86,7 @@ function getScanChain(parentScan: Scan) {
}

function getScansMatchingRule(parentScan: Scan, findings: Array<Finding>, cascadingRule: CascadingRule) {
const cascadingScans: Array<ExtendedScanSpec> = [];
const cascadingScans: Array<Scan> = [];
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 =>
Expand All @@ -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<string> = [];
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,
Expand All @@ -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(
Expand Down
163 changes: 73 additions & 90 deletions hooks/cascading-scans/hook/scan-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -57,105 +59,32 @@ export interface ScanSpec {
parameters: Array<string>;
cascades: LabelSelector & CascadingInheritance;
env?: Array<k8s.V1EnvVar>;
volumes?: Array<k8s.V1Volume>;
volumeMounts?: Array<k8s.V1VolumeMount>;
}

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<string> = [];

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) {
Expand Down Expand Up @@ -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 <CascadingRule> 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);
}
}
Loading