├── .gitignore ├── readme.md └── vt_scan_containers.sh /.gitignore: -------------------------------------------------------------------------------- 1 | commands.log 2 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Docker Backup and VirusTotal Scanner 2 | 3 | This is a Bash script that backs up Docker images or containers, and then scans them using VirusTotal. The script uploads the Docker tar files to VirusTotal for analysis. Please read Virus Total's terms of service before using this script. 4 | 5 | ## Disclaimer 6 | 7 | 1. Use this code at your own risk. 8 | 2. The file is uploaded to VirusTotal for analysis. Please read Virus Total's terms of service before using this script. 9 | 3. VirusTotal shares uploaded files with their partners, which may include antivirus companies, researchers, and other organizations. Be cautious when uploading sensitive or proprietary files. 10 | 11 | ## How to get a VirusTotal API Key 12 | 13 | To use this script, you will need a VirusTotal API key. Follow these steps to obtain one: 14 | 15 | 1. Visit [VirusTotal](https://www.virustotal.com/) and sign up for a free account. 16 | 2. After signing up, log in to your account. 17 | 3. Navigate to your [API Key](https://www.virustotal.com/gui/user/YOUR_USERNAME/apikey) page by clicking your username in the top-right corner and selecting "API Key." 18 | 4. Your API key will be displayed on the page. Copy it and use it as the `VIRUS_TOTAL_API_KEY` value when running the script. 19 | 20 | ## Usage 21 | 22 | ```bash 23 | ./vt_scan_containers.sh --OUTPUT_FOLDER=PATH --VIRUS_TOTAL_API_KEY=KEY --EXPORT_TYPE=[image/container] [--SLACK_WEB_HOOK=URL] 24 | ``` 25 | 26 | Example: 27 | 28 | ``` 29 | ./vt_scan_containers.sh --OUTPUT_FOLDER=/mnt/container_backups/ --VIRUS_TOTAL_API_KEY=e4c0f729f84EXAMPLE539a280000000 --EXPORT_TYPE=container --SLACK_WEB_HOOK=https://hooks.slack.com/services/example/example/example 30 | ``` 31 | 32 | ### Options 33 | 34 | - `--BASE_FOLDER`: Path to the folder where the Docker backups and results will be stored. 35 | - `--VIRUS_TOTAL_API_KEY`: Your VirusTotal API key. 36 | - `--EXPORT_TYPE`: Export type can be either `image` or `container`. 37 | - `--SLACK_WEB_HOOK` (Optional): Slack webhook URL to send notifications. 38 | 39 | ## Dependencies 40 | 41 | - Docker 42 | - cincan/virustotal Docker image 43 | - cURL (for sending Slack notifications) 44 | 45 | Make sure you have Docker installed and the `cincan/virustotal` Docker image available. 46 | 47 | ## How It Works 48 | 49 | 1. The script first checks for the required dependencies. 50 | 2. It exports the Docker images or containers based on the provided `EXPORT_TYPE`. 51 | 3. The exported tar files are scanned using the VirusTotal API. 52 | 4. The script waits for VirusTotal to analyze the files. 53 | 5. The analysis results are checked for malicious or suspicious content. 54 | 6. Notifications are sent to the provided Slack webhook URL if any malicious or suspicious content is detected. 55 | 56 | Example console output: 57 | 58 | ``` 59 | Starting the container/image backup and scans. 60 | 61 | Create the OUTPUT_FOLDER: /mnt/container_backups/ 62 | 63 | Delete all files in the OUTPUT_FOLDER that does not contain .virus 64 | 65 | Exporting all containers (running and stopped): 66 | nifty_goodall 67 | 68 | nifty_goodall: 69 | - Docker save container nifty_goodall to /mnt/container_backups/nifty_goodall.tar 70 | 71 | Finished exporting all images/containers. 72 | 73 | Scanning tar file: /mnt/container_backups/nifty_goodall.tar 74 | - Upload tar file to VirusTotal for scanning: nifty_goodall.tar 75 | - Received result: /files/nifty_goodall.tar NTNlMTU3ZDQzNDAwYTkzZjEzNjAzZjA4ODY3MWRhZWU6MTY4MzAyNzkxOA== 76 | 77 | Sleeping for 30 seconds to give VirusTotal time to scan the file. 78 | 79 | Analyzing result file: nifty_goodall.tar.result 80 | - Store analysis result in file: nifty_goodall.tar.result.analysis 81 | - VirusTotal is still scanning. Retrying in 15 seconds - 0/16 82 | - Store analysis result in file: nifty_goodall.tar.result.analysis 83 | - VirusTotal is still scanning. Retrying in 30 seconds - 1/16 84 | - Store analysis result in file: nifty_goodall.tar.result.analysis 85 | - VirusTotal is still scanning. Retrying in 60 seconds - 2/16 86 | - Store analysis result in file: nifty_goodall.tar.result.analysis 87 | - VirusTotal is still scanning. Retrying in 120 seconds - 3/16 88 | - Store analysis result in file: nifty_goodall.tar.result.analysis 89 | - VirusTotal is still scanning. Retrying in 240 seconds - 4/16 90 | - Store analysis result in file: nifty_goodall.tar.result.analysis 91 | - VirusTotal gave a status of completed. - 5/16 92 | ☣ Possible malicious or suspicious file in: /files/nifty_goodall.tar NTNlMTU3ZDQzNDAwYTkzZjEzNjAzZjA4ODY3MWRhZWU6MTY4MzAyNzkxOA== 93 | ``` 94 | 95 | ## Want to connect? 96 | 97 | Feel free to contact me on [Twitter](https://twitter.com/OnlineAnto), [DEV Community](https://dev.to/antoonline/) or [LinkedIn](https://www.linkedin.com/in/anto-online) if you have any questions or suggestions. 98 | 99 | Or just visit my [website](https://anto.online) to see what I do. 100 | -------------------------------------------------------------------------------- /vt_scan_containers.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Script to backup docker images and containers and scan them with VirusTotal 4 | # 5 | # Currently only EXPORT_TYPE=container is supported. Image uploads but the status always remains "queued". 6 | # 7 | # Disclaimer : 8 | # 9 | # 1) Use this code at your own risk. 10 | # 2) The file is uploaded to VirusTotal for analysis. Please read Virus Total's terms of service before using this script. 11 | # 12 | # Visit http://anto.online for more information or give a like on GitHub: https://github.com/AntoOnline/bash-script-docker-virustotal-scan-containers 13 | # 14 | 15 | # Docker image to use for VirusTotal scanning 16 | VIRUS_TOTAL_DOCKER_IMAGE="cincan/virustotal" 17 | 18 | # Max bytes to scan (600MB) 19 | MAX_FILE_SIZE_TO_SCAN=629145600 20 | 21 | # Max number of retries to check the scan status 22 | MAX_RETRIES=16 23 | 24 | # Number of seconds to wait between retries 25 | RETRY_INTERVAL=60 26 | 27 | # Sleep seconds after uploading all files to VirusTotal 28 | SLEEP_TIME_AFTER_ALL_FILES_UPLOADED=180 29 | 30 | # Emojis 31 | VIRUS_FOUND=$(echo -e "\U2623") 32 | NO_VIRUS_FOUND=$(echo -e "\U2714") 33 | 34 | # Function to print usage instructions 35 | print_usage() { 36 | echo "Usage: $0 --OUTPUT_FOLDER=PATH --VIRUS_TOTAL_API_KEY=KEY --EXPORT_TYPE=[image/container] [--SLACK_WEB_HOOK=URL]" 37 | exit 1 38 | } 39 | 40 | # Function to sanitize the item id and remove / space and dots 41 | sanitize_item_id() { 42 | local item_id="$1" 43 | local item_id_safe 44 | 45 | # Remove / and : and . and spaces 46 | item_id_safe=$(echo "$item_id" | tr -d '/: .') 47 | 48 | # Remove she word "sha" from the beginning of the string 49 | item_id_safe=$(echo "$item_id_safe" | sed 's/^sha//') 50 | 51 | echo "$item_id_safe" 52 | } 53 | 54 | # Function to backup a docker item (image or container) 55 | backup_docker_item() { 56 | local EXPORT_TYPE="$1" 57 | local item_id="$2" 58 | local OUTPUT_FOLDER="$3" 59 | local item_id_safe="$4" 60 | 61 | if [ "$EXPORT_TYPE" = "image" ]; then 62 | printf "\n$item_id:\n" 63 | 64 | command_create="docker create '$item_id'" 65 | printf -- "- Creating a temporary container from image %s\n" "$item_id" 66 | local container_id=$(eval "$command_create") 67 | echo "$command_create" >> "${OUTPUT_FOLDER}commands.log" 68 | 69 | command_export="docker export '$container_id' > '${OUTPUT_FOLDER}${item_id_safe}.tar'" 70 | printf -- "- Docker export container %s (from image %s) to %s%s.tar\n" "$container_id" "$item_id" "$OUTPUT_FOLDER" "$item_id_safe" 71 | eval "$command_export" 2>/dev/null 72 | echo "$command_export" >> "${OUTPUT_FOLDER}commands.log" 73 | 74 | command_rm="docker rm '$container_id'" 75 | printf -- "- Removing temporary container %s\n" "$container_id" 76 | eval "$command_rm" > /dev/null 2>&1 77 | echo "$command_rm" >> "${OUTPUT_FOLDER}commands.log" 78 | 79 | elif [ "$EXPORT_TYPE" = "container" ]; then 80 | printf "\n$item_id:\n" 81 | 82 | command_export="docker export '$item_id' > '${OUTPUT_FOLDER}${item_id_safe}.tar'" 83 | printf -- "- Docker save container %s to %s%s.tar\n" "$item_id" "$OUTPUT_FOLDER" "$item_id_safe" 84 | eval "$command_export" 2>/dev/null 85 | echo "$command_export" >> "${OUTPUT_FOLDER}commands.log" 86 | fi 87 | } 88 | 89 | # Function send a Slack notification 90 | send_slack_notification() { 91 | local SLACK_WEB_HOOK="$1" 92 | local slack_message="$2" 93 | 94 | if [[ -n "$SLACK_WEB_HOOK" ]]; then 95 | curl -X POST -H 'Content-type: application/json' --data "{\"text\":\"$slack_message\"}" "$SLACK_WEB_HOOK" 96 | fi 97 | } 98 | 99 | # Parse arguments 100 | for arg in "$@"; do 101 | case $arg in 102 | --OUTPUT_FOLDER=*) 103 | OUTPUT_FOLDER="${arg#*=}" 104 | shift 105 | ;; 106 | --VIRUS_TOTAL_API_KEY=*) 107 | VIRUS_TOTAL_API_KEY="${arg#*=}" 108 | shift 109 | ;; 110 | --EXPORT_TYPE=*) 111 | EXPORT_TYPE="${arg#*=}" 112 | shift 113 | ;; 114 | --SLACK_WEB_HOOK=*) 115 | SLACK_WEB_HOOK="${arg#*=}" 116 | shift 117 | ;; 118 | *) 119 | print_usage 120 | ;; 121 | esac 122 | done 123 | 124 | # Check if OUTPUT_FOLDER and VIRUS_TOTAL_API_KEY are provided 125 | if [[ -z "$OUTPUT_FOLDER" || -z "$VIRUS_TOTAL_API_KEY" || -z "$EXPORT_TYPE" ]]; then 126 | print_usage 127 | fi 128 | 129 | # Check if EXPORT_TYPE is valid 130 | if [[ "$EXPORT_TYPE" != "image" && "$EXPORT_TYPE" != "container" ]]; then 131 | echo "Invalid EXPORT_TYPE. Exiting." 132 | exit 1 133 | fi 134 | 135 | # Make sure OUTPUT_FOLDER ends with a slash 136 | if [[ "$OUTPUT_FOLDER" != */ ]]; then 137 | OUTPUT_FOLDER="${OUTPUT_FOLDER}/" 138 | fi 139 | 140 | # Check if Docker is installed 141 | if ! command -v docker &> /dev/null; then 142 | echo "Docker is not installed. Exiting." 143 | exit 1 144 | fi 145 | 146 | # Starting script 147 | printf "\n\nStarting the container/image backup and scans.\n\n" 148 | 149 | # Delete and recreate the OUTPUT_FOLDER 150 | printf "Create the OUTPUT_FOLDER: $OUTPUT_FOLDER\n\n" 151 | 152 | # Create the OUTPUT_FOLDER if it does not exist 153 | if [ ! -d "$OUTPUT_FOLDER" ]; then 154 | mkdir -p "$OUTPUT_FOLDER" 155 | fi 156 | 157 | # Set permissions to 555 158 | chmod 555 -R "$OUTPUT_FOLDER" 159 | 160 | # Delete all files in the OUTPUT_FOLDER that does not contain .virus 161 | printf "Delete all files in the OUTPUT_FOLDER that does not contain .virus\n\n" 162 | find "$OUTPUT_FOLDER" -type f ! -name "*.virus" -delete 163 | 164 | # Export image or container 165 | format_string="{{.Names}}" 166 | if [ "$EXPORT_TYPE" = "image" ]; then 167 | # Images do not contain the mounted volumes 168 | format_string="{{.ID}}" 169 | elif [ "$EXPORT_TYPE" = "container" ]; then 170 | format_string="{{.Names}}" 171 | fi 172 | 173 | if [ "$EXPORT_TYPE" = "image" ]; then 174 | item_ids=$(docker images --format "{{.Repository}}:{{.Tag}}" | sort -u) 175 | printf "Exporting all images:\n${item_ids}\n" 176 | else 177 | item_ids=$(docker ps -a --format "$format_string" | sort -u) 178 | printf "Exporting all containers (running and stopped):\n${item_ids} \n" 179 | fi 180 | 181 | # Use to debug a single image/container 182 | #item_ids="cincan/virustotal" 183 | 184 | for item_id in $item_ids; do 185 | # check if the item id is not empty 186 | item_id_safe=$(sanitize_item_id "$item_id") 187 | if [[ -n "$item_id" ]]; then 188 | backup_docker_item "$EXPORT_TYPE" "$item_id" "$OUTPUT_FOLDER" "$item_id_safe" 189 | continue 190 | fi 191 | done 192 | 193 | printf "\nFinished exporting all images/containers.\n\n" 194 | 195 | # Scan the exported tar files with VirusTotal 196 | for tar_file in "${OUTPUT_FOLDER}"*.tar; do 197 | printf "Scanning tar file: $tar_file\n" 198 | 199 | # check if the file is larger than xMB for the public api, if so, skip it 200 | if [[ $(stat -c%s "$tar_file") -gt $MAX_FILE_SIZE_TO_SCAN ]]; then 201 | printf -- "- Skipping and removing %s as it is larger than %d bytes.\n\n" "$tar_file" "$MAX_FILE_SIZE_TO_SCAN" 202 | rm "$tar_file" 203 | continue 204 | fi 205 | 206 | tar_file_name=$(basename "$tar_file") 207 | result_file="${tar_file}.result" 208 | analysis_file="${result_file}.analysis" 209 | 210 | # Perform VirusTotal scan 211 | printf -- "- Upload tar file to VirusTotal for scanning: %s\n" "$tar_file_name" 212 | command="docker run --rm -v '${OUTPUT_FOLDER}:/files' $VIRUS_TOTAL_DOCKER_IMAGE -k $VIRUS_TOTAL_API_KEY scan file '/files/${tar_file_name}'" 213 | result=$(eval "$command") 214 | echo "$command" >> "${OUTPUT_FOLDER}commands.log" 215 | printf -- "- Received result: %s\n\n" "${result}" 216 | 217 | # Check if the result contains one space and two words, if not, exit the script and send a Slack notification 218 | if [[ $(echo "$result" | wc -w) -ne 2 ]]; then 219 | slack_message="Result not received for $tar_file_name." 220 | printf "%s\n" "$slack_message" 221 | send_slack_notification $SLACK_WEB_HOOK "${slack_message}" 222 | continue 223 | fi 224 | echo "$result" > $result_file 225 | done 226 | 227 | echo "Sleeping for $SLEEP_TIME_AFTER_ALL_FILES_UPLOADED seconds to give VirusTotal time to scan the file." 228 | sleep $SLEEP_TIME_AFTER_ALL_FILES_UPLOADED 229 | printf "\n" 230 | 231 | # Scan the exported tar files with VirusTotal 232 | for tar_file in "${OUTPUT_FOLDER}"*.tar; do 233 | tar_file_name=$(basename "${tar_file}") 234 | result_file="${tar_file}.result" 235 | analysis_file="${result_file}.analysis" 236 | 237 | # Loop until the text 'status: "completed"' is found in the result file 238 | printf "Analyzing result file: %s\n" "$(basename "$result_file")" 239 | result_content=$(cat "$result_file" | tr -d '\r') 240 | 241 | completed=false 242 | retries=0 243 | while [ "$retries" -lt "$MAX_RETRIES" ] && [ "$completed" = false ]; do 244 | command_analysis="docker run --rm -v '${OUTPUT_FOLDER}:/files' $VIRUS_TOTAL_DOCKER_IMAGE -k $VIRUS_TOTAL_API_KEY analysis $result_content" 245 | analysis=$(eval "$command_analysis") 246 | echo "$command_analysis" >> "${OUTPUT_FOLDER}commands.log" 247 | printf -- "- Store analysis result in file: %s\n" "$(basename "$analysis_file")" 248 | printf "%s\n" "$analysis" > "$analysis_file" 249 | 250 | if grep -q "status" "$analysis_file" && grep -q "completed" "$analysis_file"; then 251 | printf -- "- VirusTotal gave a status of completed. - %s/%s\n" "$retries" "$MAX_RETRIES" 252 | completed=true 253 | else 254 | backoff_time=$((2**retries * RETRY_INTERVAL)) 255 | printf -- "- VirusTotal is still scanning. Retrying in $backoff_time seconds - %s/%s\n" "$retries" "$MAX_RETRIES" 256 | sleep $backoff_time 257 | retries=$((retries + 1)) 258 | fi 259 | done 260 | 261 | if [ "$retries" -eq "$MAX_RETRIES" ]; then 262 | echo -- "- Error: Reached the maximum number of retries. Skip.\n\n" 263 | continue 264 | fi 265 | 266 | # Double check if the analysis file was successfully created by looking for the word 'result' 267 | if ! grep -q "result" "$analysis_file"; then 268 | slack_message="Analysis file not created for $tar_file_name." 269 | printf "%s\n" "$slack_message" 270 | send_slack_notification $SLACK_WEB_HOOK "${slack_message}" 271 | continue 272 | fi 273 | 274 | # Check for malicious or suspicious results and send a Slack notification or print the result 275 | malicious_or_suspicious_detected=$(grep -E 'category: "malicious"|category: "suspicious"' "$analysis_file") 276 | 277 | if [ -n "$malicious_or_suspicious_detected" ]; then 278 | slack_message="Possible malicious or suspicious file in: ${result_content}" 279 | echo -e "$VIRUS_FOUND ${slack_message}\n" 280 | send_slack_notification $SLACK_WEB_HOOK "${slack_message}" 281 | 282 | # Rename the tar file to indicate that it contains a virus 283 | mv "$analysis_file" "${analysis_file}.virus" 284 | else 285 | printf "%s No malicious or suspicious file found in %s.\n\n" "$NO_VIRUS_FOUND" "$tar_file_name" 286 | fi 287 | done 288 | 289 | 290 | --------------------------------------------------------------------------------