Files
its-a-feature-Mythic/Mythic_CLI/src/cmd/manager/dockerComposeManager.go
T
its-a-feature af58bc9dbc v3.2.17
bug fixes and new features for mythic-cli, mythic_server, and the UI
2024-02-06 15:04:04 -06:00

1452 lines
49 KiB
Go

package manager
import (
"bufio"
"context"
"encoding/binary"
"errors"
"fmt"
"github.com/MythicMeta/Mythic_CLI/cmd/config"
"github.com/MythicMeta/Mythic_CLI/cmd/utils"
"github.com/creack/pty"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/volume"
"github.com/docker/docker/client"
"github.com/spf13/viper"
"golang.org/x/mod/semver"
"gopkg.in/yaml.v3"
"io"
"log"
"net"
"os"
"os/exec"
"path/filepath"
"sort"
"strconv"
"strings"
"sync"
"text/tabwriter"
"time"
)
type DockerComposeManager struct {
InstalledServicesPath string
InstalledServicesFolder string
}
// Interface Necessary commands
func (d *DockerComposeManager) GetManagerName() string {
return "docker"
}
// GenerateRequiredConfig ensure that the docker-compose.yml file exists
func (d *DockerComposeManager) GenerateRequiredConfig() {
groupNameConfig := viper.New()
groupNameConfig.SetConfigName("docker-compose")
groupNameConfig.SetConfigType("yaml")
groupNameConfig.AddConfigPath(utils.GetCwdFromExe())
if err := groupNameConfig.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
log.Printf("[-] Error while reading in docker-compose file: %s\n", err)
if _, err := os.Create("docker-compose.yml"); err != nil {
log.Fatalf("[-] Failed to create docker-compose.yml file: %v\n", err)
} else {
if err := groupNameConfig.ReadInConfig(); err != nil {
log.Printf("[-] Failed to read in new docker-compose.yml file: %v\n", err)
} else {
log.Printf("[+] Successfully created new docker-compose.yml file.\n")
}
return
}
} else {
log.Fatalf("[-] Error while parsing docker-compose file: %s", err)
}
}
}
// IsServiceRunning use Docker API to check running container list for the specified name
func (d *DockerComposeManager) IsServiceRunning(service string) bool {
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
log.Fatalf("[-] Failed to get client connection to Docker: %v", err)
}
containers, err := cli.ContainerList(context.Background(), types.ContainerListOptions{
All: true,
})
if err != nil {
log.Fatalf("[-] Failed to get container list from Docker: %v", err)
}
if len(containers) > 0 {
for _, container := range containers {
if container.Labels["name"] == strings.ToLower(service) {
return true
}
}
}
return false
}
// DoesImageExist use Docker API to check existing images for the specified name
func (d *DockerComposeManager) DoesImageExist(service string) bool {
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
log.Fatalf("Failed to get client in GetLogs: %v", err)
}
desiredImage := fmt.Sprintf("%v:latest", strings.ToLower(service))
images, err := cli.ImageList(context.Background(), types.ImageListOptions{All: true})
if err != nil {
log.Fatalf("Failed to get container list: %v", err)
}
for _, image := range images {
for _, name := range image.RepoTags {
if name == desiredImage {
return true
}
}
}
return false
}
// RemoveImages deletes unused images that aren't tied to any running Docker containers
func (d *DockerComposeManager) RemoveImages() error {
ctx := context.Background()
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
return err
}
defer cli.Close()
images, err := cli.ImageList(ctx, types.ImageListOptions{})
if err != nil {
log.Fatalf("[-] Failed to get list of images: %v\n", err)
}
for _, image := range images {
if utils.StringInSlice("<none>:<none>", image.RepoTags) {
_, err = cli.ImageRemove(ctx, image.ID, types.ImageRemoveOptions{
Force: true,
PruneChildren: true,
})
if err != nil {
log.Printf("[-] Failed to remove unused image: %v\n", err)
}
}
}
return nil
}
func (d *DockerComposeManager) RemoveContainers(services []string) error {
err := d.runDockerCompose(append([]string{"rm", "-s", "-v", "-f"}, services...))
if err != nil {
return err
}
_, err = d.runDocker(append([]string{"rm", "-f"}, services...))
if err != nil {
return err
} else {
return nil
}
}
func (d *DockerComposeManager) SaveImages(services []string, outputPath string) error {
savedImagePath := filepath.Join(utils.GetCwdFromExe(), outputPath)
if !utils.DirExists(savedImagePath) {
err := os.MkdirAll(savedImagePath, 0755)
if err != nil {
log.Fatalf("[-] Failed to create output folder: %v\n", err)
}
}
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
return errors.New(fmt.Sprintf("[-] Failed to connect to Docker: %v\n", err))
}
savedContainers := services
if len(savedContainers) == 0 {
diskAgents, err := d.GetInstalled3rdPartyServicesOnDisk()
if err != nil {
return errors.New(fmt.Sprintf("[-] Failed to get agents on disk: %v\n", err))
}
currentMythicServices, err := d.GetCurrentMythicServiceNames()
if err != nil {
return errors.New(fmt.Sprintf("[-] Failed to get mythic service list: %v\n", err))
}
savedContainers = append([]string{}, diskAgents...)
savedContainers = append(savedContainers, currentMythicServices...)
}
savedImagePath = filepath.Join(utils.GetCwdFromExe(), "saved_images", "mythic_save.tar")
finalSavedContainers := []string{}
for i, _ := range savedContainers {
if d.DoesImageExist(savedContainers[i]) {
containerName := fmt.Sprintf("%s:latest", savedContainers[i])
finalSavedContainers = append(finalSavedContainers, containerName)
} else {
log.Printf("[-] No image locally for %s\n", savedContainers[i])
}
}
log.Printf("[*] Saving the following images:\n%v\n", finalSavedContainers)
log.Printf("[*] This will take a while for Docker to compress and generate the layers...\n")
ioReadCloser, err := cli.ImageSave(context.Background(), finalSavedContainers)
if err != nil {
return errors.New(fmt.Sprintf("[-] Failed to get contents of docker image: %v\n", err))
}
outFile, err := os.Create(savedImagePath)
if err != nil {
return errors.New(fmt.Sprintf("[-] Failed to create output file: %v\n", err))
}
defer outFile.Close()
log.Printf("[*] Saving to %s\nThis will take a while...\n", savedImagePath)
_, err = io.Copy(outFile, ioReadCloser)
if err != nil {
return errors.New(fmt.Sprintf("[-] Failed to write contents to file: %v\n", err))
}
return nil
}
func (d *DockerComposeManager) LoadImages(outputPath string) error {
savedImagePath := filepath.Join(utils.GetCwdFromExe(), outputPath, "mythic_save.tar")
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
return errors.New(fmt.Sprintf("[-] Failed to connect to Docker: %v\n", err))
}
ioReadCloser, err := os.OpenFile(savedImagePath, os.O_RDONLY, 0x600)
if err != nil {
return errors.New(fmt.Sprintf("[-] Failed to read tar file: %v\n", err))
}
_, err = cli.ImageLoad(context.Background(), ioReadCloser, false)
if err != nil {
return errors.New(fmt.Sprintf("[-] Failed to load image into Docker: %v\n", err))
}
log.Printf("[+] loaded docker images!\n")
return nil
}
// CheckRequiredManagerVersion checks docker and docker-compose versions to make sure they're high enough
func (d *DockerComposeManager) CheckRequiredManagerVersion() bool {
outputString, err := d.runDocker([]string{"version", "--format", "{{.Server.Version}}"})
if err != nil {
log.Printf("[-] Failed to get docker version\n")
return false
}
if !semver.IsValid("v" + outputString) {
log.Printf("[-] Invalid version string: %s\n", outputString)
return false
}
if semver.Compare("v"+outputString, "v20.10.22") >= 0 {
return true
}
log.Printf("[-] Docker version is too old, %s, for Mythic. Please update\n", outputString)
return false
}
// GetVolumes returns a dictionary of defined volume information from the docker-compose file.
//
// This is not looking up existing volume information at runtime.
func (d *DockerComposeManager) GetVolumes() (map[string]interface{}, error) {
curConfig := d.readInDockerCompose()
volumes := map[string]interface{}{}
if curConfig.InConfig("volumes") {
volumes = curConfig.GetStringMap("volumes")
}
return volumes, nil
}
// SetVolumes sets a specific volume configuration into the docker-compose file.
func (d *DockerComposeManager) SetVolumes(volumes map[string]interface{}) {
curConfig := d.readInDockerCompose()
allConfigSettings := curConfig.AllSettings()
allConfigSettings["volumes"] = volumes
err := d.setDockerComposeDefaultsAndWrite(allConfigSettings)
if err != nil {
log.Printf("[-] Failed to update config: %v\n", err)
}
}
// GetServiceConfiguration checks docker-compose to see if that service is defined or not and returns its config or a generic one
func (d *DockerComposeManager) GetServiceConfiguration(service string) (map[string]interface{}, error) {
curConfig := d.readInDockerCompose()
pStruct := map[string]interface{}{}
if curConfig.InConfig("services." + strings.ToLower(service)) {
pStruct = curConfig.GetStringMap("services." + strings.ToLower(service))
delete(pStruct, "network_mode")
delete(pStruct, "extra_hosts")
delete(pStruct, "build")
delete(pStruct, "networks")
delete(pStruct, "command")
delete(pStruct, "image")
delete(pStruct, "healthcheck")
} else {
pStruct = map[string]interface{}{
"logging": map[string]interface{}{
"driver": "json-file",
"options": map[string]string{
"max-file": "1",
"max-size": "10m",
},
},
"restart": "always",
"labels": map[string]string{
"name": service,
},
"container_name": service,
"image": service,
}
}
return pStruct, nil
}
// SetServiceConfiguration sets a service configuration into docker-compose
func (d *DockerComposeManager) SetServiceConfiguration(service string, pStruct map[string]interface{}) error {
curConfig := d.readInDockerCompose()
allConfigValues := curConfig.AllSettings()
if _, ok := allConfigValues["services"]; !ok {
allConfigValues["services"] = map[string]interface{}{}
}
for key, _ := range allConfigValues {
if key == "services" {
allServices := allConfigValues["services"].(map[string]interface{})
if _, ok := allServices[service]; !ok {
log.Printf("[+] Added %s to docker-compose\n", strings.ToLower(service))
}
allServices[service] = pStruct
allConfigValues["services"] = allServices
}
}
err := d.setDockerComposeDefaultsAndWrite(allConfigValues)
if err != nil {
log.Printf("[-] Failed to update config: %v\n", err)
}
return err
}
// GetPathTo3rdPartyServicesOnDisk returns to path on disk to where 3rd party services are installed
func (d *DockerComposeManager) GetPathTo3rdPartyServicesOnDisk() string {
return d.InstalledServicesFolder
}
// StopServices stops certain containers that are running and optionally deletes the backing images
func (d *DockerComposeManager) StopServices(services []string, deleteImages bool) error {
dockerComposeContainers, err := d.GetAllInstalled3rdPartyServiceNames()
if err != nil {
return err
}
currentMythicServices, err := d.GetCurrentMythicServiceNames()
if err != nil {
return err
}
// in case somebody says "stop" but doesn't list containers, they mean everything
if len(services) == 0 {
services = append(dockerComposeContainers, currentMythicServices...)
}
/*
if utils.StringInSlice("mythic_react", services) {
if mythicEnv.GetBool("mythic_react_debug") {
// only need to remove the container if we're switching between debug and regular
if err = d.runDockerCompose(append([]string{"rm", "-s", "-v", "-f"}, "mythic_react")); err != nil {
fmt.Printf("[-] Failed to remove mythic_react\n")
return err
}
}
}
*/
if deleteImages {
return d.runDockerCompose(append([]string{"rm", "-s", "-v", "-f"}, services...))
} else {
return d.runDockerCompose(append([]string{"stop"}, services...))
}
}
// RemoveServices removes certain container entries from the docker-compose
func (d *DockerComposeManager) RemoveServices(services []string) error {
curConfig := d.readInDockerCompose()
allConfigValues := curConfig.AllSettings()
for key, _ := range allConfigValues {
if key == "services" {
allServices := allConfigValues["services"].(map[string]interface{})
for _, service := range services {
if d.IsServiceRunning(service) {
_ = d.StopServices([]string{strings.ToLower(service)}, true)
}
delete(allServices, strings.ToLower(service))
log.Printf("[+] Removed %s from docker-compose\n", strings.ToLower(service))
}
}
}
err := d.setDockerComposeDefaultsAndWrite(allConfigValues)
if err != nil {
log.Printf("[-] Failed to update config: %v\n", err)
return err
} else {
log.Println("[+] Successfully updated docker-compose.yml")
}
return nil
}
// StartServices kicks off docker/docker-compose for the specified services
func (d *DockerComposeManager) StartServices(services []string, rebuildOnStart bool) error {
if rebuildOnStart {
err := d.runDockerCompose(append([]string{"up", "--build", "-d"}, services...))
if err != nil {
return err
}
} else {
var needToBuild []string
var alreadyBuilt []string
for _, val := range services {
if !d.DoesImageExist(val) {
needToBuild = append(needToBuild, val)
} else {
alreadyBuilt = append(alreadyBuilt, val)
}
}
if len(needToBuild) > 0 {
if err := d.runDockerCompose(append([]string{"up", "--build", "-d"}, needToBuild...)); err != nil {
return err
}
}
if len(alreadyBuilt) > 0 {
if err := d.runDockerCompose(append([]string{"up", "-d"}, alreadyBuilt...)); err != nil {
return err
}
}
}
return nil
}
// BuildServices rebuilds services images and creates containers based on those images
func (d *DockerComposeManager) BuildServices(services []string) error {
if len(services) == 0 {
return nil
}
err := d.runDockerCompose(append([]string{"rm", "-s", "-v", "-f"}, services...))
if err != nil {
return err
}
err = d.runDockerCompose(append([]string{"up", "--build", "-d"}, services...))
if err != nil {
return err
}
return nil
}
// GetInstalled3rdPartyServicesOnDisk lists out the name of all 3rd party software installed on disk
func (d *DockerComposeManager) GetInstalled3rdPartyServicesOnDisk() ([]string, error) {
var agentsOnDisk []string
if !utils.DirExists(d.InstalledServicesFolder) {
if err := os.Mkdir(d.InstalledServicesFolder, 0775); err != nil {
return nil, err
}
}
if files, err := os.ReadDir(d.InstalledServicesFolder); err != nil {
log.Printf("[-] Failed to list contents of %s folder\n", d.InstalledServicesFolder)
return nil, err
} else {
for _, f := range files {
if f.IsDir() {
agentsOnDisk = append(agentsOnDisk, f.Name())
}
}
}
return agentsOnDisk, nil
}
func (d *DockerComposeManager) GetHealthCheck(services []string) {
for _, container := range services {
outputString, err := d.runDocker([]string{"inspect", "--format", "{{json .State.Health }}", container})
if err != nil {
log.Printf("failed to check status: %s", err.Error())
} else {
log.Printf("%s:\n%s\n\n", container, outputString)
}
}
}
func (d *DockerComposeManager) BuildUI() error {
_, err := d.runDocker([]string{"exec", "mythic_react", "/bin/sh", "-c", "npm run react-build"})
if err != nil {
log.Printf("[-] Failed to build new UI from MythicReactUI: %v\n", err)
}
return err
}
func (d *DockerComposeManager) GetLogs(service string, logCount int, follow bool) {
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
log.Fatalf("Failed to get client in GetLogs: %v", err)
}
containers, err := cli.ContainerList(context.Background(), types.ContainerListOptions{})
if err != nil {
log.Fatalf("Failed to get container list: %v", err)
}
if len(containers) > 0 {
found := false
for _, container := range containers {
if container.Labels["name"] == service {
found = true
reader, err := cli.ContainerLogs(context.Background(), container.ID, types.ContainerLogsOptions{
ShowStdout: true,
ShowStderr: true,
Follow: follow,
Tail: fmt.Sprintf("%d", logCount),
})
if err != nil {
log.Fatalf("Failed to get container GetLogs: %v", err)
}
// awesome post about the leading 8 payload/header bytes: https://medium.com/@dhanushgopinath/reading-docker-container-logs-with-golang-docker-engine-api-702233fac044
p := make([]byte, 8)
_, err = reader.Read(p)
for err == nil {
content := make([]byte, binary.BigEndian.Uint32(p[4:]))
reader.Read(content)
fmt.Printf("%s", content)
_, err = reader.Read(p)
}
reader.Close()
}
}
if !found {
log.Println("[-] Failed to find that container")
}
} else {
log.Println("[-] No containers running")
}
}
func (d *DockerComposeManager) TestPorts(services []string) {
// go through the different services in mythicEnv and check to make sure their ports aren't already used by trying to open them
//MYTHIC_SERVER_HOST:MYTHIC_SERVER_PORT
//POSTGRES_HOST:POSTGRES_PORT
//HASURA_HOST:HASURA_PORT
//RABBITMQ_HOST:RABBITMQ_PORT
//DOCUMENTATION_HOST:DOCUMENTATION_PORT
//NGINX_HOST:NGINX_PORT
portChecks := map[string][]string{
"MYTHIC_SERVER_HOST": {
"MYTHIC_SERVER_PORT",
"mythic_server",
},
"POSTGRES_HOST": {
"POSTGRES_PORT",
"mythic_postgres",
},
"HASURA_HOST": {
"HASURA_PORT",
"mythic_graphql",
},
"RABBITMQ_HOST": {
"RABBITMQ_PORT",
"mythic_rabbitmq",
},
"DOCUMENTATION_HOST": {
"DOCUMENTATION_PORT",
"mythic_documentation",
},
"NGINX_HOST": {
"NGINX_PORT",
"mythic_nginx",
},
"MYTHIC_REACT_HOST": {
"MYTHIC_REACT_PORT",
"mythic_react",
},
"JUPYTER_HOST": {
"JUPYTER_PORT",
"mythic_jupyter",
},
}
var addServices []string
var removeServices []string
mythicEnv := config.GetMythicEnv()
for key, val := range portChecks {
// only check ports for services we're about to start
if utils.StringInSlice(val[1], services) {
if mythicEnv.GetString(key) == val[1] || mythicEnv.GetString(key) == "127.0.0.1" {
addServices = append(addServices, val[1])
p, err := net.Listen("tcp", ":"+strconv.Itoa(mythicEnv.GetInt(val[0])))
if err != nil {
log.Fatalf("[-] Port %d, from variable %s, appears to already be in use: %v\n", mythicEnv.GetInt(val[0]), key, err)
}
err = p.Close()
if err != nil {
log.Printf("[-] Failed to close connection: %v\n", err)
}
} else {
removeServices = append(removeServices, val[1])
}
}
}
}
func (d *DockerComposeManager) PrintConnectionInfo() {
w := new(tabwriter.Writer)
mythicEnv := config.GetMythicEnv()
w.Init(os.Stdout, 0, 8, 2, '\t', 0)
fmt.Fprintln(w, "MYTHIC SERVICE\tWEB ADDRESS\tBOUND LOCALLY")
if mythicEnv.GetString("NGINX_HOST") == "mythic_nginx" {
if mythicEnv.GetBool("NGINX_USE_SSL") {
fmt.Fprintln(w, "Nginx (Mythic Web UI)\thttps://127.0.0.1:"+strconv.Itoa(mythicEnv.GetInt("NGINX_PORT"))+"\t", mythicEnv.GetBool("nginx_bind_localhost_only"))
} else {
fmt.Fprintln(w, "Nginx (Mythic Web UI)\thttp://127.0.0.1:"+strconv.Itoa(mythicEnv.GetInt("NGINX_PORT"))+"\t", mythicEnv.GetBool("nginx_bind_localhost_only"))
}
} else {
if mythicEnv.GetBool("NGINX_USE_SSL") {
fmt.Fprintln(w, "Nginx (Mythic Web UI)\thttps://"+mythicEnv.GetString("NGINX_HOST")+":"+strconv.Itoa(mythicEnv.GetInt("NGINX_PORT"))+"\t", mythicEnv.GetBool("nginx_bind_localhost_only"))
} else {
fmt.Fprintln(w, "Nginx (Mythic Web UI)\thttp://"+mythicEnv.GetString("NGINX_HOST")+":"+strconv.Itoa(mythicEnv.GetInt("NGINX_PORT"))+"\t", mythicEnv.GetBool("nginx_bind_localhost_only"))
}
}
if mythicEnv.GetString("MYTHIC_SERVER_HOST") == "mythic_server" {
fmt.Fprintln(w, "Mythic Backend Server\thttp://127.0.0.1:"+strconv.Itoa(mythicEnv.GetInt("MYTHIC_SERVER_PORT"))+"\t", mythicEnv.GetBool("mythic_server_bind_localhost_only"))
} else {
fmt.Fprintln(w, "Mythic Backend Server\thttp://"+mythicEnv.GetString("MYTHIC_SERVER_HOST")+":"+strconv.Itoa(mythicEnv.GetInt("MYTHIC_SERVER_PORT"))+"\t", mythicEnv.GetBool("mythic_server_bind_localhost_only"))
}
if mythicEnv.GetString("HASURA_HOST") == "mythic_graphql" {
fmt.Fprintln(w, "Hasura GraphQL Console\thttp://127.0.0.1:"+strconv.Itoa(mythicEnv.GetInt("HASURA_PORT"))+"\t", mythicEnv.GetBool("hasura_bind_localhost_only"))
} else {
fmt.Fprintln(w, "Hasura GraphQL Console\thttp://"+mythicEnv.GetString("HASURA_HOST")+":"+strconv.Itoa(mythicEnv.GetInt("HASURA_PORT"))+"\t", mythicEnv.GetBool("hasura_bind_localhost_only"))
}
if mythicEnv.GetString("JUPYTER_HOST") == "mythic_jupyter" {
fmt.Fprintln(w, "Jupyter Console\thttp://127.0.0.1:"+strconv.Itoa(mythicEnv.GetInt("JUPYTER_PORT"))+"\t", mythicEnv.GetBool("jupyter_bind_localhost_only"))
} else {
fmt.Fprintln(w, "Jupyter Console\thttp://"+mythicEnv.GetString("JUPYTER_HOST")+":"+strconv.Itoa(mythicEnv.GetInt("JUPYTER_PORT"))+"\t", mythicEnv.GetBool("jupyter_bind_localhost_only"))
}
if mythicEnv.GetString("DOCUMENTATION_HOST") == "mythic_documentation" {
fmt.Fprintln(w, "Internal Documentation\thttp://127.0.0.1:"+strconv.Itoa(mythicEnv.GetInt("DOCUMENTATION_PORT"))+"\t", mythicEnv.GetBool("documentation_bind_localhost_only"))
} else {
fmt.Fprintln(w, "Internal Documentation\thttp://"+mythicEnv.GetString("DOCUMENTATION_HOST")+":"+strconv.Itoa(mythicEnv.GetInt("DOCUMENTATION_PORT"))+"\t", mythicEnv.GetBool("documentation_bind_localhost_only"))
}
fmt.Fprintln(w, "\t\t\t\t")
fmt.Fprintln(w, "ADDITIONAL SERVICES\tADDRESS\tBOUND LOCALLY")
if mythicEnv.GetString("POSTGRES_HOST") == "mythic_postgres" {
fmt.Fprintln(w, "Postgres Database\tpostgresql://mythic_user:password@127.0.0.1:"+strconv.Itoa(mythicEnv.GetInt("POSTGRES_PORT"))+"/mythic_db\t", mythicEnv.GetBool("postgres_bind_localhost_only"))
} else {
fmt.Fprintln(w, "Postgres Database\tpostgresql://mythic_user:password@"+mythicEnv.GetString("POSTGRES_HOST")+":"+strconv.Itoa(mythicEnv.GetInt("POSTGRES_PORT"))+"/mythic_db\t", mythicEnv.GetBool("postgres_bind_localhost_only"))
}
if mythicEnv.GetString("MYTHIC_REACT_HOST") == "mythic_react" {
fmt.Fprintln(w, "React Server\thttp://127.0.0.1:"+strconv.Itoa(mythicEnv.GetInt("MYTHIC_REACT_PORT"))+"/new\t", mythicEnv.GetBool("mythic_react_bind_localhost_only"))
} else {
fmt.Fprintln(w, "React Server\thttp://"+mythicEnv.GetString("MYTHIC_REACT_HOST")+":"+strconv.Itoa(mythicEnv.GetInt("MYTHIC_REACT_PORT"))+"/new\t", mythicEnv.GetBool("mythic_react_bind_localhost_only"))
}
if mythicEnv.GetString("RABBITMQ_HOST") == "mythic_rabbitmq" {
fmt.Fprintln(w, "RabbitMQ\tamqp://"+mythicEnv.GetString("RABBITMQ_USER")+":password@127.0.0.1:"+strconv.Itoa(mythicEnv.GetInt("RABBITMQ_PORT"))+"\t", mythicEnv.GetBool("rabbitmq_bind_localhost_only"))
} else {
fmt.Fprintln(w, "RabbitMQ\tamqp://"+mythicEnv.GetString("RABBITMQ_USER")+":password@"+mythicEnv.GetString("RABBITMQ_HOST")+":"+strconv.Itoa(mythicEnv.GetInt("RABBITMQ_PORT"))+"\t", mythicEnv.GetBool("rabbitmq_bind_localhost_only"))
}
fmt.Fprintln(w, "\t\t\t\t")
w.Flush()
}
func (d *DockerComposeManager) Status(verbose bool) {
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
log.Fatalf("[-] Failed to get client in Status check: %v", err)
}
containers, err := cli.ContainerList(context.Background(), types.ContainerListOptions{
All: true,
})
if err != nil {
log.Fatalf("[-] Failed to get container list: %v\n", err)
}
w := new(tabwriter.Writer)
w.Init(os.Stdout, 0, 8, 2, '\t', 0)
var mythicLocalServices []string
var installedServices []string
sort.Slice(containers[:], func(i, j int) bool {
return containers[i].Labels["name"] < containers[j].Labels["name"]
})
elementsOnDisk, err := d.GetInstalled3rdPartyServicesOnDisk()
if err != nil {
log.Fatalf("[-] Failed to get list of installed services on disk: %v\n", err)
}
elementsInCompose, err := d.GetAllInstalled3rdPartyServiceNames()
if err != nil {
log.Fatalf("[-] Failed to get list of installed services in docker-compose: %v\n", err)
}
for _, container := range containers {
if container.Labels["name"] == "" {
continue
}
var portRanges []uint16
var portRangeMaps []string
portString := ""
info := fmt.Sprintf("%s\t%s\t%s\t", container.Labels["name"], container.State, container.Status)
if len(container.Ports) > 0 {
sort.Slice(container.Ports[:], func(i, j int) bool {
return container.Ports[i].PublicPort < container.Ports[j].PublicPort
})
for _, port := range container.Ports {
if port.PublicPort > 0 {
if port.PrivatePort == port.PublicPort && port.IP == "0.0.0.0" {
portRanges = append(portRanges, port.PrivatePort)
} else {
portRangeMaps = append(portRangeMaps, fmt.Sprintf("%d/%s -> %s:%d", port.PrivatePort, port.Type, port.IP, port.PublicPort))
}
}
}
if len(portRanges) > 0 {
sort.Slice(portRanges, func(i, j int) bool { return portRanges[i] < portRanges[j] })
}
portString = strings.Join(portRangeMaps[:], ", ")
var stringPortRanges []string
for _, val := range portRanges {
stringPortRanges = append(stringPortRanges, fmt.Sprintf("%d", val))
}
if len(stringPortRanges) > 0 && len(portString) > 0 {
portString = portString + ", "
}
portString = portString + strings.Join(stringPortRanges[:], ", ")
}
foundMountInfo := false
for _, mnt := range container.Mounts {
if strings.HasPrefix(mnt.Name, container.Labels["name"]+"_volume") {
if foundMountInfo {
info += ", " + mnt.Name
} else {
info += mnt.Name
}
foundMountInfo = true
}
}
if !foundMountInfo {
if container.Labels["name"] == "mythic_graphql" {
info += "N/A"
} else {
info += "local"
}
}
info += "\t"
if utils.StringInSlice(container.Labels["name"], config.MythicPossibleServices) {
info = info + portString
mythicLocalServices = append(mythicLocalServices, info)
} else {
if utils.StringInSlice(container.Labels["name"], elementsOnDisk) ||
utils.StringInSlice(container.Labels["name"], elementsInCompose) {
installedServices = append(installedServices, info)
elementsOnDisk = utils.RemoveStringFromSliceNoOrder(elementsOnDisk, container.Labels["name"])
elementsInCompose = utils.RemoveStringFromSliceNoOrder(elementsInCompose, container.Labels["name"])
}
}
}
fmt.Fprintln(w, "Mythic Main Services")
fmt.Fprintln(w, "CONTAINER NAME\tSTATE\tSTATUS\tMOUNT\tPORTS")
for _, line := range mythicLocalServices {
fmt.Fprintln(w, line)
}
fmt.Fprintln(w, "\t\t\t\t\t")
w.Flush()
fmt.Fprintln(w, "Installed Services")
fmt.Fprintln(w, "CONTAINER NAME\tSTATE\tSTATUS\tMOUNT")
for _, line := range installedServices {
fmt.Fprintln(w, line)
}
fmt.Fprintln(w, "\t\t\t\t")
// remove all elementsInCompose from elementsOnDisk
for _, container := range elementsInCompose {
elementsOnDisk = utils.RemoveStringFromSliceNoOrder(elementsOnDisk, container)
}
if len(elementsInCompose) > 0 && verbose {
fmt.Fprintln(w, "Docker Compose services not running, start with: ./mythic-cli start [name]")
fmt.Fprintln(w, "NAME\t")
sort.Strings(elementsInCompose)
for _, container := range elementsInCompose {
fmt.Fprintln(w, fmt.Sprintf("%s\t", container))
}
fmt.Fprintln(w, "\t")
}
if len(elementsOnDisk) > 0 && verbose {
fmt.Fprintln(w, "Extra Services, add to docker compose with: ./mythic-cli add [name]")
fmt.Fprintln(w, "NAME\t")
sort.Strings(elementsOnDisk)
for _, container := range elementsOnDisk {
fmt.Fprintln(w, fmt.Sprintf("%s\t", container))
}
fmt.Fprintln(w, "\t\t")
}
w.Flush()
}
func (d *DockerComposeManager) PrintAllServices() {
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
log.Fatalf("[-] Failed to get client in List Services: %v", err)
}
containers, err := cli.ContainerList(context.Background(), types.ContainerListOptions{
All: true,
})
if err != nil {
log.Fatalf("[-] Failed to get container list: %v\n", err)
}
w := new(tabwriter.Writer)
w.Init(os.Stdout, 0, 8, 2, '\t', 0)
var installedServices []string
sort.Slice(containers[:], func(i, j int) bool {
return containers[i].Labels["name"] < containers[j].Labels["name"]
})
elementsOnDisk, err := d.GetInstalled3rdPartyServicesOnDisk()
if err != nil {
log.Fatalf("[-] Failed to get list of installed services on disk: %v\n", err)
}
elementsInCompose, err := d.GetAllInstalled3rdPartyServiceNames()
if err != nil {
log.Fatalf("[-] Failed to get list of installed services in docker-compose: %v\n", err)
}
for _, container := range containers {
if container.Labels["name"] == "" {
continue
}
for _, mnt := range container.Mounts {
if strings.Contains(mnt.Source, d.InstalledServicesPath) {
info := fmt.Sprintf("%s\t%s\t%v\t%v", container.Labels["name"], container.Status, true, utils.StringInSlice(container.Labels["name"], elementsInCompose))
installedServices = append(installedServices, info)
elementsOnDisk = utils.RemoveStringFromSliceNoOrder(elementsOnDisk, container.Labels["name"])
elementsInCompose = utils.RemoveStringFromSliceNoOrder(elementsInCompose, container.Labels["name"])
}
}
}
for _, container := range elementsInCompose {
elementsOnDisk = utils.RemoveStringFromSliceNoOrder(elementsOnDisk, container)
}
fmt.Fprintln(w, "Name\tContainerStatus\tImageBuilt\tDockerComposeEntry")
for _, line := range installedServices {
fmt.Fprintln(w, line)
}
if len(elementsInCompose) > 0 {
sort.Strings(elementsInCompose)
for _, container := range elementsInCompose {
fmt.Fprintln(w, fmt.Sprintf("%s\t%s\t%v\t%v", container, "N/A", d.DoesImageExist(container), true))
}
}
if len(elementsOnDisk) > 0 {
sort.Strings(elementsOnDisk)
for _, container := range elementsOnDisk {
fmt.Fprintln(w, fmt.Sprintf("%s\t%s\t%v\t%v", container, "N/A", d.DoesImageExist(container), false))
}
}
w.Flush()
}
func (d *DockerComposeManager) ResetDatabase(useVolume bool) {
if !useVolume {
workingPath := utils.GetCwdFromExe()
err := os.RemoveAll(filepath.Join(workingPath, "postgres-docker", "database"))
if err != nil {
log.Fatalf("[-] Failed to remove database files\n%v\n", err)
} else {
log.Printf("[+] Successfully reset datbase files\n")
}
} else {
_ = d.RemoveContainers([]string{"mythic_postgres"})
err := d.RemoveVolume("mythic_postgres_volume")
if err != nil {
log.Printf("[-] Failed to remove database:\n%v\n", err)
}
}
}
func (d *DockerComposeManager) BackupDatabase(backupPath string, useVolume bool) error {
if !useVolume {
workingPath := utils.GetCwdFromExe()
log.Printf("[*] Staring to copy, this might take a minute...")
err := utils.CopyDir(filepath.Join(workingPath, "postgres-docker", "database"), backupPath)
if err != nil {
log.Printf("[-] Failed to copy database files\n%v\n", err)
return err
} else {
log.Printf("[+] Successfully copied database files from disk\n")
return nil
}
} else {
ctx := context.Background()
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
return err
}
defer cli.Close()
execID, err := cli.ContainerExecCreate(ctx, "mythic_postgres", types.ExecConfig{
AttachStderr: true,
AttachStdout: true,
AttachStdin: true,
Cmd: []string{"/bin/bash", "-i"},
})
if err != nil {
log.Fatalf("[!] Failed to exec into container: %v", err)
} else {
log.Printf("[*] Created docker exec session")
}
session, err := cli.ContainerExecAttach(ctx, execID.ID, types.ExecStartCheck{})
if err != nil {
log.Fatalf("[!] Failed to attach to exec session: %v", err)
} else {
log.Printf("[*] Attached to docker exec session")
}
todayString := time.Now().Format("2006-01-02-150405")
tarFileName := fmt.Sprintf("%s-mythic_postgres.tar", todayString)
defer session.Close()
dumpCommand := fmt.Sprintf("PGPASSWORD=%s pg_dump -n public --format=tar -U mythic_user -f /var/lib/postgresql/data/%s mythic_db\n",
config.GetMythicEnv().GetString("postgres_password"), tarFileName)
_, err = session.Conn.Write([]byte(dumpCommand))
if err != nil {
log.Fatalf("[!] Failed to write to exec bash: %v", err)
} else {
log.Printf("[*] Issued pg_dump command")
}
_, err = session.Conn.Write([]byte("exit\n"))
if err != nil {
log.Fatalf("[!] Failed to authenticate to exec bash: %v", err)
}
inspect, err := cli.ContainerExecInspect(ctx, execID.ID)
for inspect.Running {
time.Sleep(1 * time.Second)
log.Printf("[*] Waiting for pg_dump to finish...")
inspect, err = cli.ContainerExecInspect(ctx, execID.ID)
}
log.Printf("[*] Finished docker exec session")
err = d.CopyFromVolume("mythic_postgres_volume", tarFileName, backupPath)
if err != nil {
return err
}
log.Printf("[+] Successfully copied database files from volume")
return nil
}
}
func (d *DockerComposeManager) RestoreDatabase(backupPath string, useVolume bool) error {
if !useVolume {
workingPath := utils.GetCwdFromExe()
log.Printf("[*] Staring to copy, this might take a minute...")
err := utils.CopyDir(backupPath, filepath.Join(workingPath, "postgres-docker", "database"))
if err != nil {
log.Printf("[-] Failed to copy database files\n%v\n", err)
return err
} else {
log.Printf("[+] Successfully copied database files\n")
return nil
}
} else {
err := d.CopyIntoVolume(backupPath, "dump.tar", "mythic_postgres_volume")
if err != nil {
return err
}
log.Printf("[+] Successfully copied database files")
ctx := context.Background()
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
return err
}
defer cli.Close()
execID, err := cli.ContainerExecCreate(ctx, "mythic_postgres", types.ExecConfig{
AttachStderr: true,
AttachStdout: true,
AttachStdin: true,
Cmd: []string{"/bin/bash", "-i"},
})
if err != nil {
log.Fatalf("[!] Failed to exec into container: %v", err)
} else {
log.Printf("[*] Created docker exec session")
}
session, err := cli.ContainerExecAttach(ctx, execID.ID, types.ExecStartCheck{})
if err != nil {
log.Fatalf("[!] Failed to attach to exec session: %v", err)
} else {
log.Printf("[*] Attached to docker exec session")
}
defer session.Close()
dumpCommand := fmt.Sprintf("PGPASSWORD=%s pg_restore -U mythic_user -n public --clean --if-exists -d mythic_db /var/lib/postgresql/data/dump.tar\n",
config.GetMythicEnv().GetString("postgres_password"))
_, err = session.Conn.Write([]byte(dumpCommand))
if err != nil {
log.Fatalf("[!] Failed to write to exec bash: %v", err)
} else {
log.Printf("[*] Issued pg_dump command")
}
_, err = session.Conn.Write([]byte("rm /var/lib/postgresql/data/dump.tar; exit\n"))
if err != nil {
log.Fatalf("[!] Failed to authenticate to exec bash: %v", err)
}
inspect, err := cli.ContainerExecInspect(ctx, execID.ID)
for inspect.Running {
time.Sleep(1 * time.Second)
log.Printf("[*] Waiting for pg_dump to finish...")
inspect, err = cli.ContainerExecInspect(ctx, execID.ID)
}
log.Printf("[*] Finished docker exec session")
return nil
}
}
func (d *DockerComposeManager) BackupFiles(backupPath string, useVolume bool) error {
if !useVolume {
workingPath := utils.GetCwdFromExe()
log.Printf("[*] Staring to copy, this might take a minute...")
err := utils.CopyDir(filepath.Join(workingPath, "mythic-docker", "src", "files"), backupPath)
if err != nil {
log.Printf("[-] Failed to copy Mythic's uploads/downloads\n%v\n", err)
return err
} else {
log.Printf("[+] Successfully copied Mythic's uploads/downloads from disk\n")
return nil
}
} else {
err := d.CopyFromVolume("mythic_server_volume", "", backupPath)
if err != nil {
return err
}
log.Printf("[+] Successfully copied Mythic's uploads/downloads from volume")
return nil
}
}
func (d *DockerComposeManager) RestoreFiles(backupPath string, useVolume bool) error {
if !useVolume {
workingPath := utils.GetCwdFromExe()
log.Printf("[*] Staring to copy, this might take a minute...")
err := utils.CopyDir(backupPath, filepath.Join(workingPath, "mythic-docker", "src", "files"))
if err != nil {
log.Printf("[-] Failed to copy Mythic's uploads/downloads\n%v\n", err)
return err
} else {
log.Printf("[+] Successfully copied Mythic's uploads/downloads\n")
return nil
}
} else {
err := d.CopyIntoVolume(backupPath, "/", "mythic_server_volume")
if err != nil {
return err
}
log.Printf("[+] Successfully copied Mythic's uploads/downloads")
return nil
}
}
func (d *DockerComposeManager) PrintVolumeInformation() {
ctx := context.Background()
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
panic(err)
}
defer cli.Close()
w := new(tabwriter.Writer)
w.Init(os.Stdout, 0, 8, 2, '\t', 0)
fmt.Fprintln(w, "VOLUME\tSIZE\tCONTAINER (Ref Count)\tCONTAINER STATUS\tLOCATION")
du, err := cli.DiskUsage(ctx, types.DiskUsageOptions{})
if err != nil {
log.Fatalf("[-] Failed to get disk sizes: %v\n", err)
}
containers, err := cli.ContainerList(ctx, types.ContainerListOptions{Size: true})
if err != nil {
log.Fatalf("[-] Failed to get container list: %v\n", err)
}
if du.Volumes == nil {
log.Printf("[-] No volumes known\n")
return
}
var entries []string
volumeList, err := d.GetVolumes()
if err != nil {
log.Fatalf("[-] Failed to get volumes: %v", err)
}
for _, currentVolume := range du.Volumes {
name := currentVolume.Name
size := "unknown"
if currentVolume.UsageData != nil {
size = utils.ByteCountSI(currentVolume.UsageData.Size)
}
if _, ok := volumeList[currentVolume.Name]; !ok {
continue
}
containerPieces := strings.Split(currentVolume.Name, "_volume")
containerName := containerPieces[0]
container := "unused (0)"
containerStatus := "offline"
for _, c := range containers {
if containerName == c.Labels["name"] {
containerStatus = c.Status
}
for _, m := range c.Mounts {
if m.Name == currentVolume.Name {
container = containerName + " (" + strconv.Itoa(int(currentVolume.UsageData.RefCount)) + ")"
}
}
}
entries = append(entries, fmt.Sprintf("%s\t%s\t%s\t%s\t%s",
name,
size,
container,
containerStatus,
currentVolume.Mountpoint,
))
}
sort.Strings(entries)
for _, line := range entries {
fmt.Fprintln(w, line)
}
defer w.Flush()
return
}
func (d *DockerComposeManager) RemoveVolume(volumeName string) error {
ctx := context.Background()
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
panic(err)
}
defer cli.Close()
volumes, err := cli.VolumeList(ctx, volume.ListOptions{})
if err != nil {
return err
}
for _, currentVolume := range volumes.Volumes {
if currentVolume.Name == volumeName {
containers, err := cli.ContainerList(ctx, types.ContainerListOptions{Size: true})
if err != nil {
log.Fatalf("[-] Failed to get container list: %v\n", err)
}
for _, c := range containers {
for _, m := range c.Mounts {
if m.Name == volumeName {
containerName := c.Labels["name"]
err = cli.ContainerRemove(ctx, c.ID, types.ContainerRemoveOptions{Force: true})
if err != nil {
log.Printf(fmt.Sprintf("[!] Failed to remove container that's using the volume: %v\n", err))
} else {
log.Printf("[+] Removed container %s, which was using that volume", containerName)
}
}
}
}
err = cli.VolumeRemove(ctx, currentVolume.Name, true)
return err
}
}
log.Printf("[*] Volume not found")
return errors.New("[*] Volume not found")
}
func (d *DockerComposeManager) CopyIntoVolume(sourceFile string, destinationFileName string, destinationVolume string) error {
err := d.ensureVolume(destinationVolume)
if err != nil {
log.Fatalf("[-] Failed to ensure volume exists: %v\n", err)
}
ctx := context.Background()
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
log.Fatalf("[-] Failed to connect to docker api: %v\n", err)
}
defer cli.Close()
containers, err := cli.ContainerList(ctx, types.ContainerListOptions{Size: true})
if err != nil {
log.Fatalf("[-] Failed to get container list: %v\n", err)
}
for _, container := range containers {
for _, mnt := range container.Mounts {
if mnt.Name == destinationVolume {
log.Printf("[*] Staring to copy, this might take a minute...")
log.Printf("[*] Copying %s to %s", sourceFile, container.Labels["name"]+":"+mnt.Destination+"/"+destinationFileName)
output, err := d.runDocker([]string{"cp", sourceFile, container.Labels["name"] + ":" + mnt.Destination + "/" + destinationFileName})
log.Printf(output)
return err
}
}
}
log.Printf("[-] Failed to find %s in use by any containers", destinationVolume)
return errors.New("[-] failed to find that volume")
}
func (d *DockerComposeManager) CopyFromVolume(sourceVolumeName string, sourceFileName string, destinationName string) error {
err := d.ensureVolume(sourceVolumeName)
if err != nil {
log.Fatalf("[-] Failed to ensure volume exists: %v\n", err)
}
ctx := context.Background()
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
log.Fatalf("[-] Failed to connect to docker api: %v\n", err)
}
defer cli.Close()
containers, err := cli.ContainerList(ctx, types.ContainerListOptions{Size: true})
if err != nil {
log.Fatalf("[-] Failed to get container list: %v\n", err)
}
for _, container := range containers {
for _, mnt := range container.Mounts {
if mnt.Name == sourceVolumeName {
log.Printf("[*] Staring to copy, this might take a minute...")
output, err := d.runDocker([]string{"cp", container.Labels["name"] + ":" + mnt.Destination + "/" + sourceFileName, destinationName})
log.Printf(output)
return err
}
}
}
log.Printf("[-] Failed to find that volume name in use by any containers")
return errors.New("[-] failed to find that volume")
}
// Internal Support Commands
func (d *DockerComposeManager) getMythicEnvList() []string {
env := config.GetMythicEnv().AllSettings()
var envList []string
for key := range env {
val := config.GetMythicEnv().GetString(key)
if val != "" {
// prevent trying to append arrays or dictionaries to our environment list
envList = append(envList, strings.ToUpper(key)+"="+val)
}
}
envList = append(envList, os.Environ()...)
return envList
}
func (d *DockerComposeManager) getCwdFromExe() string {
exe, err := os.Executable()
if err != nil {
log.Fatalf("[-] Failed to get path to current executable\n")
}
return filepath.Dir(exe)
}
func (d *DockerComposeManager) runDocker(args []string) (string, error) {
lookPath, err := exec.LookPath("docker")
if err != nil {
log.Fatalf("[-] docker is not installed or available in the current PATH\n")
}
exe, err := os.Executable()
if err != nil {
log.Fatalf("[-] Failed to get lookPath to current executable\n")
}
exePath := filepath.Dir(exe)
command := exec.Command(lookPath, args...)
command.Dir = exePath
command.Env = d.getMythicEnvList()
stdout, err := command.StdoutPipe()
if err != nil {
log.Fatalf("[-] Failed to get stdout pipe for running docker-compose\n")
}
stderr, err := command.StderrPipe()
if err != nil {
log.Fatalf("[-] Failed to get stderr pipe for running docker-compose\n")
}
stdoutScanner := bufio.NewScanner(stdout)
stderrScanner := bufio.NewScanner(stderr)
outputString := ""
wg := sync.WaitGroup{}
wg.Add(2)
go func() {
for stdoutScanner.Scan() {
outputString += stdoutScanner.Text()
}
wg.Done()
}()
go func() {
for stderrScanner.Scan() {
fmt.Printf("%s\n", stderrScanner.Text())
}
wg.Done()
}()
err = command.Start()
if err != nil {
log.Fatalf("[-] Error trying to start docker: %v\n", err)
}
wg.Wait()
err = command.Wait()
if err != nil {
log.Printf("[-] Error from docker: %v\n", err)
log.Printf("[*] Docker command: %v\n", args)
return "", err
}
return outputString, nil
}
func (d *DockerComposeManager) runDockerCompose(args []string) error {
lookPath, err := exec.LookPath("docker-compose")
if err != nil {
lookPath, err = exec.LookPath("docker")
if err != nil {
log.Fatalf("[-] docker-compose and docker are not installed or available in the current PATH\n")
} else {
// adjust the current args for docker compose subcommand
args = append([]string{"compose"}, args...)
}
}
exe, err := os.Executable()
if err != nil {
log.Fatalf("[-] Failed to get lookPath to current executable\n")
}
exePath := filepath.Dir(exe)
command := exec.Command(lookPath, args...)
command.Dir = exePath
command.Env = d.getMythicEnvList()
f, err := pty.Start(command)
if err != nil {
stdout, err := command.StdoutPipe()
if err != nil {
log.Fatalf("[-] Failed to get stdout pipe for running docker-compose\n")
}
stderr, err := command.StderrPipe()
if err != nil {
log.Fatalf("[-] Failed to get stderr pipe for running docker-compose\n")
}
stdoutScanner := bufio.NewScanner(stdout)
stderrScanner := bufio.NewScanner(stderr)
go func() {
for stdoutScanner.Scan() {
fmt.Printf("%s\n", stdoutScanner.Text())
}
}()
go func() {
for stderrScanner.Scan() {
fmt.Printf("%s\n", stderrScanner.Text())
}
}()
err = command.Start()
if err != nil {
log.Fatalf("[-] Error trying to start docker-compose: %v\n", err)
}
err = command.Wait()
if err != nil {
fmt.Printf("[-] Error from docker-compose: %v\n", err)
fmt.Printf("[*] Docker compose command: %v\n", args)
return err
}
} else {
io.Copy(os.Stdout, f)
}
return nil
}
func (d *DockerComposeManager) setDockerComposeDefaultsAndWrite(curConfig map[string]interface{}) error {
file := filepath.Join(utils.GetCwdFromExe(), "docker-compose.yml")
curConfig["version"] = "2.4"
delete(curConfig, "networks")
content, err := yaml.Marshal(curConfig)
if err != nil {
return err
}
return os.WriteFile(file, content, 0644)
}
func (d *DockerComposeManager) readInDockerCompose() *viper.Viper {
var curConfig = viper.New()
curConfig.SetConfigName("docker-compose")
curConfig.SetConfigType("yaml")
curConfig.AddConfigPath(d.getCwdFromExe())
if err := curConfig.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
log.Fatalf("[-] Error while reading in docker-compose file: %s\n", err)
} else {
log.Fatalf("[-] Error while parsing docker-compose file: %s\n", err)
}
}
return curConfig
}
func (d *DockerComposeManager) ensureVolume(volumeName string) error {
containerNamePieces := strings.Split(volumeName, "_")
containerName := strings.Join(containerNamePieces[0:len(containerNamePieces)-1], "_")
ctx := context.Background()
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
return err
}
defer cli.Close()
volumes, err := cli.VolumeList(ctx, volume.ListOptions{})
if err != nil {
return err
}
foundVolume := false
for _, currentVolume := range volumes.Volumes {
if currentVolume.Name == volumeName {
foundVolume = true
}
}
if !foundVolume {
_, err = cli.VolumeCreate(ctx, volume.CreateOptions{Name: volumeName})
if err != nil {
return err
}
}
// now that we know the volume exists, make sure it's attached to a running container or we can't manipulate files
containers, err := cli.ContainerList(ctx, types.ContainerListOptions{Size: true})
if err != nil {
return err
}
for _, container := range containers {
if container.Labels["name"] == containerName {
for _, mnt := range container.Mounts {
if mnt.Name == volumeName {
// container is running and has this mount associated with it
return nil
}
}
return errors.New(fmt.Sprintf("container, %s, isn't using volume, %s", containerName, volumeName))
}
}
return errors.New(fmt.Sprintf("failed to find container, %s, for volume, %s", containerName, volumeName))
}
func (d *DockerComposeManager) GetAllInstalled3rdPartyServiceNames() ([]string, error) {
// get all services that exist within the loaded config
groupNameConfig := viper.New()
groupNameConfig.SetConfigName("docker-compose")
groupNameConfig.SetConfigType("yaml")
groupNameConfig.AddConfigPath(utils.GetCwdFromExe())
if err := groupNameConfig.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
log.Printf("[-] Error while reading in docker-compose file: %s\n", err)
return []string{}, err
} else {
log.Printf("[-] Error while parsing docker-compose file: %s\n", err)
return []string{}, err
}
}
servicesSub := groupNameConfig.Sub("services")
containerList := []string{}
if servicesSub != nil {
services := servicesSub.AllSettings()
for service := range services {
if !utils.StringInSlice(service, config.MythicPossibleServices) {
containerList = append(containerList, service)
}
}
}
return containerList, nil
}
// GetCurrentMythicServiceNames from reading in the docker-compose file, not necessarily what should be there or what's running
func (d *DockerComposeManager) GetCurrentMythicServiceNames() ([]string, error) {
groupNameConfig := viper.New()
groupNameConfig.SetConfigName("docker-compose")
groupNameConfig.SetConfigType("yaml")
groupNameConfig.AddConfigPath(utils.GetCwdFromExe())
if err := groupNameConfig.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
log.Printf("[-] Error while reading in docker-compose file: %s\n", err)
return []string{}, err
} else {
log.Printf("[-] Error while parsing docker-compose file: %s\n", err)
return []string{}, err
}
}
servicesSub := groupNameConfig.Sub("services")
containerList := []string{}
if servicesSub != nil {
services := servicesSub.AllSettings()
for service := range services {
if utils.StringInSlice(service, config.MythicPossibleServices) {
containerList = append(containerList, service)
}
}
}
return containerList, nil
}