Skip to content

Commit ef300f6

Browse files
Merge pull request #1033 from secureCodeBox/maintenance/generic-makefile-scanners
Improves the integration-tests makefile process for scanners
2 parents 7805c0e + f608fd5 commit ef300f6

46 files changed

Lines changed: 2361 additions & 222 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yaml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -825,8 +825,7 @@ jobs:
825825
--set="parser.env[0].name=CRASH_ON_FAILED_VALIDATION" \
826826
--set-string="parser.env[0].value=true"
827827
kubectl apply -f ./scanners/zap-advanced/integration-tests/scantype-configMap.yaml -n integration-tests
828-
cd tests/integration/
829-
npx jest --ci --color scanner/zap-advanced.test.js
828+
npx jest --ci --color ./scanners/zap-advanced/integration-tests/zap-advanced.test.js
830829
831830
# ---- Debuging Cluster on Failure ----
832831

scanners.mk

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,4 +81,4 @@ deploy-with-scanner:
8181
integration-tests:
8282
@echo ".: 🩺 Starting integration test in kind namespace 'integration-tests'."
8383
kubectl -n integration-tests delete scans --all
84-
cd ../../tests/integration/ && npm ci && npx --yes --package jest@$(JEST_VERSION) jest --verbose --ci --colors --coverage --passWithNoTests ${scanner-prefix}/${name}.test.js
84+
cd .. && npm ci && cd $(scanner)/integration-tests && npm run test --yes --package jest@$(JEST_VERSION) $(scanner)/integration-tests

scanners/amass/Makefile

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,3 @@ include_guard = set
99
scanner = amass
1010

1111
include ../../scanners.mk
12-
13-
integration-tests:
14-
@echo ".: 🩺 Starting integration test in kind namespace 'integration-tests'."
15-
kubectl -n integration-tests delete scans --all
16-
cd ../../tests/integration/ && npm ci
17-
cd ../../scanners/${scanner}
18-
npx --yes --package jest@$(JEST_VERSION) jest --verbose --ci --colors --coverage --passWithNoTests ${scanner}/integration-tests

scanners/amass/integration-tests/amass.test.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@
22
//
33
// SPDX-License-Identifier: Apache-2.0
44

5-
const { scan } = require("../../../tests/integration/helpers.js");
6-
5+
const {scan} = require("../../helpers");
76
jest.retryTimes(3);
87

98
test(

scanners/cmseek/Makefile

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,4 @@ custom_scanner = set
1111

1212
include ../../scanners.mk
1313

14-
deploy-test-deps: | deploy-test-dep-old-joomla
15-
16-
integration-tests:
17-
@echo ".: 🩺 Starting integration test in kind namespace 'integration-tests'."
18-
kubectl -n integration-tests delete scans --all
19-
cd ../../tests/integration/ && npm ci
20-
cd ../../scanners/${scanner}
21-
npx --yes --package jest@$(JEST_VERSION) jest --verbose --ci --colors --coverage --passWithNoTests ${scanner}/integration-tests
14+
deploy-test-deps: | deploy-test-dep-old-joomla

scanners/cmseek/integration-tests/cmseek.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
//
33
// SPDX-License-Identifier: Apache-2.0
44

5-
const { scan } = require("../../../tests/integration/helpers");
5+
const {scan} = require("../../helpers");
66

77
jest.retryTimes(3);
88

scanners/git-repo-scanner/integration-tests/git-repo-scanner.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
//
33
// SPDX-License-Identifier: Apache-2.0
44

5-
const { scan } = require("../../../tests/integration/helpers");
5+
const {scan} = require("../../helpers");
66

77
jest.retryTimes(3);
88

scanners/gitleaks/integration-tests/gitleaks.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
//
33
// SPDX-License-Identifier: Apache-2.0
44

5-
const { scan } = require("../../../tests/integration/helpers");
5+
const {scan} = require("../../helpers");
66

77
jest.retryTimes(0);
88

scanners/helpers.js

Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
1+
// SPDX-FileCopyrightText: the secureCodeBox authors
2+
//
3+
// SPDX-License-Identifier: Apache-2.0
4+
5+
const k8s = require("@kubernetes/client-node");
6+
7+
const kc = new k8s.KubeConfig();
8+
kc.loadFromDefault();
9+
10+
const k8sCRDApi = kc.makeApiClient(k8s.CustomObjectsApi);
11+
const k8sBatchApi = kc.makeApiClient(k8s.BatchV1Api);
12+
const k8sPodsApi = kc.makeApiClient(k8s.CoreV1Api);
13+
14+
let namespace = "integration-tests";
15+
16+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms * 1000));
17+
18+
async function deleteScan(name) {
19+
await k8sCRDApi.deleteNamespacedCustomObject(
20+
"execution.securecodebox.io",
21+
"v1",
22+
namespace,
23+
"scans",
24+
name,
25+
{}
26+
);
27+
}
28+
29+
async function getScan(name) {
30+
const { body: scan } = await k8sCRDApi.getNamespacedCustomObjectStatus(
31+
"execution.securecodebox.io",
32+
"v1",
33+
namespace,
34+
"scans",
35+
name
36+
);
37+
return scan;
38+
}
39+
40+
async function displayAllLogsForJob(jobName) {
41+
console.log(`Listing logs for Job '${jobName}':`);
42+
const {
43+
body: { items: pods },
44+
} = await k8sPodsApi.listNamespacedPod(
45+
namespace,
46+
true,
47+
undefined,
48+
undefined,
49+
undefined,
50+
`job-name=${jobName}`
51+
);
52+
53+
if (pods.length === 0) {
54+
console.log(`No Pods found for Job '${jobName}'`);
55+
}
56+
57+
for (const pod of pods) {
58+
console.log(
59+
`Listing logs for Job '${jobName}' > Pod '${pod.metadata.name}':`
60+
);
61+
62+
for (const container of pod.spec.containers) {
63+
try {
64+
const response = await k8sPodsApi.readNamespacedPodLog(
65+
pod.metadata.name,
66+
namespace,
67+
container.name
68+
);
69+
console.log(`Container ${container.name}:`);
70+
console.log(response.body);
71+
} catch (exception) {
72+
console.error(
73+
`Failed to display logs of container ${container.name}: ${exception.body.message}`
74+
);
75+
}
76+
}
77+
}
78+
}
79+
80+
async function logJobs() {
81+
try {
82+
const { body: jobs } = await k8sBatchApi.listNamespacedJob(namespace);
83+
84+
console.log("Logging spec & status of jobs in namespace");
85+
86+
for (const job of jobs.items) {
87+
console.log(`Job: '${job.metadata.name}' Spec:`);
88+
console.log(JSON.stringify(job.spec, null, 2));
89+
console.log(`Job: '${job.metadata.name}' Status:`);
90+
console.log(JSON.stringify(job.status, null, 2));
91+
92+
await displayAllLogsForJob(job.metadata.name);
93+
}
94+
} catch (error) {
95+
console.error("Failed to list Jobs");
96+
console.error(error);
97+
}
98+
}
99+
100+
async function disasterRecovery(scanName) {
101+
const scan = await getScan(scanName);
102+
console.error("Last Scan State:");
103+
console.dir(scan);
104+
await logJobs();
105+
}
106+
107+
/**
108+
*
109+
* @param {string} name name of the scan. Actual name will be suffixed with a random number to avoid conflicts
110+
* @param {string} scanType type of the scan. Must match the name of a ScanType CRD
111+
* @param {string[]} parameters cli argument to be passed to the scanner
112+
* @param {number} timeout in seconds
113+
* @param {object[]} volumes definitions for kubernetes volumes that should be used. Optional, useful for initContainers (see below)
114+
* @param {object[]} volumeMounts definitions for kubernetes volume mounts that should be used. Optional, useful for initContainers (see below)
115+
* @param {object[]} initContainers definitions for initContainers that should be added to the scan job to provision files for the scanner. Optional.
116+
* @returns {scan.findings} returns findings { categories, severities, count }
117+
*/
118+
async function scan(name, scanType, parameters = [], timeout = 180, volumes = [], volumeMounts = [], initContainers = []) {
119+
namespace ="integration-tests"
120+
const scanDefinition = {
121+
apiVersion: "execution.securecodebox.io/v1",
122+
kind: "Scan",
123+
metadata: {
124+
// Use `generateName` instead of name to generate a random suffix and avoid name clashes
125+
generateName: `${name}-`,
126+
},
127+
spec: {
128+
scanType,
129+
parameters,
130+
volumes,
131+
volumeMounts,
132+
initContainers,
133+
},
134+
};
135+
136+
const { body } = await k8sCRDApi.createNamespacedCustomObject(
137+
"execution.securecodebox.io",
138+
"v1",
139+
namespace,
140+
"scans",
141+
scanDefinition
142+
);
143+
144+
const actualName = body.metadata.name;
145+
146+
for (let i = 0; i < timeout; i++) {
147+
await sleep(1);
148+
const { status } = await getScan(actualName);
149+
150+
if (status && status.state === "Done") {
151+
// Wait a couple seconds to give kubernetes more time to update the fields
152+
await sleep(2);
153+
const { status } = await getScan(actualName);
154+
await deleteScan(actualName);
155+
return status.findings;
156+
} else if (status && status.state === "Errored") {
157+
console.error("Scan Errored");
158+
await disasterRecovery(actualName);
159+
160+
throw new Error(
161+
`Scan failed with description "${status.errorDescription}"`
162+
);
163+
}
164+
}
165+
console.error("Scan Timed out!");
166+
await disasterRecovery(actualName);
167+
168+
throw new Error("timed out while waiting for scan results");
169+
}
170+
171+
/**
172+
*
173+
* @param {string} name name of the scan. Actual name will be sufixed with a random number to avoid conflicts
174+
* @param {string} scanType type of the scan. Must match the name of a ScanType CRD
175+
* @param {string[]} parameters cli argument to be passed to the scanner
176+
* @param {string} nameCascade name of cascading scan
177+
* @param {object} matchLabels set invasive and intensive of cascading scan
178+
* @param {number} timeout in seconds
179+
* @returns {scan.findings} returns findings { categories, severities, count }
180+
*/
181+
async function cascadingScan(name, scanType, parameters = [], { nameCascade, matchLabels }, timeout = 180) {
182+
const scanDefinition = {
183+
apiVersion: "execution.securecodebox.io/v1",
184+
kind: "Scan",
185+
metadata: {
186+
// Use `generateName` instead of name to generate a random suffix and avoid name clashes
187+
generateName: `${name}-`,
188+
},
189+
spec: {
190+
scanType,
191+
parameters,
192+
cascades: {
193+
matchLabels,
194+
}
195+
},
196+
};
197+
198+
const { body } = await k8sCRDApi.createNamespacedCustomObject(
199+
"execution.securecodebox.io",
200+
"v1",
201+
namespace,
202+
"scans",
203+
scanDefinition
204+
);
205+
206+
const actualName = body.metadata.name;
207+
208+
for (let i = 0; i < timeout; i++) {
209+
await sleep(1);
210+
const { status } = await getScan(actualName);
211+
212+
if (status && status.state === "Done") {
213+
// Wait a couple seconds to give kubernetes more time to update the fields
214+
await sleep(5);
215+
console.log("First Scan finished")
216+
console.log(`First Scan Status: ${JSON.stringify(status, undefined, 2)}`)
217+
218+
break;
219+
} else if (status && status.state === "Errored") {
220+
console.error("Scan Errored");
221+
await disasterRecovery(actualName);
222+
throw new Error(
223+
`Initial Scan failed with description "${status.errorDescription}"`
224+
);
225+
}
226+
227+
if (i === (timeout - 1)) {
228+
throw new Error(
229+
`Initial Scan timed out failed`
230+
);
231+
}
232+
}
233+
234+
const { body: scans } = await k8sCRDApi.listNamespacedCustomObject(
235+
"execution.securecodebox.io",
236+
"v1",
237+
namespace,
238+
"scans"
239+
);
240+
241+
let cascadedScan = null;
242+
243+
for (const scan of scans.items) {
244+
if (scan.metadata.annotations && scan.metadata.annotations["cascading.securecodebox.io/chain"] === nameCascade) {
245+
cascadedScan = scan;
246+
break;
247+
}
248+
}
249+
250+
if (cascadedScan === null) {
251+
console.warn(`Didn't find matching cascaded scan in available scans: ${JSON.stringify(scans.items, undefined, 2)}`)
252+
throw new Error(`Didn't find cascaded Scan for ${nameCascade}`)
253+
}
254+
const actualNameCascade = cascadedScan.metadata.name;
255+
256+
for (let j = 0; j < timeout; j++) {
257+
await sleep(1)
258+
const { status: statusCascade } = await getScan(actualNameCascade);
259+
260+
if (statusCascade && statusCascade.state === "Done") {
261+
await sleep(2);
262+
const { status: statusCascade } = await getScan(actualNameCascade);
263+
264+
await deleteScan(actualName);
265+
await deleteScan(actualNameCascade);
266+
return statusCascade.findings;
267+
} else if (statusCascade && statusCascade.state === "Errored") {
268+
console.error("Scan Errored");
269+
await disasterRecovery(actualName);
270+
await disasterRecovery(actualNameCascade);
271+
throw new Error(
272+
`Cascade Scan failed with description "${statusCascade.errorDescription}"`
273+
);
274+
}
275+
}
276+
console.error("Cascade Scan Timed out!");
277+
await disasterRecovery(actualName);
278+
await disasterRecovery(actualNameCascade);
279+
280+
throw new Error("timed out while waiting for scan results");
281+
}
282+
283+
module.exports.scan = scan;
284+
module.exports.cascadingScan = cascadingScan;

scanners/kube-hunter/Makefile

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,3 @@ scanner = kube-hunter
1010
custom_scanner = set
1111

1212
include ../../scanners.mk
13-
14-
integration-tests:
15-
@echo ".: 🩺 Starting integration test in kind namespace 'integration-tests'."
16-
kubectl -n integration-tests delete scans --all
17-
cd ../../tests/integration/ && npm ci
18-
cd ../../scanners/${scanner}
19-
npx --yes --package jest@$(JEST_VERSION) jest --verbose --ci --colors --coverage --passWithNoTests ${scanner}/integration-tests

0 commit comments

Comments
 (0)