diff --git a/ipabusedb-docker-ssh/.env.example b/ipabusedb-docker-ssh/.env.example new file mode 100644 index 0000000..9feec17 --- /dev/null +++ b/ipabusedb-docker-ssh/.env.example @@ -0,0 +1,5 @@ +# Copy to .env (gitignored). Variables already set in your shell win. +IPABUSEDB_API_KEY= +DOCKER_CONTAINER_NAME=sshd +# Point the docker CLI at a remote daemon; credentials above stay local. +DOCKER_HOST=ssh://user@sshd-host.example.com diff --git a/ipabusedb-docker-ssh/.gitignore b/ipabusedb-docker-ssh/.gitignore index 33d94c0..e159425 100644 --- a/ipabusedb-docker-ssh/.gitignore +++ b/ipabusedb-docker-ssh/.gitignore @@ -1,2 +1,3 @@ /ipabusedb-docker-ssh /reports.csv +/.env diff --git a/ipabusedb-docker-ssh/README.md b/ipabusedb-docker-ssh/README.md index ecd6b11..c7e47b9 100644 --- a/ipabusedb-docker-ssh/README.md +++ b/ipabusedb-docker-ssh/README.md @@ -21,10 +21,21 @@ Categories are `18,22` (Brute-Force, SSH). ## Configuration -| Variable | Purpose | -| ----------------------- | ------------------------------------------------ | -| `DOCKER_CONTAINER_NAME` | container whose logs are read (required) | -| `IPABUSEDB_API_KEY` | AbuseIPDB API key (required only with `--submit`) | +A `.env` file in the working directory is loaded at startup (see +`.env.example`; `.env` is gitignored). Variables already set in the +shell take precedence over the file. + +| Variable | Purpose | +| ----------------------- | -------------------------------------------------------- | +| `DOCKER_CONTAINER_NAME` | container whose logs are read (required) | +| `IPABUSEDB_API_KEY` | AbuseIPDB API key (required only with `--submit`) | +| `DOCKER_HOST` | passed through to the `docker` CLI to target a remote daemon | + +Because the program shells out to the `docker` CLI with its own +environment, `DOCKER_HOST` (and `DOCKER_CONTEXT`, `DOCKER_TLS_VERIFY`, +`DOCKER_CERT_PATH`) work exactly as they do for `docker` itself. Set +`DOCKER_HOST=ssh://user@host` in `.env` to read logs from a remote +daemon while the AbuseIPDB key never leaves the local machine. Flags: @@ -34,6 +45,7 @@ Flags: | `-o FILE` | write the CSV to `FILE` instead of stdout | | `--input FILE` | parse `FILE` (or `-` for stdin) instead of `docker logs` | | `--submit` | POST the CSV to the AbuseIPDB bulk-report endpoint | +| `--env FILE` | env file to autoload (default `.env`; missing is ignored) | ## Usage diff --git a/ipabusedb-docker-ssh/go.mod b/ipabusedb-docker-ssh/go.mod index db6f23b..114638a 100644 --- a/ipabusedb-docker-ssh/go.mod +++ b/ipabusedb-docker-ssh/go.mod @@ -1,3 +1,5 @@ module sneak.berlin/go/hacks/ipabusedb-docker-ssh go 1.26.5 + +require github.com/joho/godotenv v1.5.1 diff --git a/ipabusedb-docker-ssh/go.sum b/ipabusedb-docker-ssh/go.sum new file mode 100644 index 0000000..d61b19e --- /dev/null +++ b/ipabusedb-docker-ssh/go.sum @@ -0,0 +1,2 @@ +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= diff --git a/ipabusedb-docker-ssh/main.go b/ipabusedb-docker-ssh/main.go index ecef150..923b2f9 100644 --- a/ipabusedb-docker-ssh/main.go +++ b/ipabusedb-docker-ssh/main.go @@ -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 == "" {