checker-ssh/checker/rules.go

137 lines
3.6 KiB
Go

// 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 checker
import (
"context"
"fmt"
sdk "git.happydns.org/checker-sdk-go/checker"
)
// Each concern is its own rule so results surface independently in the UI
// rather than being squashed under a single aggregated verdict.
func Rules() []sdk.CheckRule {
return []sdk.CheckRule{
&reachabilityRule{},
&handshakeRule{},
&protocolVersionRule{},
&bannerSoftwareRule{},
&knownVulnsRule{},
newKexAlgorithmsRule(),
newHostKeyAlgorithmsRule(),
newCipherAlgorithmsRule(),
newMacAlgorithmsRule(),
&strictKexRule{},
&preauthCompressionRule{},
&hostKeyStrengthRule{},
&sshfpAlignmentRule{},
&sshfpHashRule{},
&authMethodsRule{},
}
}
// On failure, returns a single error state the caller should emit to short-circuit its rule.
func loadSSHData(ctx context.Context, obs sdk.ObservationGetter) (*SSHData, *sdk.CheckState) {
var data SSHData
if err := obs.Get(ctx, ObservationKeySSH, &data); err != nil {
return nil, &sdk.CheckState{
Status: sdk.StatusError,
Message: fmt.Sprintf("failed to load SSH observation: %v", err),
Code: "ssh.observation_error",
}
}
return &data, nil
}
// reachableEndpoints returns the subset of endpoints that completed
// enough of the handshake to expose algorithm data.
func reachableEndpoints(eps []SSHProbe) []SSHProbe {
var out []SSHProbe
for _, ep := range eps {
if len(ep.KEX) > 0 {
out = append(out, ep)
}
}
return out
}
// severityToStatus maps an Issue severity to the SDK Status enum.
func severityToStatus(sev string) sdk.Status {
switch sev {
case SeverityCrit:
return sdk.StatusCrit
case SeverityWarn:
return sdk.StatusWarn
case SeverityInfo:
return sdk.StatusInfo
default:
return sdk.StatusOK
}
}
func issueToState(is Issue) sdk.CheckState {
st := sdk.CheckState{
Status: severityToStatus(is.Severity),
Message: is.Message,
Code: is.Code,
Subject: is.Endpoint,
}
if is.Fix != "" {
st.Meta = map[string]any{"fix": is.Fix}
}
return st
}
func statesFromIssues(issues []Issue) []sdk.CheckState {
out := make([]sdk.CheckState, 0, len(issues))
for _, is := range issues {
out = append(out, issueToState(is))
}
return out
}
func passState(code, message string) sdk.CheckState {
return sdk.CheckState{
Status: sdk.StatusOK,
Message: message,
Code: code,
}
}
func notTestedState(code, message string) sdk.CheckState {
return sdk.CheckState{
Status: sdk.StatusUnknown,
Message: message,
Code: code,
}
}
// noEndpointsState is returned by rules that need probe output but got
// nothing (no endpoints collected at all).
func noEndpointsState(code string) sdk.CheckState {
return sdk.CheckState{
Status: sdk.StatusUnknown,
Message: "No SSH endpoints were probed.",
Code: code,
}
}