diff --git a/README.md b/README.md index 1bcdc95..39bde22 100644 --- a/README.md +++ b/README.md @@ -257,6 +257,40 @@ vulcan-local -checktypes "./vulcan-checks/cmd" -t example.com -a Hostname -l deb At this moment, all the available checks are implemented in [Go](https://go.dev). For that reason it's required to have `go` installed in the system. +We allow to start the check running in the container in debug mode with the `-debug` flag. + +For that to work the check image must be based on `alpine` in the same architecture. + +```sh +vulcan-local -checktypes "./vulcan-checks/cmd" -t example.com -a Hostname -l debug -i vulcan-nuclei -debug +``` + +The check will start in debug mode exposing the port `2345` it will wait until a debug session is attached. + +In another terminal open dlv to connect to the check running inside the container. + +```sh +dlv connect 127.0.0.1:2345 +``` + +Or debug vulcan-checks in VS Code with a configuration like this and some breakpoints. + +```json +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Connect to server", + "type": "go", + "request": "attach", + "mode": "remote", + "port": 2345, + "host": "127.0.0.1" + } + ] +} +``` + ## Docker usage Using the existing docker image: diff --git a/go.mod b/go.mod index 1e67c7c..1c88089 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/adevinta/vulcan-report v1.0.0 github.com/adevinta/vulcan-types v1.2.16 github.com/docker/docker v26.1.2+incompatible + github.com/docker/go-connections v0.5.0 github.com/drone/envsubst v1.0.3 github.com/go-git/go-git/v5 v5.12.0 github.com/google/go-cmp v0.6.0 @@ -41,7 +42,6 @@ require ( github.com/docker/distribution v2.8.3+incompatible // indirect github.com/docker/docker-credential-helpers v0.6.4 // indirect github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c // indirect - github.com/docker/go-connections v0.5.0 // indirect github.com/docker/go-metrics v0.0.1 // indirect github.com/docker/go-units v0.4.0 // indirect github.com/emirpasic/gods v1.18.1 // indirect diff --git a/main.go b/main.go index 034f606..778a2e7 100644 --- a/main.go +++ b/main.go @@ -90,6 +90,7 @@ func main() { var showHelp, showVersion bool flag.BoolVar(&showHelp, "h", false, "print usage") flag.BoolVar(&showVersion, "version", false, "print version") + flag.BoolVar(&cfg.Conf.Debug, "debug", false, "activate debug mode for the check. Checktypes must point to a source code directory.") flag.Func("c", genFlagMsg("config file", "vulcan.yaml", "", envDefaultVulcanLocalUri, nil), func(s string) error { cmdConfigs = append(cmdConfigs, s) return nil @@ -238,6 +239,10 @@ func main() { } } + if cfg.Conf.Debug { + log.Info("Setting concurrency to 1 to allow debug.") + cfg.Conf.Concurrency = 1 + } exitCode, err = cmd.Run(cfg, log) if err != nil { log.Error(err) diff --git a/pkg/checktypes/code.go b/pkg/checktypes/code.go index 454d8a2..fbff7ab 100644 --- a/pkg/checktypes/code.go +++ b/pkg/checktypes/code.go @@ -43,14 +43,14 @@ func ParseCode(u string) (Code, bool) { // Build builds the checktype defined in a directory. It builds the binary and // the docker image of the checktype and returns the name of the docker image // built. -func (c Code) Build(logger log.Logger) (string, error) { - modified, err := c.isModified(logger) +func (c Code) Build(debug bool, logger log.Logger) (string, error) { + modified, err := c.isModified(debug, logger) if err != nil { return "", err } if !modified { - logger.Infof("No changes in checktype in dir %s, reusing image %s", string(c), c.imageName()) - return c.imageName(), nil + logger.Infof("No changes in checktype in dir %s, reusing image %s", string(c), c.imageName(debug)) + return c.imageName(debug), nil } logger.Infof("Compiling checktype in dir %s", c) dir := string(c) @@ -58,6 +58,37 @@ func (c Code) Build(logger log.Logger) (string, error) { if err := goBuildDir(dir); err != nil { return "", err } + dockerfile := "Dockerfile" + + // In case of debug, we need to add the delve debugger to the image. + // We create a new Dockerfile with the necessary changes. + // The original Dockerfile is kept in the dir but excluded from the Tar file. + // The new Dockerfile is removed after the image is built. + if debug { + b, err := os.ReadFile(path.Join(dir, "Dockerfile")) + if err != nil { + return "", err + } + dockerfile = "Dockerfile.debug" + add := fmt.Sprintf(` + RUN apk add --no-cache go + RUN go install github.com/go-delve/delve/cmd/dlv@latest + EXPOSE 2345 + ENTRYPOINT /root/go/bin/dlv --listen=:2345 --headless=true --api-version=2 --log exec %s + `, filepath.Base(dir)) + + b = append(b, []byte(add)...) + if err := os.WriteFile(path.Join(dir, dockerfile), b, 0644); err != nil { + return "", err + } + + defer func() { + if err := os.Remove(path.Join(dir, dockerfile)); err != nil { + logger.Errorf("error removing %s: %v", dockerfile, err) + } + }() + } + // Build a Tar file with the docker image contents. logger.Infof("Building image for checktype in dir %s", dir) contents, err := buildTarFromDir(dir) @@ -72,8 +103,8 @@ func (c Code) Build(logger log.Logger) (string, error) { t := modif.Format(time.RFC822) logger.Debugf("Last modified time for checktype in dir %s is %s", dir, t) labels := map[string]string{modifTimeLabel: t} - image := c.imageName() - r, err := buildDockerdImage(contents, []string{image}, labels) + image := c.imageName(debug) + r, err := buildDockerdImage(contents, []string{image}, labels, dockerfile) if err != nil { return "", err } @@ -82,19 +113,19 @@ func (c Code) Build(logger log.Logger) (string, error) { return image, nil } -func (c Code) isModified(logger log.Logger) (bool, error) { - labels, err := imageInfo(c.imageName()) +func (c Code) isModified(debug bool, logger log.Logger) (bool, error) { + labels, err := imageInfo(c.imageName(debug)) if err != nil { return false, err } imageTimeS, ok := labels[modifTimeLabel] if !ok { - logger.Infof("Image %s does not contain the label %s", c.imageName(), modifTimeLabel) + logger.Infof("Image %s does not contain the label %s", c.imageName(debug), modifTimeLabel) return true, nil } _, err = time.Parse(time.RFC822, imageTimeS) if err != nil { - logger.Infof("invalid time, %+w defined in the label %s of the image %s", err, modifTimeLabel, c.imageName()) + logger.Infof("invalid time, %+w defined in the label %s of the image %s", err, modifTimeLabel, c.imageName(debug)) return true, nil } dirTime, err := c.lastModified(logger) @@ -142,14 +173,16 @@ func (c Code) lastModified(logger log.Logger) (time.Time, error) { return *latest, nil } -func (c Code) imageName() string { - dir := string(c) - image := path.Base(dir) - return fmt.Sprintf("%s-%s", image, "local") +func (c Code) imageName(debug bool) string { + base := path.Base(string(c)) + if debug { + return fmt.Sprintf("%s-local-debug", base) + } + return fmt.Sprintf("%s-local", base) } func goBuildDir(dir string) error { - args := []string{"build", "-a", "-ldflags", "-extldflags -static", "."} + args := []string{"build", "-gcflags", "all=-N -l", "."} cmd := exec.Command("go", args...) cmd.Env = os.Environ() cmd.Env = append(cmd.Env, "GOOS=linux", "CGO_ENABLED=0") diff --git a/pkg/checktypes/docker.go b/pkg/checktypes/docker.go index 7ec0144..80651af 100644 --- a/pkg/checktypes/docker.go +++ b/pkg/checktypes/docker.go @@ -14,11 +14,12 @@ import ( "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/api/types/image" "github.com/docker/docker/client" ) // buildDockerImage builds and image given a tar, a list of tags and labels. -func buildDockerdImage(tarFile io.Reader, tags []string, labels map[string]string) (response string, err error) { +func buildDockerdImage(tarFile io.Reader, tags []string, labels map[string]string, dockerfile string) (response string, err error) { cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) if err != nil { return "", err @@ -26,9 +27,10 @@ func buildDockerdImage(tarFile io.Reader, tags []string, labels map[string]strin ctx := context.Background() buildOptions := types.ImageBuildOptions{ - Tags: tags, - Labels: labels, - Remove: true, + Dockerfile: dockerfile, + Tags: tags, + Labels: labels, + Remove: true, } re, err := cli.ImageBuild(ctx, tarFile, buildOptions) @@ -40,7 +42,7 @@ func buildDockerdImage(tarFile io.Reader, tags []string, labels map[string]strin return strings.Join(lines, "\n"), err } -func imageInfo(image string) (map[string]string, error) { +func imageInfo(imageName string) (map[string]string, error) { cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) if err != nil { return nil, err @@ -49,9 +51,9 @@ func imageInfo(image string) (map[string]string, error) { ctx := context.Background() filter := filters.KeyValuePair{ Key: "reference", - Value: image, + Value: imageName, } - options := types.ImageListOptions{ + options := image.ListOptions{ Filters: filters.NewArgs(filter), } infos, error := cli.ImageList(ctx, options) diff --git a/pkg/cmd/main.go b/pkg/cmd/main.go index 40fb8cf..3cac56f 100644 --- a/pkg/cmd/main.go +++ b/pkg/cmd/main.go @@ -24,6 +24,7 @@ import ( "github.com/adevinta/vulcan-agent/queue" "github.com/adevinta/vulcan-agent/queue/chanqueue" "github.com/docker/docker/client" + "github.com/docker/go-connections/nat" "github.com/sirupsen/logrus" "github.com/adevinta/vulcan-local/pkg/checktypes" @@ -182,7 +183,7 @@ func Run(cfg *config.Config, log *logrus.Logger) (int, error) { }, } beforeRun := func(params backend.RunParams, rc *docker.RunConfig) error { - return beforeCheckRun(params, rc, agentIP, gs, hostIP, cfg.Checks, log) + return beforeCheckRun(params, rc, agentIP, gs, hostIP, cfg.Checks, cfg.Conf.Debug, log) } backend, err := docker.NewBackend(log, agentConfig, beforeRun) if err != nil { @@ -334,7 +335,7 @@ func getHostIP(l agentlog.Logger) string { // properly when they are executed locally. func beforeCheckRun(params backend.RunParams, rc *docker.RunConfig, agentIP string, gs gitservice.GitService, hostIP string, - checks []config.Check, log *logrus.Logger) error { + checks []config.Check, debug bool, log *logrus.Logger) error { newTarget := params.Target // If the asset type is a DockerImage mount the docker socket in case the image is already there, // and the check can access it. @@ -387,6 +388,18 @@ func beforeCheckRun(params backend.RunParams, rc *docker.RunConfig, // depending on the target/assettype. rc.ContainerConfig.Env = upsertEnv(rc.ContainerConfig.Env, "VULCAN_ALLOW_PRIVATE_IPS", strconv.FormatBool(true)) + if debug { + log.Infof("Exposing port 2345 for debugging") + + rc.ContainerConfig.ExposedPorts = map[nat.Port]struct{}{ + nat.Port("2345/tcp"): {}, + } + + rc.HostConfig.PortBindings = nat.PortMap{ + nat.Port("2345/tcp"): []nat.PortBinding{{HostIP: "127.0.0.1", HostPort: "2345"}}, + } + } + return nil } diff --git a/pkg/config/config.go b/pkg/config/config.go index 062620d..c059731 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -64,6 +64,7 @@ type Registry struct { } type Conf struct { + Debug bool DockerBin string `yaml:"dockerBin"` GitBin string `yaml:"gitBin"` PullPolicy agentconfig.PullPolicy `yaml:"pullPolicy"` diff --git a/pkg/generator/generator.go b/pkg/generator/generator.go index 879682c..0afbb82 100644 --- a/pkg/generator/generator.go +++ b/pkg/generator/generator.go @@ -70,7 +70,7 @@ func GenerateJobs(cfg *config.Config, agentIp, hostIp string, gs gitservice.GitS continue } if code, ok := checktypes.ParseCode(ch.Image); ok { - image, err := code.Build(l) + image, err := code.Build(cfg.Conf.Debug, l) if err != nil { return nil, err }