From 97b2545e2df1dde4325fd8f8d64eeccccfd959e8 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Sun, 10 May 2026 18:59:18 +0800 Subject: [PATCH 1/4] Add CI/CD pipeline --- .drone-manifest.yml | 22 ++++++ .drone.yml | 187 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 209 insertions(+) create mode 100644 .drone-manifest.yml create mode 100644 .drone.yml diff --git a/.drone-manifest.yml b/.drone-manifest.yml new file mode 100644 index 0000000..45124a5 --- /dev/null +++ b/.drone-manifest.yml @@ -0,0 +1,22 @@ +image: happydomain/checker-caa:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}latest{{/if}} +{{#if build.tags}} +tags: +{{#each build.tags}} + - {{this}} +{{/each}} +{{/if}} +manifests: + - image: happydomain/checker-caa:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-amd64 + platform: + architecture: amd64 + os: linux + - image: happydomain/checker-caa:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm64 + platform: + architecture: arm64 + os: linux + variant: v8 + - image: happydomain/checker-caa:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm + platform: + architecture: arm + os: linux + variant: v7 diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..e96dd53 --- /dev/null +++ b/.drone.yml @@ -0,0 +1,187 @@ +--- +kind: pipeline +type: docker +name: build-amd64 + +platform: + os: linux + arch: amd64 + +steps: + - name: checker build + image: golang:1-alpine + commands: + - apk add --no-cache git make + - make + environment: + CHECKER_VERSION: "${DRONE_BRANCH}-${DRONE_COMMIT}" + CGO_ENABLED: 0 + when: + event: + exclude: + - tag + + - name: checker build tag + image: golang:1-alpine + commands: + - apk add --no-cache git make + - make + environment: + CHECKER_VERSION: "${DRONE_SEMVER}" + CGO_ENABLED: 0 + when: + event: + - tag + + - name: publish on Docker Hub + image: plugins/docker + settings: + repo: happydomain/checker-caa + auto_tag: true + auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} + dockerfile: Dockerfile + build_args: + - CHECKER_VERSION=${DRONE_BRANCH}-${DRONE_COMMIT} + username: + from_secret: docker_username + password: + from_secret: docker_password + when: + event: + exclude: + - tag + + - name: publish on Docker Hub (tag) + image: plugins/docker + settings: + repo: happydomain/checker-caa + auto_tag: true + auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} + dockerfile: Dockerfile + build_args: + - CHECKER_VERSION=${DRONE_SEMVER} + username: + from_secret: docker_username + password: + from_secret: docker_password + when: + event: + - tag + +trigger: + branch: + exclude: + - renovate/* + event: + - cron + - push + - tag + +--- +kind: pipeline +type: docker +name: build-arm64 + +platform: + os: linux + arch: arm64 + +steps: + - name: checker build + image: golang:1-alpine + commands: + - apk add --no-cache git make + - make + environment: + CHECKER_VERSION: "${DRONE_BRANCH}-${DRONE_COMMIT}" + CGO_ENABLED: 0 + when: + event: + exclude: + - tag + + - name: checker build tag + image: golang:1-alpine + commands: + - apk add --no-cache git make + - make + environment: + CHECKER_VERSION: "${DRONE_SEMVER}" + CGO_ENABLED: 0 + when: + event: + - tag + + - name: publish on Docker Hub + image: plugins/docker + settings: + repo: happydomain/checker-caa + auto_tag: true + auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} + dockerfile: Dockerfile + build_args: + - CHECKER_VERSION=${DRONE_BRANCH}-${DRONE_COMMIT} + username: + from_secret: docker_username + password: + from_secret: docker_password + when: + event: + exclude: + - tag + + - name: publish on Docker Hub (tag) + image: plugins/docker + settings: + repo: happydomain/checker-caa + auto_tag: true + auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} + dockerfile: Dockerfile + build_args: + - CHECKER_VERSION=${DRONE_SEMVER} + username: + from_secret: docker_username + password: + from_secret: docker_password + when: + event: + - tag + +trigger: + event: + - cron + - push + - tag + +--- +kind: pipeline +name: docker-manifest + +platform: + os: linux + arch: arm64 + +steps: + - name: publish on Docker Hub + image: plugins/manifest + settings: + auto_tag: true + ignore_missing: true + spec: .drone-manifest.yml + username: + from_secret: docker_username + password: + from_secret: docker_password + +trigger: + branch: + exclude: + - renovate/* + event: + - cron + - push + - tag + +depends_on: + - build-amd64 + - build-arm64 From c6400c7773d3ede713252e1f2e081a6c55d539ec Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Fri, 15 May 2026 18:44:21 +0800 Subject: [PATCH 2/4] feat: publish tls.endpoint.v1 discovery entry to enable GetRelated --- README.md | 19 ++++--------------- checker/discover.go | 26 ++++++++++++++++++++++++++ go.mod | 1 + go.sum | 2 ++ 4 files changed, 33 insertions(+), 15 deletions(-) create mode 100644 checker/discover.go diff --git a/README.md b/README.md index 7f72fb6..838cda5 100644 --- a/README.md +++ b/README.md @@ -25,22 +25,11 @@ Identifiers" mapping. - compares the observed identifiers against the `issue` / `issuewild` allow list (or flags a `DisallowIssue` violation). -## Observation payload +## Rules -This checker does not publish endpoints or add a new observation -schema. Under its own observation key `caa_policy` it returns a -pass-through view of the zone-side CAA records: - -```json -{ - "domain": "example.net", - "records": [ - { "flag": 0, "tag": "issue", "value": "letsencrypt.org" }, - { "flag": 0, "tag": "issuewild", "value": ";" } - ], - "run_at": "2026-04-22T12:34:56Z" -} -``` +| Code | Description | Severity | +|--------------------|----------------------------------------------------------------------------------------------------------------------|----------| +| `caa_compliance` | Cross-references TLS certificates observed on the domain against its CAA `issue`/`issuewild` policy, mapping each observed issuer to its CCADB-published CAA identifier. | CRITICAL | ## Rule outcomes diff --git a/checker/discover.go b/checker/discover.go new file mode 100644 index 0000000..76dadbf --- /dev/null +++ b/checker/discover.go @@ -0,0 +1,26 @@ +package checker + +import ( + sdk "git.happydns.org/checker-sdk-go/checker" + tlscontract "git.happydns.org/checker-tls/contract" +) + +// DiscoverEntries publishes one tls.endpoint.v1 entry for the domain's HTTPS +// endpoint so checker-tls probes it. Implements sdk.DiscoveryPublisher. +// On the next checker-tls run the engine will store a DiscoveryObservationRef +// linking its snapshot back to this checker; GetRelated("tls_probes") in the +// rule will then return the observed certificates. +func (p *caaProvider) DiscoverEntries(data any) ([]sdk.DiscoveryEntry, error) { + d, ok := data.(*CAAData) + if !ok || d == nil || d.Domain == "" { + return nil, nil + } + entry, err := tlscontract.NewEntry(tlscontract.TLSEndpoint{ + Host: d.Domain, + Port: 443, + }) + if err != nil { + return nil, err + } + return []sdk.DiscoveryEntry{entry}, nil +} diff --git a/go.mod b/go.mod index b75831c..aad28fc 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.25.0 require ( git.happydns.org/checker-sdk-go v1.5.0 + git.happydns.org/checker-tls v0.6.2 github.com/miekg/dns v1.1.72 ) diff --git a/go.sum b/go.sum index 2a80023..d4ff867 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ git.happydns.org/checker-sdk-go v1.5.0 h1:5uD5Cm6xJ+lwnhbJ09iCXGHbYS9zRh+Yh0NeBHkAPBY= git.happydns.org/checker-sdk-go v1.5.0/go.mod h1:aNAcfYFfbhvH9kJhE0Njp5GX0dQbxdRB0rJ0KvSC5nI= +git.happydns.org/checker-tls v0.6.2 h1:8oKia1XlD+tklyqrwzmUgFH1Kw8VLSLLF9suZ7Qr14E= +git.happydns.org/checker-tls v0.6.2/go.mod h1:9tpnxg0iOwS+7If64DRG1jqYonUAgxOBuxwfF5mVkL4= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI= From 8b7df158837c57b24906b13188f6eab648b182f4 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Fri, 15 May 2026 21:59:32 +0800 Subject: [PATCH 3/4] Include certificate count in issuer check state messages Add a per-issuer certificate counter to issuerAgg and append the count to each CheckState message and Meta map, so operators can see how many certificates were observed per issuer at a glance. --- checker/rule.go | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/checker/rule.go b/checker/rule.go index 6850730..7e91fb5 100644 --- a/checker/rule.go +++ b/checker/rule.go @@ -31,6 +31,7 @@ type issuerAgg struct { code string msg string endpoints map[string]bool + count int // number of certificates observed from this issuer } type allowList struct { @@ -152,6 +153,7 @@ func (r *caaRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, opts cur = &issuerAgg{sample: p, endpoints: map[string]bool{}} agg[k] = cur } + cur.count++ if severityRank(severity) >= severityRank(cur.severity) { cur.severity = severity cur.code = code @@ -233,22 +235,23 @@ func (r *caaRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, opts endpoints = append(endpoints, ep) } sort.Strings(endpoints) - meta := map[string]any{"endpoints": endpoints} + meta := map[string]any{"endpoints": endpoints, "cert_count": a.count} + certSuffix := fmt.Sprintf(" (%d certificate(s) checked)", a.count) switch a.severity { case SeverityCrit: out = append(out, sdk.CheckState{ - Status: sdk.StatusCrit, Message: a.msg, Code: a.code, + Status: sdk.StatusCrit, Message: a.msg + certSuffix, Code: a.code, Subject: subject, Meta: meta, }) case SeverityWarn: out = append(out, sdk.CheckState{ - Status: sdk.StatusWarn, Message: a.msg, Code: a.code, + Status: sdk.StatusWarn, Message: a.msg + certSuffix, Code: a.code, Subject: subject, Meta: meta, }) case SeverityInfo: out = append(out, sdk.CheckState{ - Status: sdk.StatusInfo, Message: a.msg, Code: a.code, + Status: sdk.StatusInfo, Message: a.msg + certSuffix, Code: a.code, Subject: subject, Meta: meta, }) default: @@ -257,7 +260,7 @@ func (r *caaRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, opts msg = "Certificate observed; no CAA records published" } out = append(out, sdk.CheckState{ - Status: sdk.StatusOK, Message: msg, Code: CodeOK, + Status: sdk.StatusOK, Message: msg + certSuffix, Code: CodeOK, Subject: subject, Meta: meta, }) } From 59af24f695dc63ae26968fe420f5104afdfda49f Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Sat, 16 May 2026 13:04:51 +0800 Subject: [PATCH 4/4] Remove redundant RunAt field from CAAData The observation timestamp is already stored by the core; there is no need to duplicate it inside the payload. --- checker/collect.go | 2 -- checker/types.go | 1 - 2 files changed, 3 deletions(-) diff --git a/checker/collect.go b/checker/collect.go index b378de2..ad0d927 100644 --- a/checker/collect.go +++ b/checker/collect.go @@ -4,7 +4,6 @@ import ( "context" "encoding/json" "fmt" - "time" sdk "git.happydns.org/checker-sdk-go/checker" ) @@ -64,7 +63,6 @@ func (p *caaProvider) Collect(ctx context.Context, opts sdk.CheckerOptions) (any return &CAAData{ Domain: domain, Records: records, - RunAt: time.Now().UTC().Format(time.RFC3339), }, nil } diff --git a/checker/types.go b/checker/types.go index b42c67b..a266ff3 100644 --- a/checker/types.go +++ b/checker/types.go @@ -40,7 +40,6 @@ const ( type CAAData struct { Domain string `json:"domain,omitempty"` Records []CAARecord `json:"records,omitempty"` - RunAt string `json:"run_at,omitempty"` } type CAARecord struct {