FIXES: Added wazuh integration and loads more

This commit is contained in:
frikky
2020-12-22 11:41:11 +01:00
parent 2248e407f7
commit 8a99fb6048
17 changed files with 333 additions and 93 deletions
+33 -18
View File
@@ -606,11 +606,14 @@ class AppBase:
if "len" in thistype or "length" in thistype or "lenght" in thistype:
tmp = ""
try:
tmpdata = data.replace("\'", "\"")
tmp = json.loads(tmpdata)
except:
print("Passing bug")
pass
try:
tmpdata = data.replace("\'", "\"")
tmp = json.loads(tmpdata)
except:
print("[ERROR] Parsing bug for length in app sdk")
pass
if isinstance(tmp, list):
return len(tmp)
@@ -649,14 +652,14 @@ class AppBase:
return tmp
except IndexError as e:
return default_error
# Parses the INNER value and recurses until everything is done
def parse_wrapper(data):
try:
if "(" not in data or ")" not in data:
return data
return (data, False)
except TypeError:
return data
return (data, False)
#print("Running %s" % data)
@@ -671,7 +674,7 @@ class AppBase:
break
if not found:
return data
return (data, False)
# Do stuff here.
innervalue = parse_nested_param(data, maxDepth(data)-0)
@@ -692,10 +695,11 @@ class AppBase:
parsed_value = parse_type(innervalue[0], thistype.lower())
print("Parsed value from %s: %s" % (thistype, parsed_value))
return parsed_value
return (parsed_value, True)
print("DATA: %s\n" % data)
return parse_wrapper(data)
return (parse_wrapper(data)[0], True)
# Looks for parantheses to grab special cases within a string, e.g:
# int(1) lower(HELLO) or length(what's the length)
@@ -737,15 +741,20 @@ class AppBase:
if len(newstring) > 0:
newdata.append(newstring)
print("Newdata: ", newdata)
parsedlist = []
non_string = False
parsed = False
for item in newdata:
ret = parse_wrapper(item)
if not isinstance(ret, str):
if not isinstance(ret[0], str):
non_string = True
parsedlist.append(ret)
parsedlist.append(ret[0])
if ret[1]:
parsed = True
if not parsed:
return data
if len(parsedlist) > 0 and not non_string:
print("Returning parsed list: ", parsedlist)
@@ -949,7 +958,6 @@ class AppBase:
if len(parsersplit) == 1:
return str(baseresult)+str(appendresult), False
baseresult = baseresult.replace("\'", "\"")
baseresult = baseresult.replace(" True,", " true,")
baseresult = baseresult.replace(" False", " false,")
@@ -958,8 +966,12 @@ class AppBase:
try:
basejson = json.loads(baseresult)
except json.decoder.JSONDecodeError as e:
print("Parser issue with JSON: %s" % e)
return str(baseresult)+str(appendresult), False
try:
baseresult = baseresult.replace("\'", "\"")
basejson = json.loads(baseresult)
except json.decoder.JSONDecodeError as e:
print("Parser issue with JSON: %s" % e)
return str(baseresult)+str(appendresult), False
print("After fourth parser return as JSON")
@@ -1365,14 +1377,17 @@ class AppBase:
if replacement.startswith("\"") and replacement.endswith("\""):
replacement = replacement[1:len(replacement)-1]
replacement = replacement.replace("\'", "\"", -1)
print("POST replacement: %s" % replacement)
json_replacement = replacement
try:
json_replacement = json.loads(replacement)
except json.decoder.JSONDecodeError as e:
print("JSON error singular: %s" % e)
try:
replacement = replacement.replace("\'", "\"", -1)
json_replacement = json.loads(replacement)
except:
print("JSON error singular: %s" % e)
if len(json_replacement) > minlength:
minlength = len(json_replacement)
+1 -1
View File
@@ -1,6 +1,6 @@
#!/bin/bash
NAME=shuffle-app_sdk
VERSION=0.8.32
VERSION=0.8.4
docker rmi docker.pkg.github.com/frikky/shuffle/$NAME:$VERSION --force
docker build . -t frikky/shuffle:app_sdk -t frikky/$NAME:$VERSION -t docker.pkg.github.com/frikky/shuffle/$NAME:$VERSION -t ghcr.io/frikky/$NAME:$VERSION
+1
View File
@@ -230,6 +230,7 @@ func buildImageMemory(fs billy.Filesystem, tags []string, dockerfileFolder strin
}
// Build the actual image
log.Printf("Building %s. This may take up to a few minutes.", dockerfileFolder)
imageBuildResponse, err := client.ImageBuild(
ctx,
dockerFileTarReader,
+7 -7
View File
@@ -2483,7 +2483,7 @@ func handleLogin(resp http.ResponseWriter, request *http.Request) {
if len(users) != 1 {
log.Printf(`Found multiple or no users with the same username: %s: %d`, data.Username, len(users))
resp.WriteHeader(401)
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Found %d users with the same username: %s"}`, len(users), data.Username)))
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Error: %d users with username %s"}`, len(users), data.Username)))
return
}
@@ -4785,7 +4785,7 @@ func getDocList(resp http.ResponseWriter, request *http.Request) {
if len(item1) == 0 {
resp.WriteHeader(500)
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "No docs available."`)))
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "No docs available."}`)))
return
}
@@ -4844,7 +4844,7 @@ func getDocs(resp http.ResponseWriter, request *http.Request) {
location := strings.Split(request.URL.String(), "/")
if len(location) != 5 {
resp.WriteHeader(404)
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Bad path. Use e.g. /api/v1/docs/workflows.md"`)))
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Bad path. Use e.g. /api/v1/docs/workflows.md"}`)))
return
}
@@ -4868,7 +4868,7 @@ func getDocs(resp http.ResponseWriter, request *http.Request) {
)
if err != nil {
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Bad path. Use e.g. /api/v1/docs/workflows.md"`)))
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Bad path. Use e.g. /api/v1/docs/workflows.md"}`)))
resp.WriteHeader(404)
//setBadMemcache(ctx, docPath)
return
@@ -4877,7 +4877,7 @@ func getDocs(resp http.ResponseWriter, request *http.Request) {
newresp, err := client.Do(req)
if err != nil {
resp.WriteHeader(404)
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Bad path. Use e.g. /api/v1/docs/workflows.md"`)))
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Bad path. Use e.g. /api/v1/docs/workflows.md"}`)))
//setBadMemcache(ctx, docPath)
return
}
@@ -4885,7 +4885,7 @@ func getDocs(resp http.ResponseWriter, request *http.Request) {
body, err := ioutil.ReadAll(newresp.Body)
if err != nil {
resp.WriteHeader(500)
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Can't parse data"`)))
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Can't parse data"}`)))
//setBadMemcache(ctx, docPath)
return
}
@@ -5958,7 +5958,7 @@ func echoOpenapiData(resp http.ResponseWriter, request *http.Request) {
if err != nil {
log.Printf("[ERROR] URLbody error: %s", err)
resp.WriteHeader(500)
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Can't get data from selected uri"`)))
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Can't get data from selected uri"}`)))
return
}
+3 -3
View File
@@ -4015,7 +4015,7 @@ func deleteWorkflowApp(resp http.ResponseWriter, request *http.Request) {
log.Printf("ID: %s", fileId)
app, err := getApp(ctx, fileId)
if err != nil {
log.Printf("Error getting app %s: %s", app.Name, err)
log.Printf("Error getting app (delete) %s: %s", fileId, err)
resp.WriteHeader(401)
resp.Write([]byte(`{"success": false}`))
return
@@ -4169,7 +4169,7 @@ func getWorkflowAppConfig(resp http.ResponseWriter, request *http.Request) {
ctx := context.Background()
app, err := getApp(ctx, fileId)
if err != nil {
log.Printf("Error getting app: %s", app.Name)
log.Printf("Error getting app (app config): %s", fileId)
resp.WriteHeader(401)
resp.Write([]byte(`{"success": false}`))
return
@@ -4438,7 +4438,7 @@ func updateWorkflowAppConfig(resp http.ResponseWriter, request *http.Request) {
ctx := context.Background()
app, err := getApp(ctx, fileId)
if err != nil {
log.Printf("Error getting app: %s (update app)", app.Name)
log.Printf("Error getting app (update app): %s", fileId)
resp.WriteHeader(401)
resp.Write([]byte(`{"success": false}`))
return
+2 -2
View File
@@ -2,7 +2,7 @@ version: '3'
services:
frontend:
#build: ./frontend
image: ghcr.io/frikky/shuffle-frontend:0.8.4
image: ghcr.io/frikky/shuffle-frontend:0.8.42
container_name: shuffle-frontend
hostname: shuffle-frontend
ports:
@@ -45,7 +45,7 @@ services:
- database
orborus:
#build: ./functions/onprem/orborus
image: ghcr.io/frikky/shuffle-orborus:0.8.31
image: ghcr.io/frikky/shuffle-orborus:0.8.32
container_name: shuffle-orborus
hostname: shuffle-orborus
networks:
+28
View File
@@ -470,6 +470,33 @@ const Admin = (props) => {
})
}
const flushQueue = (name) => {
// Just use this one?
const url = globalUrl + '/api/v1/flush_queue';
fetch(url, {
method: 'DELETE',
credentials: "include",
headers: {
'Content-Type': 'application/json',
},
})
.then(response =>
response.json().then(responseJson => {
if (responseJson["success"] === false) {
alert.error(responseJson.reason)
getEnvironments()
} else {
setLoginInfo("")
setModalOpen(false)
getEnvironments()
}
}),
)
.catch(error => {
console.log("Error when deleting: ", error)
})
}
const deleteEnvironment = (name) => {
// FIXME - add some check here ROFL
alert.info("Deleting environment " + name)
@@ -2094,6 +2121,7 @@ const Admin = (props) => {
style={{minWidth: 150, maxWidth: 150, overflow: "hidden"}}
>
<Button disabled={environment.archived} variant="outlined" style={{borderRadius: "0px"}} onClick={() => deleteEnvironment(environment.Name)} color="primary">Archive</Button>
{/*<Button disabled={environment.archived} variant="outlined" style={{borderRadius: "0px"}} onClick={() => flushQueue(environment.Name)} color="primary">Flush Queue</Button>*/}
</ListItemText>
<ListItemText
style={{minWidth: 150, maxWidth: 150, overflow: "hidden"}}
+31 -9
View File
@@ -192,7 +192,7 @@ const AngularWorkflow = (props) => {
const cloudSyncEnabled = props.userdata !== undefined && props.userdata.active_org !== null && props.userdata.active_org !== undefined ? props.userdata.active_org.cloud_sync === true : false
//const triggerEnvironments = cloudSyncEnabled ? ["cloud", "onprem"] : environments
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io"
const triggerEnvironments = isCloud ? ["cloud"] : ["cloud", "onprem"]
const triggerEnvironments = isCloud ? ["cloud"] : ["onprem", "cloud"]
const unloadText = 'Are you sure you want to leave without saving (CTRL+S)?'
useBeforeunload(() => {
@@ -2663,7 +2663,6 @@ const AngularWorkflow = (props) => {
foundResult.result = foundResult.result.split(" None").join(" \"None\"")
foundResult.result = foundResult.result.split(" False").join(" false")
foundResult.result = foundResult.result.split(" True").join(" true")
foundResult.result = foundResult.result.split("\'").join("\"")
var jsonvalid = true
try {
@@ -2672,7 +2671,15 @@ const AngularWorkflow = (props) => {
jsonvalid = false
}
} catch (e) {
jsonvalid = false
try {
foundResult.result = foundResult.result.split("\'").join("\"")
const tmp = String(JSON.parse(foundResult.result))
if (!foundResult.result.includes("{") && !foundResult.result.includes("[")) {
jsonvalid = false
}
} catch (e) {
jsonvalid = false
}
}
// Finds the FIRST json only
@@ -5874,7 +5881,6 @@ const AngularWorkflow = (props) => {
const parsedExecutionArgument = () => {
var showResult = executionData.execution_argument.trim()
showResult = showResult.split(" None").join(" \"None\"")
showResult = showResult.split("\'").join("\"")
showResult = showResult.split(" False").join(" false")
showResult = showResult.split(" True").join(" true")
@@ -5885,7 +5891,16 @@ const AngularWorkflow = (props) => {
jsonvalid = false
}
} catch (e) {
jsonvalid = false
showResult = showResult.split("\'").join("\"")
try {
const tmp = String(JSON.parse(showResult))
if (!showResult.includes("{") && !showResult.includes("[")) {
jsonvalid = false
}
} catch (e) {
jsonvalid = false
}
}
if (jsonvalid) {
@@ -6070,21 +6085,28 @@ const AngularWorkflow = (props) => {
//
// FIXME: The latter replace doens't really work if ' is used in a string
var showResult = data.result.trim()
//console.log(showResult)
showResult = showResult.split(" None").join(" \"None\"")
showResult = showResult.split(" False").join(" false")
showResult = showResult.split(" True").join(" true")
showResult = showResult.split("\'").join("\"")
var jsonvalid = true
try {
const tmp = String(JSON.parse(showResult))
if (!showResult.includes("{") && !showResult.includes("[")) {
//console.log("IN HERE: ", tmp)
jsonvalid = false
}
} catch (e) {
//console.log("Error: ", e)
jsonvalid = false
showResult = showResult.split("\'").join("\"")
try {
const tmp = String(JSON.parse(showResult))
if (!showResult.includes("{") && !showResult.includes("[")) {
jsonvalid = false
}
} catch (e) {
jsonvalid = false
}
}
const curapp = apps.find(a => a.name === data.action.app_name && a.app_version === data.action.app_version)
+4
View File
@@ -46,6 +46,10 @@ const inputColor = "#383B40"
export const GetParsedPaths = (inputdata, basekey) => {
const splitkey = " > "
var parsedValues = []
if (inputdata === undefined || inputdata === null) {
return parsedValues
}
if (typeof(inputdata) !== "object") {
return parsedValues
}
+3 -2
View File
@@ -69,7 +69,7 @@ const LoginDialog = props => {
}),
)
.catch(error => {
setLoginInfo("Error in userdata: ", error)
setLoginInfo("Error logging in: ", error)
})
}
@@ -80,6 +80,7 @@ const LoginDialog = props => {
const onSubmit = (e) => {
e.preventDefault()
setLoginInfo("")
// FIXME - add some check here ROFL
// Just use this one?
@@ -114,7 +115,7 @@ const LoginDialog = props => {
}),
)
.catch(error => {
setLoginInfo("Error in userdata: " + error)
setLoginInfo("Error logging in: " + error)
});
} else {
url = baseurl + '/api/v1/users/register';
+36
View File
@@ -0,0 +1,36 @@
#!/bin/sh
# Created by Shuffle, AS. <frikky@shuffler.io>.
WPYTHON_BIN="framework/python/bin/python3"
SCRIPT_PATH_NAME="$0"
DIR_NAME="$(cd $(dirname ${SCRIPT_PATH_NAME}); pwd -P)"
SCRIPT_NAME="$(basename ${SCRIPT_PATH_NAME})"
case ${DIR_NAME} in
*/active-response/bin | */wodles*)
if [ -z "${WAZUH_PATH}" ]; then
WAZUH_PATH="$(cd ${DIR_NAME}/../..; pwd)"
fi
PYTHON_SCRIPT="${DIR_NAME}/${SCRIPT_NAME}.py"
;;
*/bin)
if [ -z "${WAZUH_PATH}" ]; then
WAZUH_PATH="$(cd ${DIR_NAME}/..; pwd)"
fi
PYTHON_SCRIPT="${WAZUH_PATH}/framework/scripts/${SCRIPT_NAME}.py"
;;
*/integrations)
if [ -z "${WAZUH_PATH}" ]; then
WAZUH_PATH="$(cd ${DIR_NAME}/..; pwd)"
fi
PYTHON_SCRIPT="${DIR_NAME}/${SCRIPT_NAME}.py"
;;
esac
${WAZUH_PATH}/${WPYTHON_BIN} ${PYTHON_SCRIPT} "$@"
@@ -0,0 +1,177 @@
#!/usr/bin/env python
# Created by Shuffle, AS. <frikky@shuffler.io>.
# Based on the Slack integration using Webhooks
import json
import sys
import time
import os
try:
import requests
from requests.auth import HTTPBasicAuth
except Exception as e:
print("No module 'requests' found. Install: pip install requests")
sys.exit(1)
# ADD THIS TO ossec.conf configuration:
# <integration>
# <name>custom-shuffle</name>
# <hook_url>http://<IP>:3001/api/v1/hooks/<HOOK_ID></hook_url>
# <level>3</level>
# <alert_format>json</alert_format>
# </integration>
# Global vars
debug_enabled = False
pwd = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
json_alert = {}
now = time.strftime("%a %b %d %H:%M:%S %Z %Y")
# Set paths
log_file = '{0}/logs/integrations.log'.format(pwd)
def main(args):
debug("# Starting")
# Read args
alert_file_location = args[1]
webhook = args[3]
debug("# Webhook")
debug(webhook)
debug("# File location")
debug(alert_file_location)
# Load alert. Parse JSON object.
with open(alert_file_location) as alert_file:
json_alert = json.load(alert_file)
debug("# Processing alert")
debug(json_alert)
debug("# Generating message")
msg = generate_msg(json_alert)
if isinstance(msg, str):
if len(msg) == 0:
return
debug(msg)
debug("# Sending message")
send_msg(msg, webhook)
def debug(msg):
if debug_enabled:
msg = "{0}: {1}\n".format(now, msg)
print(msg)
f = open(log_file, "a")
f.write(msg)
f.close()
# Skips container kills to stop self-recursion
def filter_msg(alert):
# These are things that recursively happen because Shuffle starts Docker containers
# Docker integration rules: https://github.com/wazuh/wazuh-ruleset/blob/ae36745db1d3f312db0392f5925c2f2b0ec009a9/rules/0560-docker_integration_rules.xml
skip = ["87924", "87900", "87901", "87902", "87903", "87904", "86001", "86002", "86003", "87932", "80710", "87929", "87928",]
if alert["rule"]["id"] in skip:
return False
#try:
# if "docker" in alert["rule"]["description"].lower() and "
#msg['text'] = alert.get('full_log')
#except:
# pass
#msg['title'] = alert['rule']['description'] if 'description' in alert['rule'] else "N/A"
return True
def generate_msg(alert):
if not filter_msg(alert):
print("Skipping rule %s" % alert["rule"]["id"])
return ""
level = alert['rule']['level']
if (level <= 4):
color = "good"
elif (level >= 5 and level <= 7):
color = "warning"
else:
color = "danger"
msg = {}
msg['color'] = color
msg['pretext'] = "WAZUH Alert"
msg['title'] = alert['rule']['description'] if 'description' in alert['rule'] else "N/A"
msg['text'] = alert.get('full_log')
msg['rule_id'] = alert["rule"]["id"]
msg['timestamp'] = alert["timestamp"]
msg['id'] = alert['id']
msg["all_fields"] = alert
#msg['fields'] = []
# msg['fields'].append({
# "title": "Agent",
# "value": "({0}) - {1}".format(
# alert['agent']['id'],
# alert['agent']['name']
# ),
# })
#if 'agentless' in alert:
# msg['fields'].append({
# "title": "Agentless Host",
# "value": alert['agentless']['host'],
# })
#msg['fields'].append({"title": "Location", "value": alert['location']})
#msg['fields'].append({
# "title": "Rule ID",
# "value": "{0} _(Level {1})_".format(alert['rule']['id'], level),
#})
#attach = {'attachments': [msg]}
return json.dumps(msg)
def send_msg(msg, url):
headers = {'content-type': 'application/json', 'Accept-Charset': 'UTF-8'}
res = requests.post(url, data=msg, headers=headers)
debug(res)
if __name__ == "__main__":
try:
# Read arguments
bad_arguments = False
if len(sys.argv) >= 4:
msg = '{0} {1} {2} {3} {4}'.format(
now,
sys.argv[1],
sys.argv[2],
sys.argv[3],
sys.argv[4] if len(sys.argv) > 4 else '',
)
debug_enabled = (len(sys.argv) > 4 and sys.argv[4] == 'debug')
else:
msg = '{0} Wrong arguments'.format(now)
bad_arguments = True
# Logging the call
f = open(log_file, 'a')
f.write(msg + '\n')
f.close()
if bad_arguments:
debug("# Exiting: Bad arguments.")
sys.exit(1)
# Main function
main(sys.argv)
except Exception as e:
debug(str(e))
raise
+1 -3
View File
@@ -1,7 +1,5 @@
<integration>
<name>Shuffle</name>
<name>custom-shuffle</name>
<hook_url>http://<IP>:3001/api/v1/hooks/webhook_<HOOK_ID></hook_url>
<level>2</level>
<group>multiple_drops|authentication_failures</group>
<alert_format>json</alert_format>
</integration>
@@ -1 +0,0 @@
requests
-44
View File
@@ -1,44 +0,0 @@
#!/usr/bin/env python
# Based on
# https://wazuh.com/blog/how-to-integrate-external-software-using-integrator/
import sys
import json
import requests
from requests.auth import HTTPBasicAuth
# Set the project attributes
project_alias = 'TI'
issue_name ='FIM'
# Read configuration parameters
alert_file = open(sys.argv[1])
user = sys.argv[2].split(':')[0]
api_key = sys.argv[2].split(':')[1]
hook_url = sys.argv[3]
# Read the alert file
alert_json = json.loads(alert_file.read())
alert_file.close()
# Extract issue fields
alert_level = alert_json['rule']['level']
description = alert_json['rule']['description']
path = alert_json['syscheck']['path']
# Generate request
msg_data = {}
msg_data['fields'] = {}
msg_data['fields']['project'] = {}
msg_data['fields']['project']['key'] = project_alias
msg_data['fields']['summary'] = 'FIM alert on [' + path + ']'
msg_data['fields']['description'] = '- State: ' + description + '\n- Alert level: ' + str(alert_level)
msg_data['fields']['issuetype'] = {}
msg_data['fields']['issuetype']['name'] = issue_name
headers = {'content-type': 'application/json', 'Accept-Charset': 'UTF-8'}
# Send the request
requests.post(hook_url, data=json.dumps(msg_data), headers=headers, auth=(user, api_key))
sys.exit(0)
+1 -1
View File
@@ -1,5 +1,5 @@
NAME=shuffle-orborus
VERSION=0.8.31
VERSION=0.8.32
echo "Running docker build with $NAME:$VERSION"
#docker rmi frikky/shuffle:$NAME --force
+5 -2
View File
@@ -562,6 +562,7 @@ func zombiecheck(workerTimeout int) error {
stopContainers := []string{}
removeContainers := []string{}
log.Printf("Workertimeout: %d", int64(workerTimeout))
for _, container := range containers {
// Skip random containers. Only handle things related to Shuffle.
if !strings.Contains(container.Image, baseimagename) {
@@ -587,10 +588,10 @@ func zombiecheck(workerTimeout int) error {
continue
}
log.Printf("[INFO] NAME: %s", name)
currenttime := time.Now().Unix()
log.Printf("[INFO] (%s) NAME: %s. TIME: %d", container.State, name, currenttime-container.Created)
// Need to check time here too because a container can be removed the same instant as its created
currenttime := time.Now().Unix()
if container.State != "running" && currenttime-container.Created > int64(workerTimeout) {
removeContainers = append(removeContainers, container.ID)
containerNames[container.ID] = name
@@ -606,6 +607,7 @@ func zombiecheck(workerTimeout int) error {
}
// FIXME - add killing of apps with same execution ID too
log.Printf("[INFO] Should STOP %d containers.", len(stopContainers))
for _, containername := range stopContainers {
log.Printf("[INFO] Stopping and removing container %s", containerNames[containername])
go dockercli.ContainerStop(ctx, containername, nil)
@@ -617,6 +619,7 @@ func zombiecheck(workerTimeout int) error {
Force: true,
}
log.Printf("[INFO] Should REMOVE %d containers.", len(removeContainers))
for _, containername := range removeContainers {
go dockercli.ContainerRemove(ctx, containername, removeOptions)
}