-
Notifications
You must be signed in to change notification settings - Fork 2
/
check.go
264 lines (220 loc) · 6.76 KB
/
check.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
package main
import (
"bytes"
"context"
"errors"
"fmt"
"os"
"strings"
"time"
"github.com/masterzen/winrm"
log "github.com/sirupsen/logrus"
"github.com/spf13/pflag"
"golang.org/x/crypto/ssh"
)
const (
Port = 5985
TlsPort = 5986
AuthDefault = AuthNTLM
AuthBasic = "basic"
AuthNTLM = "ntlm"
AuthSSH = "ssh"
AuthTLS = "tls"
)
type Config struct {
Host string
Port int
User string
Password string
NoTls bool
Insecure bool
TlsCAPath string
tlsCA []byte
TlsCertPath string
tlsCert []byte
TlsKeyPath string
tlsKey []byte
Command string
IcingaCommand string
AuthType string
SSHHost string
SSHUser string
SSHPassword string
validated bool
}
func BuildConfigFlags(fs *pflag.FlagSet) (config *Config) {
config = &Config{}
fs.StringVarP(&config.Host, "host", "H", "127.0.0.1",
"Host name, IP Address of the remote host")
fs.IntVarP(&config.Port, "port", "p", 0, "Port number WinRM") // TODO: document default
fs.StringVarP(&config.User, "user", "U", "", "Username of the remote host")
fs.StringVarP(&config.Password, "password", "P", "", "Password of the user")
fs.BoolVarP(&config.Insecure, "insecure", "k", false,
"Don't verify the hostname on the returned certificate")
fs.BoolVar(&config.NoTls, "no-tls", false, "Don't use a TLS connection, use the HTTP protocol")
fs.StringVar(&config.TlsCAPath, "ca", "", "CA certificate")
fs.StringVar(&config.TlsCertPath, "cert", "", "Client certificate")
fs.StringVar(&config.TlsKeyPath, "key", "", "Client Key")
fs.StringVar(&config.Command, "cmd", "", "Command to execute on the remote machine")
fs.StringVar(&config.IcingaCommand, "icingacmd", "",
"Executes commands of Icinga PowerShell Framework (e.g. Invoke-IcingaCheckCPU)")
fs.StringVar(&config.AuthType, "auth", AuthDefault, "Authentication mechanism - Basic, NTLM, TLS, SSH")
// AuthSSH
fs.StringVar(&config.SSHHost, "sshhost", "", "SSH Host (mandatory if --auth=SSH)")
fs.StringVar(&config.SSHUser, "sshuser", "", "SSH Username (mandatory if --auth=SSH)")
fs.StringVar(&config.SSHPassword, "sshpassword", "", "SSH Password (mandatory if --auth=SSH)")
// Compat flags
// TODO: remove?
fs.BoolVarP(&config.Insecure, "unsecure", "u", false,
"Don't verify the hostname on the returned certificate")
_ = fs.MarkHidden("unsecure")
return
}
// Validate ensures the configuration is valid, and will set and evaluate some settings
func (c *Config) Validate() (err error) {
c.validated = false
if c.Host == "" {
return errors.New("host must be configured")
}
// Any commands?
if c.Command == "" && c.IcingaCommand == "" {
return errors.New("no command specified")
} else if c.Command != "" && c.IcingaCommand != "" {
return errors.New("you can only use command OR icingacommand")
}
// Set default port if unset
if c.Port < 1 {
c.Port = TlsPort
if c.NoTls {
c.Port = Port
}
}
if c.TlsCertPath != "" {
c.tlsCert, err = os.ReadFile(c.TlsCertPath)
if err != nil {
return fmt.Errorf("could not read certificate: %w", err)
}
if c.TlsKeyPath == "" {
return errors.New("please specify certificate key when tls is enabled")
}
c.tlsKey, err = os.ReadFile(c.TlsKeyPath)
if err != nil {
return fmt.Errorf("could not read certificate key: %w", err)
}
if c.AuthType == "" {
c.AuthType = AuthTLS
} else {
log.Warnf("auth type is %s, but TLS certificates are supplied", c.AuthType)
}
}
if c.TlsCAPath != "" {
c.tlsCA, err = os.ReadFile(c.TlsCAPath)
if err != nil {
return fmt.Errorf("could not read CA file: %w", err)
}
}
// AuthType
if c.AuthType == "" {
c.AuthType = AuthDefault
}
auth := strings.ToLower(c.AuthType)
switch auth {
case AuthBasic, AuthNTLM:
if c.User == "" || c.Password == "" {
return errors.New("user and password must be configured")
}
case AuthTLS:
case AuthSSH:
if c.SSHHost == "" || c.SSHUser == "" || c.SSHPassword == "" {
return fmt.Errorf("please specify host, user and port for auth type: %s", c.AuthType)
}
default:
return fmt.Errorf("invalid auth type specified: %s", c.AuthType)
}
// store the lower case variant for later
c.AuthType = auth
// Validation complete
c.validated = true
return nil
}
func (c *Config) BuildCommand() (cmd string) {
var wrap string
if c.IcingaCommand != "" {
wrap = "try { Use-Icinga; exit (%s) } catch { Write-Host ('UNKNOWN: ' + $error); exit 3 }"
cmd = fmt.Sprintf(wrap, c.IcingaCommand)
} else {
wrap = "try { %s; exit $LASTEXITCODE } catch { Write-Host ('UNKNOWN: ' + $error); exit 3 }"
cmd = fmt.Sprintf(wrap, c.Command)
}
log.WithField("cmd", cmd).Debug("prepared pwsh for execution")
cmd = winrm.Powershell(cmd)
log.WithField("cmd", cmd).Debug("prepared winrm command for execution")
return
}
func (c *Config) Run(timeout time.Duration) (err error, rc int, output string) {
if !c.validated {
panic("you need to call Validate() before Run()")
}
log.WithField("config", *c).Debug("Running check with config")
endpoint := winrm.NewEndpoint(
c.Host, // Host to connect to
c.Port, // Winrm port
!c.NoTls, // Use TLS
c.Insecure, // Allow insecure connection
c.tlsCA, // CA certificate
c.tlsCert, // Client Certificate
c.tlsKey, // Client Key
timeout, // Timeout
)
params := winrm.DefaultParameters
// prepare auth parameters
switch c.AuthType {
case AuthNTLM:
params.TransportDecorator = func() winrm.Transporter {
return &winrm.ClientNTLM{}
}
case AuthTLS:
params.TransportDecorator = func() winrm.Transporter {
return &winrm.ClientAuthRequest{}
}
case AuthSSH:
// TODO: port configuration?
var sshClient *ssh.Client
sshClient, err = ssh.Dial("tcp", c.SSHHost+":22", &ssh.ClientConfig{
User: c.SSHUser,
Auth: []ssh.AuthMethod{ssh.Password(c.SSHPassword)},
HostKeyCallback: ssh.InsecureIgnoreHostKey(), //nolint:gosec // TODO: really?
})
if err != nil {
err = fmt.Errorf("could not connect via SSH: %w", err)
return
}
params.Dial = sshClient.Dial
default: // default is AuthBasic
params.TransportDecorator = nil
}
// prepare client
client, err := winrm.NewClientWithParameters(endpoint, c.User, c.Password, params)
if err != nil {
err = fmt.Errorf("could not create client: %w", err)
return
}
// execute the check remotely
var (
stdout = &bytes.Buffer{}
stderr = &bytes.Buffer{}
)
ctx := context.Background()
rc, err = client.RunWithContext(ctx, c.BuildCommand(), stdout, stderr)
if err != nil {
err = fmt.Errorf("execution of remote cmd failed: %w", err)
return
}
output = stdout.String()
// Info the debug output can confuse testing
if log.GetLevel() >= log.DebugLevel && stderr.Len() > 0 {
output += fmt.Sprintln("stderr contained:")
output += fmt.Sprintln(stderr.String())
}
return
}