-
Notifications
You must be signed in to change notification settings - Fork 34
Expand file tree
/
Copy pathhostname.go
More file actions
157 lines (134 loc) · 5.11 KB
/
hostname.go
File metadata and controls
157 lines (134 loc) · 5.11 KB
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
// Copyright (c) Mondoo, Inc.
// SPDX-License-Identifier: BUSL-1.1
package hostname
import (
"fmt"
"io"
"strings"
"github.com/rs/zerolog/log"
"github.com/spf13/afero"
"go.mondoo.com/cnquery/v11/providers-sdk/v1/inventory"
"go.mondoo.com/cnquery/v11/providers/os/connection/shared"
"go.mondoo.com/cnquery/v11/providers/os/registry"
)
// Hostname returns the hostname of the system.
// On Linux systems we prefer `hostname -f` over `/etc/hostname` since systemd is not updating the value all the time.
// On Windows the `hostname` command (without the -f flag) works more reliable than `powershell -c "$env:computername"`
// since it will return a non-zero exit code.
func Hostname(conn shared.Connection, pf *inventory.Platform) (string, bool) {
if !pf.IsFamily(inventory.FAMILY_UNIX) && !pf.IsFamily(inventory.FAMILY_WINDOWS) {
log.Warn().Msg("your platform is not supported for hostname detection")
return "", false
}
// On unix systems we try to get the hostname via `hostname -f` first since it returns the fqdn.
if pf.IsFamily(inventory.FAMILY_UNIX) {
fqdn, err := runCommand(conn, "hostname -f")
if err == nil && fqdn != "localhost" && fqdn != "" {
return fqdn, true
}
log.Debug().Err(err).Msg("could not detect hostname via `hostname -f` command")
// If the output of `hostname -f` is localhost, we try to fetch it via `getent hosts`,
// start with the most common protocol IPv4.
hostname, err := parseGetentHosts(conn, "127.0.0.1")
if err == nil && hostname != "" {
return hostname, true
}
log.Debug().Err(err).Str("ipversion", "IPv4").Msg("could not detect hostname")
// When IPv4 is not configured, try IPv6.
hostname, err = parseGetentHosts(conn, "::1")
if err == nil && hostname != "" {
return hostname, true
}
log.Debug().Err(err).Str("ipversion", "IPv6").Msg("could not detect hostname")
}
// This is the preferred way to get the hostname on windows, it is important to not use the -f flag here
hostname, err := runCommand(conn, "hostname")
if err == nil && hostname != "" {
return hostname, true
}
log.Debug().Err(err).Msg("could not run `hostname` command")
// Fallback to for unix systems to /etc/hostname, since hostname command is not available on all systems
// This mechanism is also working for static analysis
if pf.IsFamily(inventory.FAMILY_LINUX) {
afs := &afero.Afero{Fs: conn.FileSystem()}
ok, err := afs.Exists("/etc/hostname")
if err == nil && ok {
content, err := afs.ReadFile("/etc/hostname")
if err == nil {
return strings.TrimSpace(string(content)), true
}
} else {
log.Debug().Err(err).Msg("could not read /etc/hostname file")
}
}
// Fallback for windows systems to using registry for static analysis
if pf.IsFamily(inventory.FAMILY_WINDOWS) && conn.Capabilities().Has(shared.Capability_FileSearch) {
fi, err := conn.FileInfo(registry.SystemRegPath)
if err != nil {
log.Debug().Err(err).Msg("could not find SYSTEM registry file, cannot perform hostname lookup")
return "", false
}
rh := registry.NewRegistryHandler()
defer func() {
err := rh.UnloadSubkeys()
if err != nil {
log.Debug().Err(err).Msg("could not unload registry subkeys")
}
}()
err = rh.LoadSubkey(registry.System, fi.Path)
if err != nil {
log.Debug().Err(err).Msg("could not load SYSTEM registry key file")
return "", false
}
key, err := rh.GetRegistryItemValue(registry.System, "ControlSet001\\Control\\ComputerName\\ComputerName", "ComputerName")
if err == nil {
return key.Value.String, true
}
// we also can try ControlSet002 as a fallback
log.Debug().Err(err).Msg("unable to read windows registry, trying ControlSet002 fallback")
key, err = rh.GetRegistryItemValue(registry.System, "ControlSet002\\Control\\ComputerName\\ComputerName", "ComputerName")
if err == nil {
return key.Value.String, true
}
}
return "", false
}
// runCommand is a wrapper around shared.Connection.RunCommand that helps execute commands
// and read the standard output all in one function.
func runCommand(conn shared.Connection, commandString string) (string, error) {
cmd, err := conn.RunCommand(commandString)
if err != nil && cmd.ExitStatus == 0 {
return "", err
}
data, err := io.ReadAll(cmd.Stdout)
if err != nil {
return "", err
}
return strings.TrimSpace(string(data)), nil
}
// parseGetentHosts runs `getent hosts <address>` and returns the first valid hostname
// that is not a variant of "localhost".
func parseGetentHosts(conn shared.Connection, ip string) (string, error) {
output, err := runCommand(conn, fmt.Sprintf("getent hosts %s", ip))
if err != nil {
return "", err
}
fields := strings.Fields(output)
if len(fields) < 2 {
return "", fmt.Errorf("no hostnames found for IP %s", ip)
}
for _, host := range fields[1:] {
if !isLocalhostVariant(host) {
return host, nil
}
}
return "", fmt.Errorf("no non-localhost hostname found for IP %s", ip)
}
// isLocalhostVariant returns true if the given hostname is a variant of "localhost"
func isLocalhostVariant(host string) bool {
lh := strings.ToLower(host)
return lh == "localhost" ||
lh == "localhost.localdomain" ||
lh == "ip6-localhost" ||
lh == "ip6-loopback"
}