Skip to content
This repository was archived by the owner on Oct 14, 2020. It is now read-only.

Add Cascading Rules for Ncrack #128

Merged
merged 8 commits into from
Oct 5, 2020
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
25 changes: 25 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,31 @@ jobs:
helm -n integration-tests install zap ./scanners/zap/ --set="parserImage.tag=sha-$(git rev-parse --short HEAD)"
cd tests/integration/
npx jest --ci --color zap
- name: "cascading Scans Integration Tests"
run: |
# We'll run these in a separate namespace so that only the cascadingRules we want to test will be used
kubectl create namespace cascading-tests
# Install declarative-subsequent-scans hook
helm upgrade --install dssh ./hooks/declarative-subsequent-scans/ -n cascading-tests
# Install nmap
helm -n cascading-tests install nmap ./scanners/nmap/ --set="parserImage.tag=sha-$(git rev-parse --short HEAD)"
# Install ncrack
printf "root\nadmin\n" > users.txt
printf "THEPASSWORDYOUCREATED\n123456\npassword\n" > passwords.txt
kubectl create secret generic --from-file users.txt --from-file passwords.txt ncrack-lists -n cascading-tests
cat <<EOF | helm -n cascading-tests install ncrack ./scanners/ncrack --set="parserImage.tag=sha-$(git rev-parse --short HEAD)" --values -
scannerJob:
extraVolumes:
- name: ncrack-lists
secret:
secretName: ncrack-lists
extraVolumeMounts:
- name: ncrack-lists
mountPath: "/ncrack/"
EOF
# Actually run the tests
cd tests/integration/
npx jest --ci --color cascade
- name: Inspect Post Failure
if: failure()
run: |
Expand Down
30 changes: 30 additions & 0 deletions scanners/ncrack/cascading-rules/crackssh.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
apiVersion: "cascading.experimental.securecodebox.io/v1"
kind: CascadingRule
metadata:
name: "ncrack-ssh"
labels:
securecodebox.io/invasive: invasive
securecodebox.io/intensive: high
spec:
matches:
anyOf:
- category: "Open Port"
attributes:
port: 22
state: open
- category: "Open Port"
attributes:
service: "ssh"
state: open
scanSpec:
scanType: "ncrack"
parameters:
- -v
- -d10
- -U
- /ncrack/users.txt
- -P
- /ncrack/passwords.txt
- -p
- ssh:{{attributes.port}}
- "{{attributes.ip_address}}"
9 changes: 9 additions & 0 deletions scanners/ncrack/templates/cascading-rules.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# The CascadingRules are not directly in the /templates directory as their curly bracket syntax clashes with helms templates ... :(
# We import them as raw files to avoid these clashes as escaping them is even more messy
{{ range $path, $_ := .Files.Glob "cascading-rules/*" }}
# Include File
{{ $.Files.Get $path }}
# Separate multiple files
---
{{ end }}

16 changes: 16 additions & 0 deletions scanners/nmap/examples/dummy-ssh-cascade/scan.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
apiVersion: "execution.experimental.securecodebox.io/v1"
kind: Scan
metadata:
name: "nmap-dummy-ssh"
spec:
scanType: "nmap"
parameters:
# Internal cluster is blocking our ping probes, therefore we skip them
- "-Pn"
# Service Detection enabled
- "-sV"
# Actual Service Address will depend on you cluster and namespace configuration. 🤷‍
- "dummy-ssh.demo-apps.svc"
cascades:
matchLabels:
securecodebox.io/intensive: high
115 changes: 114 additions & 1 deletion tests/integration/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const k8sCRDApi = kc.makeApiClient(k8s.CustomObjectsApi);
const k8sBatchApi = kc.makeApiClient(k8s.BatchV1Api);
const k8sPodsApi = kc.makeApiClient(k8s.CoreV1Api);

const namespace = "integration-tests";
let namespace = "integration-tests";

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

Expand Down Expand Up @@ -109,6 +109,7 @@ async function disasterRecovery(scanName) {
* @returns {scan.findings} returns findings { categories, severities, count }
*/
async function scan(name, scanType, parameters = [], timeout = 180) {
namespace ="integration-tests"
const scanDefinition = {
apiVersion: "execution.experimental.securecodebox.io/v1",
kind: "Scan",
Expand Down Expand Up @@ -157,4 +158,116 @@ async function scan(name, scanType, parameters = [], timeout = 180) {
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 {number} timeout in seconds
* @returns {scan.findings} returns findings { categories, severities, count }
*/
async function cascadingScan(name, scanType, parameters = [], { nameCascade, matchLabels }, timeout = 180) {
namespace = "cascading-tests";

const scanDefinition = {
apiVersion: "execution.experimental.securecodebox.io/v1",
kind: "Scan",
metadata: {
// Use `generateName` instead of name to generate a random sufix and avoid name clashes
generateName: `${name}-`,
},
spec: {
scanType,
parameters,
cascades: {
matchLabels,
}
},
};

const { body } = await k8sCRDApi.createNamespacedCustomObject(
"execution.experimental.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);

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`
);
}
}

console.log("First Scan finished")

const { body: scans } = await k8sCRDApi.listNamespacedCustomObject(
"execution.experimental.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) {
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;
33 changes: 33 additions & 0 deletions tests/integration/scanner/cascade.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
const { cascadingScan } = require('../helpers')

test(
"Cascading Scan nmap -> ncrack on dummy-ssh",
async () => {
const { categories, severities, count } = await cascadingScan(
"nmap-dummy-ssh",
"nmap",
["-Pn", "-sV", "dummy-ssh.demo-apps.svc"],
{
nameCascade: "ncrack-ssh",
matchLabels: {
"securecodebox.io/invasive": "invasive",
"securecodebox.io/intensive": "high"
}
},
120
);

expect(count).toBe(1);
expect(categories).toEqual(
{
"Discovered Credentials": 1,
}
);
expect(severities).toEqual(
{
"high": 1,
}
);
},
3 * 60 * 1000
);