// 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 . // // 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 . 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, } }