REST API for storing, querying, and managing DMARC report and domain data.
  • Go 99.8%
  • Dockerfile 0.2%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
alice 0465db9c8e
All checks were successful
Bump Tag on Main / bump-tag (push) Successful in 9s
service-dmarc-api CI / build-and-push (push) Successful in 3m35s
Go lint / lint (push) Successful in 3m56s
Merge pull request 'Fix v0.2.8 migration: backfill org_id instead of a bare NOT NULL add' (#20) from fix/token-org-id-migration into main
Reviewed-on: #20
2026-08-28 13:47:52 +00:00
.forgejo/workflows ci(github-action)!: Update action actions/setup-go ( v4.3.0 → v7.0.0 ) 2026-08-28 05:45:34 +10:00
.github/instructions inital commit 2026-05-05 17:16:11 +10:00
cmd/api inital commit again 2026-05-09 04:54:53 +10:00
docs Add backstage 2026-05-18 20:15:56 +10:00
internal Fix v0.2.8 migration: backfill org_id instead of a bare NOT NULL add 2026-08-28 23:34:27 +10:00
.dockerignore inital commit 2026-05-05 17:16:11 +10:00
.gitignore inital commit 2026-05-05 17:16:11 +10:00
catalog-info.yaml chore: update Backstage catalog source location to Forgejo 2026-08-28 01:36:52 +10:00
Dockerfile feat(container): update image docker.io/library/golang ( 1.25 → 1.27 ) 2026-08-28 05:45:34 +10:00
go.mod fix(deps): update module github.com/mattn/go-sqlite3 ( v1.14.44 → v1.14.50 ) 2026-08-28 05:45:22 +10:00
go.sum fix(deps): update module github.com/mattn/go-sqlite3 ( v1.14.44 → v1.14.50 ) 2026-08-28 05:45:22 +10:00
INCIDENT-RUNBOOK.md feat: add personal API token authentication 2026-08-28 01:37:02 +10:00
README.md docs: add Forgejo Actions status badges to README 2026-08-28 02:28:47 +10:00
renovate.json refactor: extend shared Renovate preset instead of duplicating config 2026-08-28 04:03:36 +10:00
todo.md feat: add personal API token authentication 2026-08-28 01:37:02 +10:00

service-dmarc-api

build-and-push lint

A REST API service for storing and querying DMARC aggregate reports. Accepts reports from the DMARC loader and provides query endpoints.

Requirements

  • Go 1.25+
  • SQLite (included in Go) or PostgreSQL

Configuration

SQLite (default, local development)

go run ./cmd/api

Data stored in dmarc.db in the current directory.

PostgreSQL (production)

export DATABASE_URL="postgres://user:password@localhost/dmarc"
go run ./cmd/api -db-driver postgres

Build

go mod tidy
go build -o api ./cmd/api
./api -addr :8080

Swagger UI

Once the service is running, browse to:

GET /api/docs

The OpenAPI spec is also available at:

GET /api/docs/openapi.json

API Endpoints

Health Check

GET /api/health

Post a Report

POST /api/dmarc/reports
Content-Type: application/json

{
  "report_id": "example.com-1234567890",
  "reporter_email": "noreply-dmarc-support@example.net",
  "org_name": "Example Inc",
  "domain": "example.com",
  "begin_date": 1234567890,
  "end_date": 1234654290,
  "policy_p": "reject",
  "policy_sp": "reject",
  "policy_adkim": "r",
  "policy_aspf": "r",
  "policy_pct": 100,
  "total_messages": 42,
  "raw_xml": "<base64-encoded-xml>",
  "records": [
    {
      "source_ip": "192.0.2.1",
      "count": 10,
      "policy_disp": "none",
      "dkim_result": "pass",
      "spf_result": "pass",
      "header_from": "example.com",
      "envelope_from": "noreply@example.com",
      "envelope_to": "user@example.com"
    }
  ]
}

Note: duplicate detection is based on the pair (reporter_email, report_id). Legacy alias: POST /api/reports

Response:

{
  "id": 1
}

Duplicate response:

HTTP 409 Conflict
{
  "error": "duplicate report",
  "code": "REPORT_DUPLICATE",
  "report_id": "example.com-1234567890",
  "reporter_email": "noreply-dmarc-support@example.net"
}

List Reports

GET /api/dmarc/reports?domain=example.com&limit=10&start_date=2026-01-01&end_date=2026-01-31

Legacy alias: GET /api/reports

Query parameters (optional):

  • domain - Filter by domain
  • limit - Max results (default 100)
  • after_date - Legacy RFC3339 timestamp, treated as start_date when start_date is not set
  • start_date - Start bound for begin_date (YYYY-MM-DD or RFC3339)
  • end_date - End bound for begin_date (YYYY-MM-DD or RFC3339)

Response:

{
  "total": 2,
  "reports": [
    {
      "id": 1,
      "report_id": "example.com-1234567890",
      "org_name": "Example Inc",
      "domain": "example.com",
      "begin_date": "2026-01-01T00:00:00Z",
      "end_date": "2026-01-02T00:00:00Z",
      "policy_p": "reject",
      ...
    }
  ]
}

Get Report with Records

GET /api/dmarc/reports/{id}

Legacy alias: GET /api/reports/{id}

Response:

{
  "report": {
    "id": 1,
    "report_id": "example.com-1234567890",
    ...
  },
  "records": [
    {
      "id": 1,
      "report_id": 1,
      "source_ip": "192.0.2.1",
      "count": 10,
      ...
    }
  ]
}

Get Domain DMARC Aggregate Records

GET /api/dmarc/domains/{domain}/records?limit=1000&start_date=2026-01-01&end_date=2026-01-31

Legacy alias: GET /api/domains/{domain}/records

Project Structure

service-dmarc-api/
  cmd/api/
    main.go                 # CLI entry point
  internal/
    db/
      db.go                 # Database setup and migrations
      store.go              # Database queries
    models/
      report.go             # Data models
    server/
      server.go             # HTTP handlers
  go.mod
  README.md

Observability Conventions

Logs

  • All request failures and write audits are emitted as structured logs.
  • Denied access events are logged as access_denied with fields:
    • action, result, reason, status
    • request_id, method, path, actor, actor_type
  • DB write audit events are logged as db_change_audit.

Metrics

  • HTTP server metrics:
    • dmarc_api_http_requests_total
    • dmarc_api_http_request_duration_seconds
  • Operation metrics (write paths):
    • dmarc_api_operations_total{operation,result,error_class}
    • dmarc_api_operation_duration_seconds{operation}
  • Auth, verification, and workflow metrics:
    • dmarc_api_auth_failures_total{guard,reason}
    • dmarc_api_domain_verify_attempts_total{forced,result}
    • dmarc_api_domain_verify_dns_lookup_failures_total
    • dmarc_api_defaults_seeded_total{record_type,source}
    • dmarc_api_instance_register_total{service,result}

Tracing

  • Request-level instrumentation is enabled via OpenTelemetry HTTP middleware.
  • Write operations create additional spans named by route operation keys.
  • Outbound HTTP requests propagate trace context and X-Request-Id.

Request ID and Trace Propagation

  • Incoming request IDs are read from REQUEST_ID_HEADERS (default includes X-Request-Id).
  • If no inbound request ID exists, a UUID is generated and returned as X-Request-Id.
  • Outbound HTTP requests inject:
    • W3C trace context via OTel propagator
    • X-Request-Id from request context when available