Compare commits
9 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3490b5ee2b | |||
| 5acf464d4e | |||
| 84649767c9 | |||
| 5ba745982e | |||
| 2710dfb459 | |||
| 181c5961f1 | |||
| 463e3fb457 | |||
| bf409ba33c | |||
| a9a704c0ff |
26 changed files with 1003 additions and 120 deletions
22
.drone-manifest.yml
Normal file
22
.drone-manifest.yml
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
image: happydomain/checker-zonemaster:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}latest{{/if}}
|
||||||
|
{{#if build.tags}}
|
||||||
|
tags:
|
||||||
|
{{#each build.tags}}
|
||||||
|
- {{this}}
|
||||||
|
{{/each}}
|
||||||
|
{{/if}}
|
||||||
|
manifests:
|
||||||
|
- image: happydomain/checker-zonemaster:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-amd64
|
||||||
|
platform:
|
||||||
|
architecture: amd64
|
||||||
|
os: linux
|
||||||
|
- image: happydomain/checker-zonemaster:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm64
|
||||||
|
platform:
|
||||||
|
architecture: arm64
|
||||||
|
os: linux
|
||||||
|
variant: v8
|
||||||
|
- image: happydomain/checker-zonemaster:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm
|
||||||
|
platform:
|
||||||
|
architecture: arm
|
||||||
|
os: linux
|
||||||
|
variant: v7
|
||||||
187
.drone.yml
Normal file
187
.drone.yml
Normal 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-zonemaster
|
||||||
|
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-zonemaster
|
||||||
|
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-zonemaster
|
||||||
|
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-zonemaster
|
||||||
|
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
|
||||||
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
checker-zonemaster
|
||||||
|
checker-zonemaster.so
|
||||||
|
|
@ -10,5 +10,8 @@ RUN CGO_ENABLED=0 go build -ldflags "-X main.Version=${CHECKER_VERSION}" -o /che
|
||||||
|
|
||||||
FROM scratch
|
FROM scratch
|
||||||
COPY --from=builder /checker-zonemaster /checker-zonemaster
|
COPY --from=builder /checker-zonemaster /checker-zonemaster
|
||||||
|
USER 65534:65534
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
|
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||||
|
CMD ["/checker-zonemaster", "-healthcheck"]
|
||||||
ENTRYPOINT ["/checker-zonemaster"]
|
ENTRYPOINT ["/checker-zonemaster"]
|
||||||
|
|
|
||||||
7
Makefile
7
Makefile
|
|
@ -6,12 +6,12 @@ CHECKER_SOURCES := main.go $(wildcard checker/*.go)
|
||||||
|
|
||||||
GO_LDFLAGS := -X main.Version=$(CHECKER_VERSION)
|
GO_LDFLAGS := -X main.Version=$(CHECKER_VERSION)
|
||||||
|
|
||||||
.PHONY: all plugin docker clean
|
.PHONY: all plugin docker test clean
|
||||||
|
|
||||||
all: $(CHECKER_NAME)
|
all: $(CHECKER_NAME)
|
||||||
|
|
||||||
$(CHECKER_NAME): $(CHECKER_SOURCES)
|
$(CHECKER_NAME): $(CHECKER_SOURCES)
|
||||||
go build -ldflags "$(GO_LDFLAGS)" -o $@ .
|
go build -tags standalone -ldflags "$(GO_LDFLAGS)" -o $@ .
|
||||||
|
|
||||||
plugin: $(CHECKER_NAME).so
|
plugin: $(CHECKER_NAME).so
|
||||||
|
|
||||||
|
|
@ -21,5 +21,8 @@ $(CHECKER_NAME).so: $(CHECKER_SOURCES) $(wildcard plugin/*.go)
|
||||||
docker:
|
docker:
|
||||||
docker build --build-arg CHECKER_VERSION=$(CHECKER_VERSION) -t $(CHECKER_IMAGE) .
|
docker build --build-arg CHECKER_VERSION=$(CHECKER_VERSION) -t $(CHECKER_IMAGE) .
|
||||||
|
|
||||||
|
test:
|
||||||
|
go test -tags standalone ./...
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -f $(CHECKER_NAME) $(CHECKER_NAME).so
|
rm -f $(CHECKER_NAME) $(CHECKER_NAME).so
|
||||||
|
|
|
||||||
48
README.md
48
README.md
|
|
@ -55,6 +55,20 @@ the running checker-zonemaster server (e.g.,
|
||||||
`http://checker-zonemaster:8080`). happyDomain will delegate observation
|
`http://checker-zonemaster:8080`). happyDomain will delegate observation
|
||||||
collection to this endpoint.
|
collection to this endpoint.
|
||||||
|
|
||||||
|
### Deployment
|
||||||
|
|
||||||
|
The `/collect` endpoint has no built-in authentication and will issue
|
||||||
|
JSON-RPC calls to whatever Zonemaster API URL is configured via the
|
||||||
|
`zonemasterAPIURL` admin option (defaulting to the official public API
|
||||||
|
at `https://zonemaster.net/api`). Operators should point this option
|
||||||
|
only at trusted Zonemaster instances; pointing it at an untrusted host
|
||||||
|
turns the checker into an SSRF vector, since responses are parsed and
|
||||||
|
surfaced back to the caller. The checker itself is meant to run on a
|
||||||
|
trusted network, reachable only by the happyDomain instance that drives
|
||||||
|
it. Restrict access via a reverse proxy with authentication, a network
|
||||||
|
ACL, or by binding the listener to a private interface; do not expose
|
||||||
|
it directly to the public internet.
|
||||||
|
|
||||||
## Options
|
## Options
|
||||||
|
|
||||||
| Scope | Id | Description |
|
| Scope | Id | Description |
|
||||||
|
|
@ -64,29 +78,21 @@ collection to this endpoint.
|
||||||
| User | `language` | Result language (`en`, `fr`, `de`, …) |
|
| User | `language` | Result language (`en`, `fr`, `de`, …) |
|
||||||
| Admin | `zonemasterAPIURL` | Zonemaster JSON-RPC endpoint (default: official API) |
|
| Admin | `zonemasterAPIURL` | Zonemaster JSON-RPC endpoint (default: official API) |
|
||||||
|
|
||||||
## Protocol
|
## Rules
|
||||||
|
|
||||||
### POST /collect
|
Each rule wraps one Zonemaster test module and emits a `<rule>.summary`
|
||||||
|
state plus one `<rule>.<level>` state per WARNING-or-worse Zonemaster
|
||||||
|
message, so downstream consumers can match on stable codes.
|
||||||
|
|
||||||
Request:
|
| Code | Description | Severity |
|
||||||
```json
|
|---------------------------|-----------------------------------------------------------------------------------|----------|
|
||||||
{
|
| `zonemaster.dnssec` | DNSSEC tests (signatures, NSEC/NSEC3, DS/DNSKEY coherence). | CRITICAL |
|
||||||
"key": "zonemaster",
|
| `zonemaster.delegation` | Delegation tests (parent/child NS agreement, glue, referrals). | CRITICAL |
|
||||||
"options": {
|
| `zonemaster.consistency` | Consistency tests (SOA serial, NS set, zone content across servers). | CRITICAL |
|
||||||
"domainName": "example.com",
|
| `zonemaster.connectivity` | Connectivity tests (UDP/TCP reachability of authoritative servers, AS diversity). | CRITICAL |
|
||||||
"zonemasterAPIURL": "https://zonemaster.net/api",
|
| `zonemaster.nameserver` | Nameserver tests (server behaviour, EDNS, unknown RR handling). | CRITICAL |
|
||||||
"language": "en",
|
| `zonemaster.syntax` | Syntax tests (domain name syntax, hostname legality). | CRITICAL |
|
||||||
"profile": "default"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The collect call is long-running: it starts a Zonemaster test, polls until
|
|
||||||
completion, and returns the full result tree as the observation payload.
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
This project is licensed under the **MIT License** (see `LICENSE`). The
|
MIT (see `LICENSE`). Third-party attributions in `NOTICE`.
|
||||||
third-party Apache-2.0 attributions for `checker-sdk-go` are recorded in
|
|
||||||
`NOTICE` and must accompany any binary or source redistribution of this
|
|
||||||
project.
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
@ -13,6 +14,37 @@ import (
|
||||||
sdk "git.happydns.org/checker-sdk-go/checker"
|
sdk "git.happydns.org/checker-sdk-go/checker"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// maxResponseBytes caps the body size we'll read from the Zonemaster API.
|
||||||
|
// Real result payloads are tens to a few hundred KB; 8 MiB is generous head-
|
||||||
|
// room and still bounded so a misbehaving or hostile endpoint can't exhaust
|
||||||
|
// memory.
|
||||||
|
const maxResponseBytes = 8 << 20
|
||||||
|
|
||||||
|
// maxCollectDuration caps the total time spent collecting (start + poll +
|
||||||
|
// fetch). The caller's context still wins if it has a tighter deadline.
|
||||||
|
const maxCollectDuration = 15 * time.Minute
|
||||||
|
|
||||||
|
// pollInterval is how often we ask the Zonemaster API for test progress.
|
||||||
|
const pollInterval = 2 * time.Second
|
||||||
|
|
||||||
|
// zmHTTPClient is the HTTP client used for all Zonemaster API calls. It has
|
||||||
|
// per-phase timeouts so a stalling endpoint can never hang us indefinitely
|
||||||
|
// even if the caller passes a context without a deadline.
|
||||||
|
var zmHTTPClient = &http.Client{
|
||||||
|
Timeout: 60 * time.Second,
|
||||||
|
Transport: &http.Transport{
|
||||||
|
Proxy: http.ProxyFromEnvironment,
|
||||||
|
DialContext: (&net.Dialer{
|
||||||
|
Timeout: 10 * time.Second,
|
||||||
|
KeepAlive: 30 * time.Second,
|
||||||
|
}).DialContext,
|
||||||
|
TLSHandshakeTimeout: 10 * time.Second,
|
||||||
|
ResponseHeaderTimeout: 30 * time.Second,
|
||||||
|
ExpectContinueTimeout: 1 * time.Second,
|
||||||
|
IdleConnTimeout: 90 * time.Second,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
func (p *zonemasterProvider) Collect(ctx context.Context, opts sdk.CheckerOptions) (any, error) {
|
func (p *zonemasterProvider) Collect(ctx context.Context, opts sdk.CheckerOptions) (any, error) {
|
||||||
domainName, ok := opts["domainName"].(string)
|
domainName, ok := opts["domainName"].(string)
|
||||||
if !ok || domainName == "" {
|
if !ok || domainName == "" {
|
||||||
|
|
@ -36,6 +68,11 @@ func (p *zonemasterProvider) Collect(ctx context.Context, opts sdk.CheckerOption
|
||||||
profile = prof
|
profile = prof
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cap the total collection time even when the caller's context has no
|
||||||
|
// deadline. The caller's deadline still wins if it's tighter.
|
||||||
|
ctx, cancel := context.WithTimeout(ctx, maxCollectDuration)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
// Step 1: start the test.
|
// Step 1: start the test.
|
||||||
startResult, err := zmCallJSONRPC(ctx, apiURL, "start_domain_test", zmStartTestParams{
|
startResult, err := zmCallJSONRPC(ctx, apiURL, "start_domain_test", zmStartTestParams{
|
||||||
Domain: domainName,
|
Domain: domainName,
|
||||||
|
|
@ -56,9 +93,10 @@ func (p *zonemasterProvider) Collect(ctx context.Context, opts sdk.CheckerOption
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 2: poll for completion.
|
// Step 2: poll for completion.
|
||||||
ticker := time.NewTicker(2 * time.Second)
|
ticker := time.NewTicker(pollInterval)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
poll:
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
|
|
@ -75,12 +113,11 @@ func (p *zonemasterProvider) Collect(ctx context.Context, opts sdk.CheckerOption
|
||||||
}
|
}
|
||||||
|
|
||||||
if progress >= 100 {
|
if progress >= 100 {
|
||||||
goto testComplete
|
break poll
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
testComplete:
|
|
||||||
// Step 3: fetch results.
|
// Step 3: fetch results.
|
||||||
rawResults, err := zmCallJSONRPC(ctx, apiURL, "get_test_results", zmGetResultsParams{
|
rawResults, err := zmCallJSONRPC(ctx, apiURL, "get_test_results", zmGetResultsParams{
|
||||||
ID: testID,
|
ID: testID,
|
||||||
|
|
@ -117,19 +154,37 @@ func zmCallJSONRPC(ctx context.Context, apiURL, method string, params any) (json
|
||||||
}
|
}
|
||||||
req.Header.Set("Content-Type", "application/json")
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
resp, err := http.DefaultClient.Do(req)
|
resp, err := zmHTTPClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to call API: %w", err)
|
return nil, fmt.Errorf("failed to call API: %w", err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// Cap the body we'll ever read so a misbehaving endpoint can't exhaust
|
||||||
|
// memory. +1 lets us detect that the cap was hit.
|
||||||
|
limited := io.LimitReader(resp.Body, maxResponseBytes+1)
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
b, _ := io.ReadAll(resp.Body)
|
b, readErr := io.ReadAll(limited)
|
||||||
|
if readErr != nil {
|
||||||
|
return nil, fmt.Errorf("API returned status %d (failed to read body: %v)", resp.StatusCode, readErr)
|
||||||
|
}
|
||||||
|
if len(b) > maxResponseBytes {
|
||||||
|
b = b[:maxResponseBytes]
|
||||||
|
}
|
||||||
return nil, fmt.Errorf("API returned status %d: %s", resp.StatusCode, string(b))
|
return nil, fmt.Errorf("API returned status %d: %s", resp.StatusCode, string(b))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body, readErr := io.ReadAll(limited)
|
||||||
|
if readErr != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read response: %w", readErr)
|
||||||
|
}
|
||||||
|
if len(body) > maxResponseBytes {
|
||||||
|
return nil, fmt.Errorf("API response exceeds %d bytes", maxResponseBytes)
|
||||||
|
}
|
||||||
|
|
||||||
var rpcResp zmJSONRPCResponse
|
var rpcResp zmJSONRPCResponse
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&rpcResp); err != nil {
|
if err := json.Unmarshal(body, &rpcResp); err != nil {
|
||||||
return nil, fmt.Errorf("failed to decode response: %w", err)
|
return nil, fmt.Errorf("failed to decode response: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ import (
|
||||||
var Version = "built-in"
|
var Version = "built-in"
|
||||||
|
|
||||||
// Definition returns the CheckerDefinition for the zonemaster checker.
|
// Definition returns the CheckerDefinition for the zonemaster checker.
|
||||||
func Definition() *sdk.CheckerDefinition {
|
func (p *zonemasterProvider) Definition() *sdk.CheckerDefinition {
|
||||||
return &sdk.CheckerDefinition{
|
return &sdk.CheckerDefinition{
|
||||||
ID: "zonemaster",
|
ID: "zonemaster",
|
||||||
Name: "Zonemaster",
|
Name: "Zonemaster",
|
||||||
|
|
@ -75,13 +75,11 @@ func Definition() *sdk.CheckerDefinition {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Rules: []sdk.CheckRule{
|
Rules: Rules(),
|
||||||
Rule(),
|
|
||||||
},
|
|
||||||
Interval: &sdk.CheckIntervalSpec{
|
Interval: &sdk.CheckIntervalSpec{
|
||||||
Min: 1 * time.Hour,
|
Min: 12 * time.Hour,
|
||||||
Max: 7 * 24 * time.Hour,
|
Max: 30 * 24 * time.Hour,
|
||||||
Default: 24 * time.Hour,
|
Default: 7 * 24 * time.Hour,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
69
checker/interactive.go
Normal file
69
checker/interactive.go
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
//go:build standalone
|
||||||
|
|
||||||
|
package checker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
sdk "git.happydns.org/checker-sdk-go/checker"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (p *zonemasterProvider) RenderForm() []sdk.CheckerOptionField {
|
||||||
|
return []sdk.CheckerOptionField{
|
||||||
|
{
|
||||||
|
Id: "domainName",
|
||||||
|
Type: "string",
|
||||||
|
Label: "Domain name to check",
|
||||||
|
Placeholder: "example.com",
|
||||||
|
Required: true,
|
||||||
|
Description: "Fully-qualified domain name to submit to the Zonemaster engine.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Id: "profile",
|
||||||
|
Type: "string",
|
||||||
|
Label: "Profile",
|
||||||
|
Placeholder: "default",
|
||||||
|
Description: "Zonemaster test profile to apply (engine-defined; usually \"default\").",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Id: "language",
|
||||||
|
Type: "string",
|
||||||
|
Label: "Result language",
|
||||||
|
Placeholder: "en",
|
||||||
|
Description: "Language for human-readable test messages (en, fr, de, es, sv, da, fi, nb, nl, pt).",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Id: "zonemasterAPIURL",
|
||||||
|
Type: "string",
|
||||||
|
Label: "Zonemaster API URL",
|
||||||
|
Placeholder: "https://zonemaster.net/api",
|
||||||
|
Description: "JSON-RPC endpoint of the Zonemaster backend to query.",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *zonemasterProvider) ParseForm(r *http.Request) (sdk.CheckerOptions, error) {
|
||||||
|
domainName := strings.TrimSpace(r.FormValue("domainName"))
|
||||||
|
if domainName == "" {
|
||||||
|
return nil, errors.New("domainName is required")
|
||||||
|
}
|
||||||
|
domainName = strings.TrimSuffix(domainName, ".")
|
||||||
|
|
||||||
|
opts := sdk.CheckerOptions{
|
||||||
|
"domainName": domainName,
|
||||||
|
}
|
||||||
|
|
||||||
|
if v := strings.TrimSpace(r.FormValue("profile")); v != "" {
|
||||||
|
opts["profile"] = v
|
||||||
|
}
|
||||||
|
if v := strings.TrimSpace(r.FormValue("language")); v != "" {
|
||||||
|
opts["language"] = v
|
||||||
|
}
|
||||||
|
if v := strings.TrimSpace(r.FormValue("zonemasterAPIURL")); v != "" {
|
||||||
|
opts["zonemasterAPIURL"] = strings.TrimSuffix(v, "/")
|
||||||
|
}
|
||||||
|
|
||||||
|
return opts, nil
|
||||||
|
}
|
||||||
|
|
@ -14,8 +14,3 @@ type zonemasterProvider struct{}
|
||||||
func (p *zonemasterProvider) Key() sdk.ObservationKey {
|
func (p *zonemasterProvider) Key() sdk.ObservationKey {
|
||||||
return ObservationKeyZonemaster
|
return ObservationKeyZonemaster
|
||||||
}
|
}
|
||||||
|
|
||||||
// Definition implements sdk.CheckerDefinitionProvider.
|
|
||||||
func (p *zonemasterProvider) Definition() *sdk.CheckerDefinition {
|
|
||||||
return Definition()
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -15,13 +15,14 @@ import (
|
||||||
// zmLevelDisplayOrder defines the severity order used for sorting and display.
|
// zmLevelDisplayOrder defines the severity order used for sorting and display.
|
||||||
var zmLevelDisplayOrder = []string{"CRITICAL", "ERROR", "WARNING", "NOTICE", "INFO", "DEBUG"}
|
var zmLevelDisplayOrder = []string{"CRITICAL", "ERROR", "WARNING", "NOTICE", "INFO", "DEBUG"}
|
||||||
|
|
||||||
var zmLevelRank = func() map[string]int {
|
var zmLevelRank = map[string]int{
|
||||||
m := make(map[string]int, len(zmLevelDisplayOrder))
|
"CRITICAL": 6,
|
||||||
for i, l := range zmLevelDisplayOrder {
|
"ERROR": 5,
|
||||||
m[l] = len(zmLevelDisplayOrder) - i
|
"WARNING": 4,
|
||||||
}
|
"NOTICE": 3,
|
||||||
return m
|
"INFO": 2,
|
||||||
}()
|
"DEBUG": 1,
|
||||||
|
}
|
||||||
|
|
||||||
type zmLevelCount struct {
|
type zmLevelCount struct {
|
||||||
Level string
|
Level string
|
||||||
|
|
@ -50,7 +51,7 @@ var zonemasterHTMLTemplate = template.Must(
|
||||||
template.New("zonemaster").
|
template.New("zonemaster").
|
||||||
Funcs(template.FuncMap{
|
Funcs(template.FuncMap{
|
||||||
"badgeClass": func(level string) string {
|
"badgeClass": func(level string) string {
|
||||||
switch strings.ToUpper(level) {
|
switch normLevel(level) {
|
||||||
case "CRITICAL":
|
case "CRITICAL":
|
||||||
return "badge-critical"
|
return "badge-critical"
|
||||||
case "ERROR":
|
case "ERROR":
|
||||||
|
|
@ -71,7 +72,7 @@ var zonemasterHTMLTemplate = template.Must(
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Zonemaster{{if .Domain}} — {{.Domain}}{{end}}</title>
|
<title>Zonemaster{{if .Domain}}, {{.Domain}}{{end}}</title>
|
||||||
<style>
|
<style>
|
||||||
*, *::before, *::after { box-sizing: border-box; }
|
*, *::before, *::after { box-sizing: border-box; }
|
||||||
:root {
|
:root {
|
||||||
|
|
@ -157,7 +158,7 @@ details[open] > summary::before { transform: rotate(90deg); }
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<div class="hd">
|
<div class="hd">
|
||||||
<h1>Zonemaster{{if .Domain}} — <code>{{.Domain}}</code>{{end}}</h1>
|
<h1>Zonemaster{{if .Domain}}, <code>{{.Domain}}</code>{{end}}</h1>
|
||||||
<div class="meta">
|
<div class="meta">
|
||||||
{{- if .CreatedAt}}Run at {{.CreatedAt}}{{end -}}
|
{{- if .CreatedAt}}Run at {{.CreatedAt}}{{end -}}
|
||||||
{{- if and .CreatedAt .HashID}} · {{end -}}
|
{{- if and .CreatedAt .HashID}} · {{end -}}
|
||||||
|
|
@ -222,7 +223,7 @@ func (p *zonemasterProvider) GetHTMLReport(ctx sdk.ReportContext) (string, error
|
||||||
rs := moduleMap[name]
|
rs := moduleMap[name]
|
||||||
counts := map[string]int{}
|
counts := map[string]int{}
|
||||||
for _, r := range rs {
|
for _, r := range rs {
|
||||||
lvl := strings.ToUpper(r.Level)
|
lvl := normLevel(r.Level)
|
||||||
counts[lvl]++
|
counts[lvl]++
|
||||||
totalCounts[lvl]++
|
totalCounts[lvl]++
|
||||||
}
|
}
|
||||||
|
|
|
||||||
267
checker/rule.go
267
checker/rule.go
|
|
@ -9,20 +9,32 @@ import (
|
||||||
sdk "git.happydns.org/checker-sdk-go/checker"
|
sdk "git.happydns.org/checker-sdk-go/checker"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Rule returns a new zonemaster check rule.
|
// Rules returns the full list of CheckRules exposed by the Zonemaster checker.
|
||||||
func Rule() sdk.CheckRule {
|
// Each rule narrows the Zonemaster results to a single test category so
|
||||||
return &zonemasterRule{}
|
// callers can see at a glance which category passed and which did not,
|
||||||
|
// instead of squashing every Zonemaster message into a single monolithic
|
||||||
|
// state. The Zonemaster-returned severity (INFO/NOTICE/WARNING/ERROR/
|
||||||
|
// CRITICAL) is treated as a raw input coming from Zonemaster's own
|
||||||
|
// judgement; each rule maps it onto happyDomain's CheckState.Status.
|
||||||
|
func Rules() []sdk.CheckRule {
|
||||||
|
return []sdk.CheckRule{
|
||||||
|
dnssecRule(),
|
||||||
|
delegationRule(),
|
||||||
|
consistencyRule(),
|
||||||
|
connectivityRule(),
|
||||||
|
nameserverRule(),
|
||||||
|
syntaxRule(),
|
||||||
|
zoneRule(),
|
||||||
|
addressRule(),
|
||||||
|
basicRule(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type zonemasterRule struct{}
|
// ── shared helpers ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
func (r *zonemasterRule) Name() string { return "zonemaster" }
|
// validateZonemasterOptions validates the options accepted by the Zonemaster
|
||||||
|
// checker. Shared across rules that implement OptionsValidator.
|
||||||
func (r *zonemasterRule) Description() string {
|
func validateZonemasterOptions(opts sdk.CheckerOptions) error {
|
||||||
return "Runs Zonemaster DNS validation tests against the zone"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *zonemasterRule) ValidateOptions(opts sdk.CheckerOptions) error {
|
|
||||||
if v, ok := opts["zonemasterAPIURL"]; ok {
|
if v, ok := opts["zonemasterAPIURL"]; ok {
|
||||||
s, ok := v.(string)
|
s, ok := v.(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|
@ -44,69 +56,182 @@ func (r *zonemasterRule) ValidateOptions(opts sdk.CheckerOptions) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *zonemasterRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, opts sdk.CheckerOptions) []sdk.CheckState {
|
// loadZonemasterData fetches the Zonemaster observation. On error, returns a
|
||||||
|
// CheckState the caller should emit to short-circuit its rule.
|
||||||
|
func loadZonemasterData(ctx context.Context, obs sdk.ObservationGetter) (*ZonemasterData, *sdk.CheckState) {
|
||||||
var data ZonemasterData
|
var data ZonemasterData
|
||||||
if err := obs.Get(ctx, ObservationKeyZonemaster, &data); err != nil {
|
if err := obs.Get(ctx, ObservationKeyZonemaster, &data); err != nil {
|
||||||
return []sdk.CheckState{{
|
return nil, &sdk.CheckState{
|
||||||
Status: sdk.StatusError,
|
Status: sdk.StatusError,
|
||||||
Message: fmt.Sprintf("Failed to get Zonemaster data: %v", err),
|
Message: fmt.Sprintf("failed to load Zonemaster observation: %v", err),
|
||||||
Code: "zonemaster_error",
|
Code: "zonemaster.observation_error",
|
||||||
}}
|
|
||||||
}
|
|
||||||
|
|
||||||
var errorCount, warningCount int
|
|
||||||
var criticalMsgs []string
|
|
||||||
|
|
||||||
for _, res := range data.Results {
|
|
||||||
switch strings.ToUpper(res.Level) {
|
|
||||||
case "CRITICAL", "ERROR":
|
|
||||||
errorCount++
|
|
||||||
if len(criticalMsgs) < 5 {
|
|
||||||
criticalMsgs = append(criticalMsgs, res.Message)
|
|
||||||
}
|
|
||||||
case "WARNING":
|
|
||||||
warningCount++
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return &data, nil
|
||||||
meta := map[string]any{
|
}
|
||||||
"errorCount": errorCount,
|
|
||||||
"warningCount": warningCount,
|
// normLevel returns the canonical (upper-case) form of a Zonemaster severity
|
||||||
"totalChecks": len(data.Results),
|
// string. Use this anywhere a severity needs to be compared, looked up or
|
||||||
"hashId": data.HashID,
|
// keyed so the canonical list stays in one place.
|
||||||
"createdAt": data.CreatedAt,
|
func normLevel(level string) string {
|
||||||
}
|
return strings.ToUpper(level)
|
||||||
|
}
|
||||||
if errorCount > 0 {
|
|
||||||
statusLine := fmt.Sprintf("%d error(s), %d warning(s) found", errorCount, warningCount)
|
// levelToStatus maps a Zonemaster-returned severity to happyDomain's status.
|
||||||
if len(criticalMsgs) > 0 {
|
// Zonemaster's own judgement is treated as raw input; this is happyDomain's
|
||||||
n := 2
|
// own mapping onto the SDK status enum.
|
||||||
if len(criticalMsgs) < n {
|
func levelToStatus(level string) sdk.Status {
|
||||||
n = len(criticalMsgs)
|
switch normLevel(level) {
|
||||||
}
|
case "CRITICAL", "ERROR":
|
||||||
statusLine += ": " + strings.Join(criticalMsgs[:n], "; ")
|
return sdk.StatusCrit
|
||||||
}
|
case "WARNING":
|
||||||
return []sdk.CheckState{{
|
return sdk.StatusWarn
|
||||||
Status: sdk.StatusCrit,
|
case "NOTICE", "INFO", "DEBUG":
|
||||||
Message: statusLine,
|
return sdk.StatusInfo
|
||||||
Code: "zonemaster_errors",
|
default:
|
||||||
Meta: meta,
|
return sdk.StatusUnknown
|
||||||
}}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if warningCount > 0 {
|
// worstStatus returns the more severe of two statuses. StatusError always
|
||||||
return []sdk.CheckState{{
|
// wins because it means "we could not evaluate".
|
||||||
Status: sdk.StatusWarn,
|
func worstStatus(a, b sdk.Status) sdk.Status {
|
||||||
Message: fmt.Sprintf("%d warning(s) found", warningCount),
|
rank := func(s sdk.Status) int {
|
||||||
Code: "zonemaster_warnings",
|
switch s {
|
||||||
Meta: meta,
|
case sdk.StatusError:
|
||||||
}}
|
return 6
|
||||||
}
|
case sdk.StatusCrit:
|
||||||
|
return 5
|
||||||
return []sdk.CheckState{{
|
case sdk.StatusWarn:
|
||||||
Status: sdk.StatusOK,
|
return 4
|
||||||
Message: fmt.Sprintf("All checks passed (%d checks)", len(data.Results)),
|
case sdk.StatusInfo:
|
||||||
Code: "zonemaster_ok",
|
return 2
|
||||||
Meta: meta,
|
case sdk.StatusOK:
|
||||||
}}
|
return 1
|
||||||
|
default:
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if rank(a) >= rank(b) {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// categoryRule is the common shape used by every per-category Zonemaster
|
||||||
|
// rule: load the observation, filter messages whose module matches one of
|
||||||
|
// the declared names, map Zonemaster severities onto CheckState.Status,
|
||||||
|
// and emit a summary state plus one state per WARNING-or-worse message.
|
||||||
|
// INFO/NOTICE messages are folded into the summary counts so the state
|
||||||
|
// list stays readable.
|
||||||
|
type categoryRule struct {
|
||||||
|
name string
|
||||||
|
description string
|
||||||
|
modules []string // case-insensitive module names handled by this rule
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *categoryRule) Name() string { return r.name }
|
||||||
|
func (r *categoryRule) Description() string { return r.description }
|
||||||
|
|
||||||
|
func (r *categoryRule) ValidateOptions(opts sdk.CheckerOptions) error {
|
||||||
|
return validateZonemasterOptions(opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *categoryRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, _ sdk.CheckerOptions) []sdk.CheckState {
|
||||||
|
data, errSt := loadZonemasterData(ctx, obs)
|
||||||
|
if errSt != nil {
|
||||||
|
return []sdk.CheckState{*errSt}
|
||||||
|
}
|
||||||
|
|
||||||
|
matched := filterByModules(data.Results, r.modules)
|
||||||
|
if len(matched) == 0 {
|
||||||
|
return []sdk.CheckState{{
|
||||||
|
Status: sdk.StatusUnknown,
|
||||||
|
Message: fmt.Sprintf("No %s messages returned by Zonemaster for this zone.", r.name),
|
||||||
|
Code: r.name + ".not_tested",
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
critCount, errCount, warnCount, noticeCount, infoCount int
|
||||||
|
worst = sdk.StatusOK
|
||||||
|
issueStates []sdk.CheckState
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, res := range matched {
|
||||||
|
lvl := normLevel(res.Level)
|
||||||
|
st := levelToStatus(lvl)
|
||||||
|
worst = worstStatus(worst, st)
|
||||||
|
|
||||||
|
switch lvl {
|
||||||
|
case "CRITICAL":
|
||||||
|
critCount++
|
||||||
|
case "ERROR":
|
||||||
|
errCount++
|
||||||
|
case "WARNING":
|
||||||
|
warnCount++
|
||||||
|
case "NOTICE":
|
||||||
|
noticeCount++
|
||||||
|
default:
|
||||||
|
infoCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
if st == sdk.StatusCrit || st == sdk.StatusWarn {
|
||||||
|
issueStates = append(issueStates, sdk.CheckState{
|
||||||
|
Status: st,
|
||||||
|
Message: res.Message,
|
||||||
|
Code: r.name + "." + strings.ToLower(lvl),
|
||||||
|
Subject: res.Testcase,
|
||||||
|
Meta: map[string]any{
|
||||||
|
"module": res.Module,
|
||||||
|
"testcase": res.Testcase,
|
||||||
|
"level": lvl,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
summary := sdk.CheckState{
|
||||||
|
Status: worst,
|
||||||
|
Code: r.name + ".summary",
|
||||||
|
Meta: map[string]any{
|
||||||
|
"total": len(matched),
|
||||||
|
"critical": critCount,
|
||||||
|
"error": errCount,
|
||||||
|
"warning": warnCount,
|
||||||
|
"notice": noticeCount,
|
||||||
|
"info": infoCount,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case critCount+errCount > 0:
|
||||||
|
summary.Message = fmt.Sprintf("%d error(s), %d warning(s) reported by Zonemaster (%d checks).", critCount+errCount, warnCount, len(matched))
|
||||||
|
case warnCount > 0:
|
||||||
|
summary.Message = fmt.Sprintf("%d warning(s) reported by Zonemaster (%d checks).", warnCount, len(matched))
|
||||||
|
default:
|
||||||
|
summary.Status = sdk.StatusOK
|
||||||
|
summary.Message = fmt.Sprintf("No issues reported by Zonemaster (%d checks).", len(matched))
|
||||||
|
}
|
||||||
|
|
||||||
|
return append([]sdk.CheckState{summary}, issueStates...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// filterByModules returns the subset of results whose Module matches any of
|
||||||
|
// the given module names (case-insensitive).
|
||||||
|
func filterByModules(results []ZonemasterTestResult, modules []string) []ZonemasterTestResult {
|
||||||
|
if len(modules) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
set := make(map[string]struct{}, len(modules))
|
||||||
|
for _, m := range modules {
|
||||||
|
set[strings.ToLower(m)] = struct{}{}
|
||||||
|
}
|
||||||
|
var out []ZonemasterTestResult
|
||||||
|
for _, r := range results {
|
||||||
|
if _, ok := set[strings.ToLower(r.Module)]; ok {
|
||||||
|
out = append(out, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
}
|
}
|
||||||
|
|
|
||||||
298
checker/rule_test.go
Normal file
298
checker/rule_test.go
Normal file
|
|
@ -0,0 +1,298 @@
|
||||||
|
package checker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
sdk "git.happydns.org/checker-sdk-go/checker"
|
||||||
|
)
|
||||||
|
|
||||||
|
// fakeObs is a minimal ObservationGetter for tests. If err is non-nil, Get
|
||||||
|
// returns it; otherwise, it JSON-roundtrips data into dest.
|
||||||
|
type fakeObs struct {
|
||||||
|
data any
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeObs) Get(_ context.Context, _ sdk.ObservationKey, dest any) error {
|
||||||
|
if f.err != nil {
|
||||||
|
return f.err
|
||||||
|
}
|
||||||
|
b, err := json.Marshal(f.data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return json.Unmarshal(b, dest)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeObs) GetRelated(_ context.Context, _ sdk.ObservationKey) ([]sdk.RelatedObservation, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLevelToStatus(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
level string
|
||||||
|
want sdk.Status
|
||||||
|
}{
|
||||||
|
{"CRITICAL", sdk.StatusCrit},
|
||||||
|
{"ERROR", sdk.StatusCrit},
|
||||||
|
{"critical", sdk.StatusCrit}, // case-insensitive
|
||||||
|
{"WARNING", sdk.StatusWarn},
|
||||||
|
{"NOTICE", sdk.StatusInfo},
|
||||||
|
{"INFO", sdk.StatusInfo},
|
||||||
|
{"DEBUG", sdk.StatusInfo},
|
||||||
|
{"", sdk.StatusUnknown},
|
||||||
|
{"BANANA", sdk.StatusUnknown},
|
||||||
|
}
|
||||||
|
for _, tc := range cases {
|
||||||
|
t.Run(tc.level, func(t *testing.T) {
|
||||||
|
if got := levelToStatus(tc.level); got != tc.want {
|
||||||
|
t.Errorf("levelToStatus(%q) = %v, want %v", tc.level, got, tc.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWorstStatus(t *testing.T) {
|
||||||
|
// Severity ordering used by worstStatus:
|
||||||
|
// Error > Crit > Warn > Info > OK > Unknown
|
||||||
|
cases := []struct {
|
||||||
|
a, b, want sdk.Status
|
||||||
|
}{
|
||||||
|
{sdk.StatusOK, sdk.StatusOK, sdk.StatusOK},
|
||||||
|
{sdk.StatusOK, sdk.StatusInfo, sdk.StatusInfo},
|
||||||
|
{sdk.StatusInfo, sdk.StatusWarn, sdk.StatusWarn},
|
||||||
|
{sdk.StatusWarn, sdk.StatusCrit, sdk.StatusCrit},
|
||||||
|
{sdk.StatusCrit, sdk.StatusError, sdk.StatusError},
|
||||||
|
{sdk.StatusError, sdk.StatusCrit, sdk.StatusError},
|
||||||
|
{sdk.StatusUnknown, sdk.StatusOK, sdk.StatusOK},
|
||||||
|
{sdk.StatusUnknown, sdk.StatusUnknown, sdk.StatusUnknown},
|
||||||
|
}
|
||||||
|
for _, tc := range cases {
|
||||||
|
if got := worstStatus(tc.a, tc.b); got != tc.want {
|
||||||
|
t.Errorf("worstStatus(%v, %v) = %v, want %v", tc.a, tc.b, got, tc.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFilterByModules(t *testing.T) {
|
||||||
|
results := []ZonemasterTestResult{
|
||||||
|
{Module: "DNSSEC", Message: "a"},
|
||||||
|
{Module: "Delegation", Message: "b"},
|
||||||
|
{Module: "dnssec", Message: "c"},
|
||||||
|
{Module: "Syntax", Message: "d"},
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("matches case-insensitively", func(t *testing.T) {
|
||||||
|
got := filterByModules(results, []string{"dnssec"})
|
||||||
|
if len(got) != 2 {
|
||||||
|
t.Fatalf("got %d results, want 2: %+v", len(got), got)
|
||||||
|
}
|
||||||
|
if got[0].Message != "a" || got[1].Message != "c" {
|
||||||
|
t.Errorf("unexpected results: %+v", got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("multiple modules", func(t *testing.T) {
|
||||||
|
got := filterByModules(results, []string{"delegation", "syntax"})
|
||||||
|
if len(got) != 2 {
|
||||||
|
t.Errorf("got %d, want 2", len(got))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("empty modules returns nil", func(t *testing.T) {
|
||||||
|
if got := filterByModules(results, nil); got != nil {
|
||||||
|
t.Errorf("got %+v, want nil", got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("no match returns empty", func(t *testing.T) {
|
||||||
|
if got := filterByModules(results, []string{"nope"}); len(got) != 0 {
|
||||||
|
t.Errorf("got %+v, want empty", got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateZonemasterOptions(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
opts sdk.CheckerOptions
|
||||||
|
wantErr string // substring; empty means no error expected
|
||||||
|
}{
|
||||||
|
{"empty opts", sdk.CheckerOptions{}, ""},
|
||||||
|
{"empty url", sdk.CheckerOptions{"zonemasterAPIURL": ""}, ""},
|
||||||
|
{"valid http", sdk.CheckerOptions{"zonemasterAPIURL": "http://localhost:5000/api"}, ""},
|
||||||
|
{"valid https", sdk.CheckerOptions{"zonemasterAPIURL": "https://zonemaster.net/api"}, ""},
|
||||||
|
{"non-string", sdk.CheckerOptions{"zonemasterAPIURL": 42}, "must be a string"},
|
||||||
|
{"bad scheme", sdk.CheckerOptions{"zonemasterAPIURL": "ftp://x/api"}, "http or https"},
|
||||||
|
{"no host", sdk.CheckerOptions{"zonemasterAPIURL": "http:///api"}, "must include a host"},
|
||||||
|
{"unparseable", sdk.CheckerOptions{"zonemasterAPIURL": "http://[::1"}, "zonemasterAPIURL"},
|
||||||
|
}
|
||||||
|
for _, tc := range cases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
err := validateZonemasterOptions(tc.opts)
|
||||||
|
switch {
|
||||||
|
case tc.wantErr == "" && err != nil:
|
||||||
|
t.Errorf("unexpected error: %v", err)
|
||||||
|
case tc.wantErr != "" && err == nil:
|
||||||
|
t.Errorf("expected error containing %q, got nil", tc.wantErr)
|
||||||
|
case tc.wantErr != "" && !strings.Contains(err.Error(), tc.wantErr):
|
||||||
|
t.Errorf("error %q does not contain %q", err.Error(), tc.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormLevel(t *testing.T) {
|
||||||
|
cases := map[string]string{
|
||||||
|
"": "",
|
||||||
|
"info": "INFO",
|
||||||
|
"WaRnInG": "WARNING",
|
||||||
|
"CRITICAL": "CRITICAL",
|
||||||
|
}
|
||||||
|
for in, want := range cases {
|
||||||
|
if got := normLevel(in); got != want {
|
||||||
|
t.Errorf("normLevel(%q) = %q, want %q", in, got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCategoryRuleEvaluate_NoData(t *testing.T) {
|
||||||
|
r := &categoryRule{name: "zonemaster.dnssec", modules: []string{"dnssec"}}
|
||||||
|
obs := &fakeObs{data: ZonemasterData{Results: nil}}
|
||||||
|
|
||||||
|
states := r.Evaluate(context.Background(), obs, nil)
|
||||||
|
if len(states) != 1 {
|
||||||
|
t.Fatalf("got %d states, want 1", len(states))
|
||||||
|
}
|
||||||
|
if states[0].Status != sdk.StatusUnknown {
|
||||||
|
t.Errorf("status = %v, want StatusUnknown", states[0].Status)
|
||||||
|
}
|
||||||
|
if states[0].Code != "zonemaster.dnssec.not_tested" {
|
||||||
|
t.Errorf("code = %q", states[0].Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCategoryRuleEvaluate_ObservationError(t *testing.T) {
|
||||||
|
r := &categoryRule{name: "zonemaster.dnssec", modules: []string{"dnssec"}}
|
||||||
|
obs := &fakeObs{err: errors.New("boom")}
|
||||||
|
|
||||||
|
states := r.Evaluate(context.Background(), obs, nil)
|
||||||
|
if len(states) != 1 {
|
||||||
|
t.Fatalf("got %d states, want 1", len(states))
|
||||||
|
}
|
||||||
|
if states[0].Status != sdk.StatusError {
|
||||||
|
t.Errorf("status = %v, want StatusError", states[0].Status)
|
||||||
|
}
|
||||||
|
if states[0].Code != "zonemaster.observation_error" {
|
||||||
|
t.Errorf("code = %q", states[0].Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCategoryRuleEvaluate_AllOK(t *testing.T) {
|
||||||
|
r := &categoryRule{name: "zonemaster.dnssec", modules: []string{"dnssec"}}
|
||||||
|
obs := &fakeObs{data: ZonemasterData{Results: []ZonemasterTestResult{
|
||||||
|
{Module: "dnssec", Level: "INFO", Message: "ok1"},
|
||||||
|
{Module: "dnssec", Level: "NOTICE", Message: "ok2"},
|
||||||
|
{Module: "delegation", Level: "ERROR", Message: "ignored, wrong module"},
|
||||||
|
}}}
|
||||||
|
|
||||||
|
states := r.Evaluate(context.Background(), obs, nil)
|
||||||
|
if len(states) != 1 {
|
||||||
|
t.Fatalf("got %d states, want 1 (summary only): %+v", len(states), states)
|
||||||
|
}
|
||||||
|
if states[0].Status != sdk.StatusOK {
|
||||||
|
t.Errorf("status = %v, want StatusOK", states[0].Status)
|
||||||
|
}
|
||||||
|
if got, _ := states[0].Meta["total"].(int); got != 2 {
|
||||||
|
t.Errorf("total = %d, want 2", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCategoryRuleEvaluate_MixedSeverities(t *testing.T) {
|
||||||
|
r := &categoryRule{name: "zonemaster.dnssec", modules: []string{"dnssec"}}
|
||||||
|
obs := &fakeObs{data: ZonemasterData{Results: []ZonemasterTestResult{
|
||||||
|
{Module: "DNSSEC", Level: "INFO", Message: "i"},
|
||||||
|
{Module: "dnssec", Level: "WARNING", Message: "w", Testcase: "tc-w"},
|
||||||
|
{Module: "dnssec", Level: "ERROR", Message: "e", Testcase: "tc-e"},
|
||||||
|
{Module: "dnssec", Level: "CRITICAL", Message: "c", Testcase: "tc-c"},
|
||||||
|
}}}
|
||||||
|
|
||||||
|
states := r.Evaluate(context.Background(), obs, nil)
|
||||||
|
// Expect 1 summary + 3 issue states (warning + error + critical).
|
||||||
|
if len(states) != 4 {
|
||||||
|
t.Fatalf("got %d states, want 4: %+v", len(states), states)
|
||||||
|
}
|
||||||
|
|
||||||
|
summary := states[0]
|
||||||
|
if summary.Status != sdk.StatusCrit {
|
||||||
|
t.Errorf("summary status = %v, want StatusCrit", summary.Status)
|
||||||
|
}
|
||||||
|
if got, _ := summary.Meta["critical"].(int); got != 1 {
|
||||||
|
t.Errorf("critical = %d, want 1", got)
|
||||||
|
}
|
||||||
|
if got, _ := summary.Meta["error"].(int); got != 1 {
|
||||||
|
t.Errorf("error = %d, want 1", got)
|
||||||
|
}
|
||||||
|
if got, _ := summary.Meta["warning"].(int); got != 1 {
|
||||||
|
t.Errorf("warning = %d, want 1", got)
|
||||||
|
}
|
||||||
|
if got, _ := summary.Meta["info"].(int); got != 1 {
|
||||||
|
t.Errorf("info = %d, want 1", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Issue states: codes should be dotted, lowercased levels.
|
||||||
|
wantCodes := map[string]bool{
|
||||||
|
"zonemaster.dnssec.warning": false,
|
||||||
|
"zonemaster.dnssec.error": false,
|
||||||
|
"zonemaster.dnssec.critical": false,
|
||||||
|
}
|
||||||
|
for _, s := range states[1:] {
|
||||||
|
if _, ok := wantCodes[s.Code]; !ok {
|
||||||
|
t.Errorf("unexpected issue code: %q", s.Code)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
wantCodes[s.Code] = true
|
||||||
|
if s.Subject == "" {
|
||||||
|
t.Errorf("issue state %q missing Subject", s.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for code, seen := range wantCodes {
|
||||||
|
if !seen {
|
||||||
|
t.Errorf("missing issue state for %q", code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRulesContainsAllCategories(t *testing.T) {
|
||||||
|
got := Rules()
|
||||||
|
wantNames := []string{
|
||||||
|
"zonemaster.dnssec",
|
||||||
|
"zonemaster.delegation",
|
||||||
|
"zonemaster.consistency",
|
||||||
|
"zonemaster.connectivity",
|
||||||
|
"zonemaster.nameserver",
|
||||||
|
"zonemaster.syntax",
|
||||||
|
"zonemaster.zone",
|
||||||
|
"zonemaster.address",
|
||||||
|
"zonemaster.basic",
|
||||||
|
}
|
||||||
|
if len(got) != len(wantNames) {
|
||||||
|
t.Fatalf("Rules() returned %d rules, want %d", len(got), len(wantNames))
|
||||||
|
}
|
||||||
|
seen := map[string]bool{}
|
||||||
|
for _, r := range got {
|
||||||
|
seen[r.Name()] = true
|
||||||
|
if r.Description() == "" {
|
||||||
|
t.Errorf("rule %q has empty description", r.Name())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, n := range wantNames {
|
||||||
|
if !seen[n] {
|
||||||
|
t.Errorf("Rules() missing %q", n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
13
checker/rules_address.go
Normal file
13
checker/rules_address.go
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
package checker
|
||||||
|
|
||||||
|
import sdk "git.happydns.org/checker-sdk-go/checker"
|
||||||
|
|
||||||
|
// addressRule covers Zonemaster's address test module (IP addresses of
|
||||||
|
// nameservers, private/reserved ranges, IPv6 coverage).
|
||||||
|
func addressRule() sdk.CheckRule {
|
||||||
|
return &categoryRule{
|
||||||
|
name: "zonemaster.address",
|
||||||
|
description: "Zonemaster address tests (IP addresses of nameservers, private/reserved ranges).",
|
||||||
|
modules: []string{"address"},
|
||||||
|
}
|
||||||
|
}
|
||||||
14
checker/rules_basic.go
Normal file
14
checker/rules_basic.go
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
package checker
|
||||||
|
|
||||||
|
import sdk "git.happydns.org/checker-sdk-go/checker"
|
||||||
|
|
||||||
|
// basicRule covers Zonemaster's basic/system test modules (initial
|
||||||
|
// reachability and fundamental pre-conditions for running the other test
|
||||||
|
// categories).
|
||||||
|
func basicRule() sdk.CheckRule {
|
||||||
|
return &categoryRule{
|
||||||
|
name: "zonemaster.basic",
|
||||||
|
description: "Zonemaster basic tests (initial reachability and fundamental requirements).",
|
||||||
|
modules: []string{"basic", "system"},
|
||||||
|
}
|
||||||
|
}
|
||||||
13
checker/rules_connectivity.go
Normal file
13
checker/rules_connectivity.go
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
package checker
|
||||||
|
|
||||||
|
import sdk "git.happydns.org/checker-sdk-go/checker"
|
||||||
|
|
||||||
|
// connectivityRule covers Zonemaster's connectivity test module (UDP/TCP
|
||||||
|
// reachability of authoritative servers, AS diversity).
|
||||||
|
func connectivityRule() sdk.CheckRule {
|
||||||
|
return &categoryRule{
|
||||||
|
name: "zonemaster.connectivity",
|
||||||
|
description: "Zonemaster connectivity tests (reachability of authoritative servers over UDP/TCP, AS diversity).",
|
||||||
|
modules: []string{"connectivity"},
|
||||||
|
}
|
||||||
|
}
|
||||||
13
checker/rules_consistency.go
Normal file
13
checker/rules_consistency.go
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
package checker
|
||||||
|
|
||||||
|
import sdk "git.happydns.org/checker-sdk-go/checker"
|
||||||
|
|
||||||
|
// consistencyRule covers Zonemaster's consistency test module (SOA serial,
|
||||||
|
// NS set, zone content identical across authoritative servers).
|
||||||
|
func consistencyRule() sdk.CheckRule {
|
||||||
|
return &categoryRule{
|
||||||
|
name: "zonemaster.consistency",
|
||||||
|
description: "Zonemaster consistency tests (SOA serial, NS set, zone content across servers).",
|
||||||
|
modules: []string{"consistency"},
|
||||||
|
}
|
||||||
|
}
|
||||||
13
checker/rules_delegation.go
Normal file
13
checker/rules_delegation.go
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
package checker
|
||||||
|
|
||||||
|
import sdk "git.happydns.org/checker-sdk-go/checker"
|
||||||
|
|
||||||
|
// delegationRule covers Zonemaster's delegation test module (parent/child NS
|
||||||
|
// agreement, glue correctness, referral integrity).
|
||||||
|
func delegationRule() sdk.CheckRule {
|
||||||
|
return &categoryRule{
|
||||||
|
name: "zonemaster.delegation",
|
||||||
|
description: "Zonemaster delegation tests (parent/child NS agreement, glue, referrals).",
|
||||||
|
modules: []string{"delegation"},
|
||||||
|
}
|
||||||
|
}
|
||||||
13
checker/rules_dnssec.go
Normal file
13
checker/rules_dnssec.go
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
package checker
|
||||||
|
|
||||||
|
import sdk "git.happydns.org/checker-sdk-go/checker"
|
||||||
|
|
||||||
|
// dnssecRule covers Zonemaster's DNSSEC test module (signatures, NSEC/NSEC3,
|
||||||
|
// DS/DNSKEY coherence, algorithm posture).
|
||||||
|
func dnssecRule() sdk.CheckRule {
|
||||||
|
return &categoryRule{
|
||||||
|
name: "zonemaster.dnssec",
|
||||||
|
description: "Zonemaster DNSSEC tests (signatures, NSEC/NSEC3, DS/DNSKEY coherence).",
|
||||||
|
modules: []string{"dnssec"},
|
||||||
|
}
|
||||||
|
}
|
||||||
13
checker/rules_nameserver.go
Normal file
13
checker/rules_nameserver.go
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
package checker
|
||||||
|
|
||||||
|
import sdk "git.happydns.org/checker-sdk-go/checker"
|
||||||
|
|
||||||
|
// nameserverRule covers Zonemaster's nameserver test module (server
|
||||||
|
// behaviour, EDNS posture, unknown RR handling).
|
||||||
|
func nameserverRule() sdk.CheckRule {
|
||||||
|
return &categoryRule{
|
||||||
|
name: "zonemaster.nameserver",
|
||||||
|
description: "Zonemaster nameserver tests (server behaviour, EDNS, unknown RR handling).",
|
||||||
|
modules: []string{"nameserver"},
|
||||||
|
}
|
||||||
|
}
|
||||||
13
checker/rules_syntax.go
Normal file
13
checker/rules_syntax.go
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
package checker
|
||||||
|
|
||||||
|
import sdk "git.happydns.org/checker-sdk-go/checker"
|
||||||
|
|
||||||
|
// syntaxRule covers Zonemaster's syntax test module (domain name syntax,
|
||||||
|
// hostname legality).
|
||||||
|
func syntaxRule() sdk.CheckRule {
|
||||||
|
return &categoryRule{
|
||||||
|
name: "zonemaster.syntax",
|
||||||
|
description: "Zonemaster syntax tests (domain name syntax, hostname legality).",
|
||||||
|
modules: []string{"syntax"},
|
||||||
|
}
|
||||||
|
}
|
||||||
13
checker/rules_zone.go
Normal file
13
checker/rules_zone.go
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
package checker
|
||||||
|
|
||||||
|
import sdk "git.happydns.org/checker-sdk-go/checker"
|
||||||
|
|
||||||
|
// zoneRule covers Zonemaster's zone test module (SOA values, MX presence,
|
||||||
|
// mandatory records at the apex).
|
||||||
|
func zoneRule() sdk.CheckRule {
|
||||||
|
return &categoryRule{
|
||||||
|
name: "zonemaster.zone",
|
||||||
|
description: "Zonemaster zone tests (SOA values, MX presence, mandatory records).",
|
||||||
|
modules: []string{"zone"},
|
||||||
|
}
|
||||||
|
}
|
||||||
2
go.mod
2
go.mod
|
|
@ -2,4 +2,4 @@ module git.happydns.org/checker-zonemaster
|
||||||
|
|
||||||
go 1.25.0
|
go 1.25.0
|
||||||
|
|
||||||
require git.happydns.org/checker-sdk-go v1.2.0
|
require git.happydns.org/checker-sdk-go v1.5.0
|
||||||
|
|
|
||||||
4
go.sum
4
go.sum
|
|
@ -1,2 +1,2 @@
|
||||||
git.happydns.org/checker-sdk-go v1.2.0 h1:v4MpKAz0W3PwP+bxx3pya8w893sVH5xTD1of1cc0TV8=
|
git.happydns.org/checker-sdk-go v1.5.0 h1:5uD5Cm6xJ+lwnhbJ09iCXGHbYS9zRh+Yh0NeBHkAPBY=
|
||||||
git.happydns.org/checker-sdk-go v1.2.0/go.mod h1:aNAcfYFfbhvH9kJhE0Njp5GX0dQbxdRB0rJ0KvSC5nI=
|
git.happydns.org/checker-sdk-go v1.5.0/go.mod h1:aNAcfYFfbhvH9kJhE0Njp5GX0dQbxdRB0rJ0KvSC5nI=
|
||||||
|
|
|
||||||
6
main.go
6
main.go
|
|
@ -4,8 +4,8 @@ import (
|
||||||
"flag"
|
"flag"
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
|
"git.happydns.org/checker-sdk-go/checker/server"
|
||||||
zonemaster "git.happydns.org/checker-zonemaster/checker"
|
zonemaster "git.happydns.org/checker-zonemaster/checker"
|
||||||
sdk "git.happydns.org/checker-sdk-go/checker"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Version is the standalone binary's version. It defaults to "custom-build"
|
// Version is the standalone binary's version. It defaults to "custom-build"
|
||||||
|
|
@ -23,8 +23,8 @@ func main() {
|
||||||
// CheckerDefinition.Version.
|
// CheckerDefinition.Version.
|
||||||
zonemaster.Version = Version
|
zonemaster.Version = Version
|
||||||
|
|
||||||
server := sdk.NewServer(zonemaster.Provider())
|
srv := server.New(zonemaster.Provider())
|
||||||
if err := server.ListenAndServe(*listenAddr); err != nil {
|
if err := srv.ListenAndServe(*listenAddr); err != nil {
|
||||||
log.Fatalf("server error: %v", err)
|
log.Fatalf("server error: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,8 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
zonemaster "git.happydns.org/checker-zonemaster/checker"
|
|
||||||
sdk "git.happydns.org/checker-sdk-go/checker"
|
sdk "git.happydns.org/checker-sdk-go/checker"
|
||||||
|
zonemaster "git.happydns.org/checker-zonemaster/checker"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Version is the plugin's version. It defaults to "custom-build" and is
|
// Version is the plugin's version. It defaults to "custom-build" and is
|
||||||
|
|
@ -20,5 +20,6 @@ var Version = "custom-build"
|
||||||
// that the host will register in its global registries.
|
// that the host will register in its global registries.
|
||||||
func NewCheckerPlugin() (*sdk.CheckerDefinition, sdk.ObservationProvider, error) {
|
func NewCheckerPlugin() (*sdk.CheckerDefinition, sdk.ObservationProvider, error) {
|
||||||
zonemaster.Version = Version
|
zonemaster.Version = Version
|
||||||
return zonemaster.Definition(), zonemaster.Provider(), nil
|
prvd := zonemaster.Provider()
|
||||||
|
return prvd.(sdk.CheckerDefinitionProvider).Definition(), prvd, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue