ipabusedb-docker-ssh: autoload .env via godotenv, document DOCKER_HOST passthrough, surface docker CLI errors
This commit is contained in:
1 parent
21bda80f88
commit
b8c36689ae
6 files changed
+56
-2
No files matched your search
@@ -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
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
/ipabusedb-docker-ssh
|
/ipabusedb-docker-ssh
|
||||||
/reports.csv
|
/reports.csv
|
||||||
|
/.env
|
||||||
@@ -21,10 +21,21 @@ Categories are `18,22` (Brute-Force, SSH).
|
|||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
|
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 |
|
| Variable | Purpose |
|
||||||
| ----------------------- | ------------------------------------------------ |
|
| ----------------------- | -------------------------------------------------------- |
|
||||||
| `DOCKER_CONTAINER_NAME` | container whose logs are read (required) |
|
| `DOCKER_CONTAINER_NAME` | container whose logs are read (required) |
|
||||||
| `IPABUSEDB_API_KEY` | AbuseIPDB API key (required only with `--submit`) |
|
| `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:
|
Flags:
|
||||||
|
|
||||||
@@ -34,6 +45,7 @@ Flags:
|
|||||||
| `-o FILE` | write the CSV to `FILE` instead of stdout |
|
| `-o FILE` | write the CSV to `FILE` instead of stdout |
|
||||||
| `--input FILE` | parse `FILE` (or `-` for stdin) instead of `docker logs` |
|
| `--input FILE` | parse `FILE` (or `-` for stdin) instead of `docker logs` |
|
||||||
| `--submit` | POST the CSV to the AbuseIPDB bulk-report endpoint |
|
| `--submit` | POST the CSV to the AbuseIPDB bulk-report endpoint |
|
||||||
|
| `--env FILE` | env file to autoload (default `.env`; missing is ignored) |
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
module sneak.berlin/go/hacks/ipabusedb-docker-ssh
|
module sneak.berlin/go/hacks/ipabusedb-docker-ssh
|
||||||
|
|
||||||
go 1.26.5
|
go 1.26.5
|
||||||
|
|
||||||
|
require github.com/joho/godotenv v1.5.1
|
||||||
@@ -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=
|
||||||
@@ -2,10 +2,13 @@
|
|||||||
// "docker logs -t", extracts brute-force / invalid-user attempts, and
|
// "docker logs -t", extracts brute-force / invalid-user attempts, and
|
||||||
// emits a CSV suitable for the AbuseIPDB bulk-report endpoint.
|
// 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)
|
// DOCKER_CONTAINER_NAME container whose logs are read (required)
|
||||||
// IPABUSEDB_API_KEY AbuseIPDB API key (required with --submit)
|
// 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:
|
// Flags:
|
||||||
//
|
//
|
||||||
@@ -35,6 +38,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/joho/godotenv"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -74,8 +79,13 @@ type parser struct {
|
|||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
events map[netip.Addr]*event
|
events map[netip.Addr]*event
|
||||||
skipped int
|
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 {
|
func newParser(cutoff time.Time) *parser {
|
||||||
return &parser{cutoff: cutoff, events: map[netip.Addr]*event{}}
|
return &parser{cutoff: cutoff, events: map[netip.Addr]*event{}}
|
||||||
}
|
}
|
||||||
@@ -92,6 +102,10 @@ func (p *parser) handleLine(line string) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
p.mu.Lock()
|
p.mu.Lock()
|
||||||
p.skipped++
|
p.skipped++
|
||||||
|
p.unparsed = append(p.unparsed, line)
|
||||||
|
if len(p.unparsed) > keepUnparsed {
|
||||||
|
p.unparsed = p.unparsed[1:]
|
||||||
|
}
|
||||||
p.mu.Unlock()
|
p.mu.Unlock()
|
||||||
return
|
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, "--since", since.String())
|
||||||
}
|
}
|
||||||
args = append(args, container)
|
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...)
|
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()
|
stdout, err := cmd.StdoutPipe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -221,6 +241,12 @@ func readDockerLogs(ctx context.Context, p *parser, container string, since time
|
|||||||
wg.Wait()
|
wg.Wait()
|
||||||
close(errs)
|
close(errs)
|
||||||
if err := cmd.Wait(); err != nil {
|
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)
|
return fmt.Errorf("docker logs %s: %w", container, err)
|
||||||
}
|
}
|
||||||
for err := range errs {
|
for err := range errs {
|
||||||
@@ -286,8 +312,14 @@ func run() error {
|
|||||||
outPath := flag.String("o", "", "write CSV to this file instead of stdout")
|
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")
|
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")
|
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()
|
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")
|
apiKey := os.Getenv("IPABUSEDB_API_KEY")
|
||||||
container := os.Getenv("DOCKER_CONTAINER_NAME")
|
container := os.Getenv("DOCKER_CONTAINER_NAME")
|
||||||
if *doSubmit && apiKey == "" {
|
if *doSubmit && apiKey == "" {
|
||||||
|
|||||||
Reference in new issue
Block a user