Compare commits

..

4 commits

Author SHA1 Message Date
59af24f695 Remove redundant RunAt field from CAAData
All checks were successful
continuous-integration/drone/push Build is passing
The observation timestamp is already stored by the core; there is no
need to duplicate it inside the payload.
2026-05-16 13:05:05 +08:00
8b7df15883 Include certificate count in issuer check state messages
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
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.
2026-05-15 21:59:56 +08:00
c6400c7773 feat: publish tls.endpoint.v1 discovery entry to enable GetRelated
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2026-05-15 18:44:35 +08:00
97b2545e2d Add CI/CD pipeline
All checks were successful
continuous-integration/drone/push Build is passing
2026-05-10 19:02:20 +08:00
9 changed files with 251 additions and 24 deletions

22
.drone-manifest.yml Normal file
View file

@ -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

187
.drone.yml Normal file
View file

@ -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

View file

@ -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

View file

@ -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
}

26
checker/discover.go Normal file
View file

@ -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
}

View file

@ -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,
})
}

View file

@ -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 {

1
go.mod
View file

@ -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
)

2
go.sum
View file

@ -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=