ipabusedb-docker-ssh: autoload .env via godotenv, document DOCKER_HOST passthrough, surface docker CLI errors

This commit is contained in:
sneak committed 2026-11-06 19:59:33 -08:00
1 parent 21bda80f88
commit b8c36689ae
6 files changed
+59 -5

No files matched your search

+33 -1
View File
@@ -2,10 +2,13 @@
// "docker logs -t", extracts brute-force / invalid-user attempts, and
// emits a CSV suitable for the AbuseIPDB bulk-report endpoint.
//
// Environment:
// Environment (a .env file in the working directory is loaded first;
// variables already set in the process environment take precedence):
//
// DOCKER_CONTAINER_NAME container whose logs are read (required)
// IPABUSEDB_API_KEY AbuseIPDB API key (required with --submit)
// DOCKER_HOST passed through to the docker CLI, so e.g.
// ssh://user@host reads logs from a remote daemon
//
// Flags:
//
@@ -35,6 +38,8 @@ import (
"strings"
"sync"
"time"
"github.com/joho/godotenv"
)
const (
@@ -74,8 +79,13 @@ type parser struct {
mu sync.Mutex
events map[netip.Addr]*event
skipped int
// unparsed keeps the last few lines without a valid timestamp; the
// docker CLI writes its own errors to stderr without one.
unparsed []string
}
const keepUnparsed = 5
func newParser(cutoff time.Time) *parser {
return &parser{cutoff: cutoff, events: map[netip.Addr]*event{}}
}
@@ -92,6 +102,10 @@ func (p *parser) handleLine(line string) {
if err != nil {
p.mu.Lock()
p.skipped++
p.unparsed = append(p.unparsed, line)
if len(p.unparsed) > keepUnparsed {
p.unparsed = p.unparsed[1:]
}
p.mu.Unlock()
return
}
@@ -193,7 +207,13 @@ func readDockerLogs(ctx context.Context, p *parser, container string, since time
args = append(args, "--since", since.String())
}
args = append(args, container)
// cmd.Env is left nil so the docker CLI inherits the full process
// environment, including DOCKER_HOST / DOCKER_CONTEXT / DOCKER_TLS_*
// loaded from .env, and can therefore target a remote daemon.
cmd := exec.CommandContext(ctx, "docker", args...)
if host := os.Getenv("DOCKER_HOST"); host != "" {
slog.Info("reading logs via docker", "DOCKER_HOST", host, "container", container)
}
stdout, err := cmd.StdoutPipe()
if err != nil {
@@ -221,6 +241,12 @@ func readDockerLogs(ctx context.Context, p *parser, container string, since time
wg.Wait()
close(errs)
if err := cmd.Wait(); err != nil {
p.mu.Lock()
detail := strings.Join(p.unparsed, "\n")
p.mu.Unlock()
if detail != "" {
return fmt.Errorf("docker logs %s: %w\n%s", container, err, detail)
}
return fmt.Errorf("docker logs %s: %w", container, err)
}
for err := range errs {
@@ -286,8 +312,14 @@ func run() error {
outPath := flag.String("o", "", "write CSV to this file instead of stdout")
inPath := flag.String("input", "", "parse this file (or - for stdin) instead of running docker logs")
doSubmit := flag.Bool("submit", false, "POST the CSV to the AbuseIPDB bulk-report endpoint")
envFile := flag.String("env", ".env", "env file to autoload (missing file is ignored)")
flag.Parse()
// Load .env without overriding variables already in the environment.
if err := godotenv.Load(*envFile); err != nil && !errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("loading %s: %w", *envFile, err)
}
apiKey := os.Getenv("IPABUSEDB_API_KEY")
container := os.Getenv("DOCKER_CONTAINER_NAME")
if *doSubmit && apiKey == "" {