New checker: domain lock status
Some checks are pending
continuous-integration/drone/push Build is pending
Some checks are pending
continuous-integration/drone/push Build is pending
This commit is contained in:
parent
54cf661481
commit
e9b20aa958
3 changed files with 311 additions and 0 deletions
|
|
@ -47,6 +47,7 @@ type WHOISData struct {
|
|||
ExpiryDate time.Time `json:"expiryDate"`
|
||||
Registrar string `json:"registrar"`
|
||||
Contacts map[string]*happydns.ContactInfo `json:"contacts,omitempty"`
|
||||
Status []string `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
// whoisProvider is a placeholder WHOIS observation provider.
|
||||
|
|
@ -80,6 +81,7 @@ func (p *whoisProvider) Collect(ctx context.Context, opts happydns.CheckerOption
|
|||
ExpiryDate: *info.ExpirationDate,
|
||||
Registrar: registrar,
|
||||
Contacts: info.Contacts,
|
||||
Status: info.Status,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
|
|||
159
checkers/domain_lock.go
Normal file
159
checkers/domain_lock.go
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
// This file is part of the happyDomain (R) project.
|
||||
// Copyright (c) 2020-2026 happyDomain
|
||||
// Authors: Pierre-Olivier Mercier, et al.
|
||||
//
|
||||
// This program is offered under a commercial and under the AGPL license.
|
||||
// For commercial licensing, contact us at <contact@happydomain.org>.
|
||||
//
|
||||
// For AGPL licensing:
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package checkers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.happydns.org/happyDomain/internal/checker"
|
||||
"git.happydns.org/happyDomain/model"
|
||||
)
|
||||
|
||||
const defaultRequiredLockStatuses = "clientTransferProhibited"
|
||||
|
||||
// domainLockRule verifies that a domain carries the expected EPP lock
|
||||
// statuses (e.g. clientTransferProhibited) as reported by RDAP/WHOIS.
|
||||
type domainLockRule struct{}
|
||||
|
||||
func (r *domainLockRule) Name() string {
|
||||
return "domain_lock_check"
|
||||
}
|
||||
|
||||
func (r *domainLockRule) Description() string {
|
||||
return "Verifies that a domain carries the expected EPP lock statuses (e.g. clientTransferProhibited)"
|
||||
}
|
||||
|
||||
func (r *domainLockRule) ValidateOptions(opts happydns.CheckerOptions) error {
|
||||
v, ok := opts["requiredStatuses"]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
s, ok := v.(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("requiredStatuses must be a string")
|
||||
}
|
||||
for _, p := range strings.Split(s, ",") {
|
||||
if strings.TrimSpace(p) != "" {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("requiredStatuses must contain at least one EPP status code")
|
||||
}
|
||||
|
||||
func (r *domainLockRule) Evaluate(ctx context.Context, obs happydns.ObservationGetter, opts happydns.CheckerOptions) happydns.CheckState {
|
||||
var whois WHOISData
|
||||
if err := obs.Get(ctx, ObservationKeyWhois, &whois); err != nil {
|
||||
return happydns.CheckState{
|
||||
Status: happydns.StatusError,
|
||||
Message: fmt.Sprintf("Failed to get WHOIS data: %v", err),
|
||||
Code: "lock_error",
|
||||
}
|
||||
}
|
||||
|
||||
requiredStr := defaultRequiredLockStatuses
|
||||
if v, ok := opts["requiredStatuses"].(string); ok && v != "" {
|
||||
requiredStr = v
|
||||
}
|
||||
|
||||
var required []string
|
||||
for _, s := range strings.Split(requiredStr, ",") {
|
||||
s = strings.TrimSpace(s)
|
||||
if s != "" {
|
||||
required = append(required, s)
|
||||
}
|
||||
}
|
||||
|
||||
if len(required) == 0 {
|
||||
return happydns.CheckState{
|
||||
Status: happydns.StatusOK,
|
||||
Message: "No required lock statuses configured",
|
||||
Code: "lock_skipped",
|
||||
}
|
||||
}
|
||||
|
||||
present := make(map[string]bool, len(whois.Status))
|
||||
for _, s := range whois.Status {
|
||||
present[strings.ToLower(s)] = true
|
||||
}
|
||||
|
||||
var missing []string
|
||||
for _, req := range required {
|
||||
if !present[strings.ToLower(req)] {
|
||||
missing = append(missing, req)
|
||||
}
|
||||
}
|
||||
|
||||
if len(missing) > 0 {
|
||||
return happydns.CheckState{
|
||||
Status: happydns.StatusCrit,
|
||||
Message: fmt.Sprintf("Missing lock status: %s", strings.Join(missing, ", ")),
|
||||
Code: "lock_missing",
|
||||
Meta: map[string]any{
|
||||
"missing": missing,
|
||||
"present": whois.Status,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return happydns.CheckState{
|
||||
Status: happydns.StatusOK,
|
||||
Message: fmt.Sprintf("All required statuses present: %s", strings.Join(required, ", ")),
|
||||
Code: "lock_ok",
|
||||
Meta: map[string]any{
|
||||
"required": required,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
checker.RegisterChecker(&happydns.CheckerDefinition{
|
||||
ID: "domain_lock",
|
||||
Name: "Domain Lock Status",
|
||||
Availability: happydns.CheckerAvailability{
|
||||
ApplyToDomain: true,
|
||||
},
|
||||
ObservationKeys: []happydns.ObservationKey{ObservationKeyWhois},
|
||||
Options: happydns.CheckerOptionsDocumentation{
|
||||
DomainOpts: []happydns.CheckerOptionDocumentation{
|
||||
{
|
||||
Id: "requiredStatuses",
|
||||
Type: "string",
|
||||
Label: "Required lock statuses",
|
||||
Description: "Comma-separated list of EPP status codes that must be present on the domain (e.g. clientTransferProhibited, clientUpdateProhibited, clientDeleteProhibited).",
|
||||
Default: defaultRequiredLockStatuses,
|
||||
Placeholder: defaultRequiredLockStatuses,
|
||||
},
|
||||
},
|
||||
},
|
||||
Rules: []happydns.CheckRule{
|
||||
&domainLockRule{},
|
||||
},
|
||||
Interval: &happydns.CheckIntervalSpec{
|
||||
Min: 1 * time.Hour,
|
||||
Max: 7 * 24 * time.Hour,
|
||||
Default: 24 * time.Hour,
|
||||
},
|
||||
})
|
||||
}
|
||||
150
checkers/domain_lock_test.go
Normal file
150
checkers/domain_lock_test.go
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
// This file is part of the happyDomain (R) project.
|
||||
// Copyright (c) 2020-2026 happyDomain
|
||||
// Authors: Pierre-Olivier Mercier, et al.
|
||||
//
|
||||
// This program is offered under a commercial and under the AGPL license.
|
||||
// For commercial licensing, contact us at <contact@happydomain.org>.
|
||||
//
|
||||
// For AGPL licensing:
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package checkers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"git.happydns.org/happyDomain/model"
|
||||
)
|
||||
|
||||
func TestDomainLockRule_Evaluate(t *testing.T) {
|
||||
rule := &domainLockRule{}
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
status []string
|
||||
opts happydns.CheckerOptions
|
||||
want happydns.Status
|
||||
code string
|
||||
}{
|
||||
{
|
||||
name: "default required present",
|
||||
status: []string{"clientTransferProhibited", "ok"},
|
||||
opts: nil,
|
||||
want: happydns.StatusOK,
|
||||
code: "lock_ok",
|
||||
},
|
||||
{
|
||||
name: "default required missing",
|
||||
status: []string{"ok"},
|
||||
opts: nil,
|
||||
want: happydns.StatusCrit,
|
||||
code: "lock_missing",
|
||||
},
|
||||
{
|
||||
name: "multiple required all present",
|
||||
status: []string{"clientTransferProhibited", "clientUpdateProhibited", "clientDeleteProhibited"},
|
||||
opts: happydns.CheckerOptions{
|
||||
"requiredStatuses": "clientTransferProhibited,clientUpdateProhibited,clientDeleteProhibited",
|
||||
},
|
||||
want: happydns.StatusOK,
|
||||
code: "lock_ok",
|
||||
},
|
||||
{
|
||||
name: "multiple required some missing",
|
||||
status: []string{"clientTransferProhibited"},
|
||||
opts: happydns.CheckerOptions{
|
||||
"requiredStatuses": "clientTransferProhibited,clientUpdateProhibited",
|
||||
},
|
||||
want: happydns.StatusCrit,
|
||||
code: "lock_missing",
|
||||
},
|
||||
{
|
||||
name: "case insensitive match",
|
||||
status: []string{"clienttransferprohibited"},
|
||||
opts: nil,
|
||||
want: happydns.StatusOK,
|
||||
code: "lock_ok",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
obs := newWhoisObs(&WHOISData{Status: tc.status})
|
||||
st := rule.Evaluate(context.Background(), obs, tc.opts)
|
||||
if st.Status != tc.want {
|
||||
t.Errorf("status = %v, want %v (msg=%q)", st.Status, tc.want, st.Message)
|
||||
}
|
||||
if st.Code != tc.code {
|
||||
t.Errorf("code = %q, want %q", st.Code, tc.code)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Sanity: WHOIS data with nil/empty status must report missing locks, not panic.
|
||||
func TestDomainLockRule_NilStatus(t *testing.T) {
|
||||
rule := &domainLockRule{}
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
status []string
|
||||
}{
|
||||
{"nil status", nil},
|
||||
{"empty status", []string{}},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
obs := newWhoisObs(&WHOISData{Status: tc.status})
|
||||
st := rule.Evaluate(context.Background(), obs, nil)
|
||||
if st.Status != happydns.StatusCrit {
|
||||
t.Errorf("status = %v, want Crit", st.Status)
|
||||
}
|
||||
if st.Code != "lock_missing" {
|
||||
t.Errorf("code = %q, want lock_missing", st.Code)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDomainLockRule_EvaluateObservationError(t *testing.T) {
|
||||
rule := &domainLockRule{}
|
||||
obs := &stubObservationGetter{key: ObservationKeyWhois, err: errString("nope")}
|
||||
st := rule.Evaluate(context.Background(), obs, nil)
|
||||
if st.Status != happydns.StatusError || st.Code != "lock_error" {
|
||||
t.Errorf("got %v / %q", st.Status, st.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDomainLockRule_ValidateOptions(t *testing.T) {
|
||||
rule := &domainLockRule{}
|
||||
cases := []struct {
|
||||
name string
|
||||
opts happydns.CheckerOptions
|
||||
wantErr bool
|
||||
}{
|
||||
{"default", nil, false},
|
||||
{"valid", happydns.CheckerOptions{"requiredStatuses": "clientTransferProhibited"}, false},
|
||||
{"empty after split", happydns.CheckerOptions{"requiredStatuses": " , , "}, true},
|
||||
{"wrong type", happydns.CheckerOptions{"requiredStatuses": 123}, true},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
err := rule.ValidateOptions(tc.opts)
|
||||
if (err != nil) != tc.wantErr {
|
||||
t.Errorf("err=%v wantErr=%v", err, tc.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue