Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
63 changes: 48 additions & 15 deletions pkg/checktypes/code.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,21 +43,52 @@ 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)
// Run go build in the checktype dir.
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)
Expand All @@ -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
}
Expand All @@ -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)
Expand Down Expand Up @@ -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")
Expand Down
16 changes: 9 additions & 7 deletions pkg/checktypes/docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,23 @@ 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
}

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)
Expand All @@ -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
Expand All @@ -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)
Expand Down
17 changes: 15 additions & 2 deletions pkg/cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
}

Expand Down
1 change: 1 addition & 0 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down
2 changes: 1 addition & 1 deletion pkg/generator/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down