Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 1 addition & 2 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -907,8 +907,7 @@ jobs:
--set="parser.env[0].name=CRASH_ON_FAILED_VALIDATION" \
--set-string="parser.env[0].value=true"
kubectl apply -f ./scanners/zap-advanced/integration-tests/scantype-configMap.yaml -n integration-tests
cd tests/integration/
npx jest --ci --color scanner/zap-advanced.test.js
npx jest --ci --color ./scanners/zap-advanced/integration-tests/zap-advanced.test.js

# ---- Debuging Cluster on Failure ----

Expand Down
2 changes: 1 addition & 1 deletion scanners.mk
Original file line number Diff line number Diff line change
Expand Up @@ -81,4 +81,4 @@ deploy-with-scanner:
integration-tests:
@echo ".: 🩺 Starting integration test in kind namespace 'integration-tests'."
kubectl -n integration-tests delete scans --all
cd ../../tests/integration/ && npm ci && npx --yes --package jest@$(JEST_VERSION) jest --verbose --ci --colors --coverage --passWithNoTests ${scanner-prefix}/${name}.test.js
cd .. && npm ci && cd $(scanner)/integration-tests && npm run test --yes --package jest@$(JEST_VERSION) $(scanner)/integration-tests
7 changes: 0 additions & 7 deletions scanners/amass/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,3 @@ include_guard = set
scanner = amass

include ../../scanners.mk

integration-tests:
@echo ".: 🩺 Starting integration test in kind namespace 'integration-tests'."
kubectl -n integration-tests delete scans --all
cd ../../tests/integration/ && npm ci
cd ../../scanners/${scanner}
npx --yes --package jest@$(JEST_VERSION) jest --verbose --ci --colors --coverage --passWithNoTests ${scanner}/integration-tests
3 changes: 1 addition & 2 deletions scanners/amass/integration-tests/amass.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@
//
// SPDX-License-Identifier: Apache-2.0

const { scan } = require("../../../tests/integration/helpers.js");

const {scan} = require("../../helpers");
jest.retryTimes(3);

test(
Expand Down
9 changes: 1 addition & 8 deletions scanners/cmseek/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,4 @@ custom_scanner = set

include ../../scanners.mk

deploy-test-deps: | deploy-test-dep-old-joomla

integration-tests:
@echo ".: 🩺 Starting integration test in kind namespace 'integration-tests'."
kubectl -n integration-tests delete scans --all
cd ../../tests/integration/ && npm ci
cd ../../scanners/${scanner}
npx --yes --package jest@$(JEST_VERSION) jest --verbose --ci --colors --coverage --passWithNoTests ${scanner}/integration-tests
deploy-test-deps: | deploy-test-dep-old-joomla
2 changes: 1 addition & 1 deletion scanners/cmseek/integration-tests/cmseek.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//
// SPDX-License-Identifier: Apache-2.0

const { scan } = require("../../../tests/integration/helpers");
const {scan} = require("../../helpers");

jest.retryTimes(3);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//
// SPDX-License-Identifier: Apache-2.0

const { scan } = require("../../../tests/integration/helpers");
const {scan} = require("../../helpers");

jest.retryTimes(3);

Expand Down
2 changes: 1 addition & 1 deletion scanners/gitleaks/integration-tests/gitleaks.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//
// SPDX-License-Identifier: Apache-2.0

const { scan } = require("../../../tests/integration/helpers");
const {scan} = require("../../helpers");

jest.retryTimes(3);

Expand Down
284 changes: 284 additions & 0 deletions scanners/helpers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,284 @@
// SPDX-FileCopyrightText: the secureCodeBox authors
Comment thread
Weltraumschaf marked this conversation as resolved.
//
// SPDX-License-Identifier: Apache-2.0

const k8s = require("@kubernetes/client-node");

const kc = new k8s.KubeConfig();
kc.loadFromDefault();

const k8sCRDApi = kc.makeApiClient(k8s.CustomObjectsApi);
const k8sBatchApi = kc.makeApiClient(k8s.BatchV1Api);
const k8sPodsApi = kc.makeApiClient(k8s.CoreV1Api);

let namespace = "integration-tests";

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms * 1000));

async function deleteScan(name) {
await k8sCRDApi.deleteNamespacedCustomObject(
"execution.securecodebox.io",
"v1",
namespace,
"scans",
name,
{}
);
}

async function getScan(name) {
const { body: scan } = await k8sCRDApi.getNamespacedCustomObjectStatus(
"execution.securecodebox.io",
"v1",
namespace,
"scans",
name
);
return scan;
}

async function displayAllLogsForJob(jobName) {
console.log(`Listing logs for Job '${jobName}':`);
const {
body: { items: pods },
} = await k8sPodsApi.listNamespacedPod(
namespace,
true,
undefined,
undefined,
undefined,
`job-name=${jobName}`
);

if (pods.length === 0) {
console.log(`No Pods found for Job '${jobName}'`);
}

for (const pod of pods) {
console.log(
`Listing logs for Job '${jobName}' > Pod '${pod.metadata.name}':`
);

for (const container of pod.spec.containers) {
try {
const response = await k8sPodsApi.readNamespacedPodLog(
pod.metadata.name,
namespace,
container.name
);
console.log(`Container ${container.name}:`);
console.log(response.body);
} catch (exception) {
console.error(
`Failed to display logs of container ${container.name}: ${exception.body.message}`
);
}
}
}
}

async function logJobs() {
try {
const { body: jobs } = await k8sBatchApi.listNamespacedJob(namespace);

console.log("Logging spec & status of jobs in namespace");

for (const job of jobs.items) {
console.log(`Job: '${job.metadata.name}' Spec:`);
console.log(JSON.stringify(job.spec, null, 2));
console.log(`Job: '${job.metadata.name}' Status:`);
console.log(JSON.stringify(job.status, null, 2));

await displayAllLogsForJob(job.metadata.name);
}
} catch (error) {
console.error("Failed to list Jobs");
console.error(error);
}
}

async function disasterRecovery(scanName) {
const scan = await getScan(scanName);
console.error("Last Scan State:");
console.dir(scan);
await logJobs();
}

/**
*
* @param {string} name name of the scan. Actual name will be suffixed with a random number to avoid conflicts
* @param {string} scanType type of the scan. Must match the name of a ScanType CRD
* @param {string[]} parameters cli argument to be passed to the scanner
* @param {number} timeout in seconds
* @param {object[]} volumes definitions for kubernetes volumes that should be used. Optional, useful for initContainers (see below)
* @param {object[]} volumeMounts definitions for kubernetes volume mounts that should be used. Optional, useful for initContainers (see below)
* @param {object[]} initContainers definitions for initContainers that should be added to the scan job to provision files for the scanner. Optional.
* @returns {scan.findings} returns findings { categories, severities, count }
*/
async function scan(name, scanType, parameters = [], timeout = 180, volumes = [], volumeMounts = [], initContainers = []) {
namespace ="integration-tests"
const scanDefinition = {
apiVersion: "execution.securecodebox.io/v1",
kind: "Scan",
metadata: {
// Use `generateName` instead of name to generate a random suffix and avoid name clashes
generateName: `${name}-`,
},
spec: {
scanType,
parameters,
volumes,
volumeMounts,
initContainers,
},
};

const { body } = await k8sCRDApi.createNamespacedCustomObject(
"execution.securecodebox.io",
"v1",
namespace,
"scans",
scanDefinition
);

const actualName = body.metadata.name;

for (let i = 0; i < timeout; i++) {
await sleep(1);
const { status } = await getScan(actualName);

if (status && status.state === "Done") {
// Wait a couple seconds to give kubernetes more time to update the fields
await sleep(2);
const { status } = await getScan(actualName);
await deleteScan(actualName);
return status.findings;
} else if (status && status.state === "Errored") {
console.error("Scan Errored");
await disasterRecovery(actualName);

throw new Error(
`Scan failed with description "${status.errorDescription}"`
);
}
}
console.error("Scan Timed out!");
await disasterRecovery(actualName);

throw new Error("timed out while waiting for scan results");
}

/**
*
* @param {string} name name of the scan. Actual name will be sufixed with a random number to avoid conflicts
* @param {string} scanType type of the scan. Must match the name of a ScanType CRD
* @param {string[]} parameters cli argument to be passed to the scanner
* @param {string} nameCascade name of cascading scan
* @param {object} matchLabels set invasive and intensive of cascading scan
* @param {number} timeout in seconds
* @returns {scan.findings} returns findings { categories, severities, count }
*/
async function cascadingScan(name, scanType, parameters = [], { nameCascade, matchLabels }, timeout = 180) {
const scanDefinition = {
apiVersion: "execution.securecodebox.io/v1",
kind: "Scan",
metadata: {
// Use `generateName` instead of name to generate a random suffix and avoid name clashes
generateName: `${name}-`,
},
spec: {
scanType,
parameters,
cascades: {
matchLabels,
}
},
};

const { body } = await k8sCRDApi.createNamespacedCustomObject(
"execution.securecodebox.io",
"v1",
namespace,
"scans",
scanDefinition
);

const actualName = body.metadata.name;

for (let i = 0; i < timeout; i++) {
await sleep(1);
const { status } = await getScan(actualName);

if (status && status.state === "Done") {
// Wait a couple seconds to give kubernetes more time to update the fields
await sleep(5);
console.log("First Scan finished")
console.log(`First Scan Status: ${JSON.stringify(status, undefined, 2)}`)

break;
} else if (status && status.state === "Errored") {
console.error("Scan Errored");
await disasterRecovery(actualName);
throw new Error(
`Initial Scan failed with description "${status.errorDescription}"`
);
}

if (i === (timeout - 1)) {
throw new Error(
`Initial Scan timed out failed`
);
}
}

const { body: scans } = await k8sCRDApi.listNamespacedCustomObject(
"execution.securecodebox.io",
"v1",
namespace,
"scans"
);

let cascadedScan = null;

for (const scan of scans.items) {
if (scan.metadata.annotations && scan.metadata.annotations["cascading.securecodebox.io/chain"] === nameCascade) {
cascadedScan = scan;
break;
}
}

if (cascadedScan === null) {
console.warn(`Didn't find matching cascaded scan in available scans: ${JSON.stringify(scans.items, undefined, 2)}`)
throw new Error(`Didn't find cascaded Scan for ${nameCascade}`)
}
const actualNameCascade = cascadedScan.metadata.name;

for (let j = 0; j < timeout; j++) {
await sleep(1)
const { status: statusCascade } = await getScan(actualNameCascade);

if (statusCascade && statusCascade.state === "Done") {
await sleep(2);
const { status: statusCascade } = await getScan(actualNameCascade);

await deleteScan(actualName);
await deleteScan(actualNameCascade);
return statusCascade.findings;
} else if (statusCascade && statusCascade.state === "Errored") {
console.error("Scan Errored");
await disasterRecovery(actualName);
await disasterRecovery(actualNameCascade);
throw new Error(
`Cascade Scan failed with description "${statusCascade.errorDescription}"`
);
}
}
console.error("Cascade Scan Timed out!");
await disasterRecovery(actualName);
await disasterRecovery(actualNameCascade);

throw new Error("timed out while waiting for scan results");
}

module.exports.scan = scan;
module.exports.cascadingScan = cascadingScan;
7 changes: 0 additions & 7 deletions scanners/kube-hunter/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,3 @@ scanner = kube-hunter
custom_scanner = set

include ../../scanners.mk

integration-tests:
@echo ".: 🩺 Starting integration test in kind namespace 'integration-tests'."
kubectl -n integration-tests delete scans --all
cd ../../tests/integration/ && npm ci
cd ../../scanners/${scanner}
npx --yes --package jest@$(JEST_VERSION) jest --verbose --ci --colors --coverage --passWithNoTests ${scanner}/integration-tests
2 changes: 1 addition & 1 deletion scanners/kube-hunter/integration-tests/kube-hunter.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//
// SPDX-License-Identifier: Apache-2.0

const { scan } = require("../../../tests/integration/helpers");
const {scan} = require("../../helpers");

jest.retryTimes(3);

Expand Down
7 changes: 0 additions & 7 deletions scanners/kubeaudit/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,3 @@ deploy-test-deps:
kubectl create namespace kubeaudit-tests --dry-run=client -o yaml | kubectl apply -f -
# Install jshop in kubeaudit-tests namespace
helm -n kubeaudit-tests upgrade --install juice-shop ../../demo-targets/juice-shop/ --wait

integration-tests:
@echo ".: 🩺 Starting integration test in kind namespace 'integration-tests'."
kubectl -n integration-tests delete scans --all
cd ../../tests/integration/ && npm ci
cd ../../scanners/${scanner}
npx --yes --package jest@$(JEST_VERSION) jest --verbose --ci --colors --coverage --passWithNoTests ${scanner}/integration-tests
Loading