Skip to content

Commit 53279b3

Browse files
committed
CNJR-7305: Add /etc/hosts file from host machine and container
1 parent a8bd413 commit 53279b3

File tree

6 files changed

+350
-0
lines changed

6 files changed

+350
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
2727
container to the inspect report. CNJR-4619
2828
- Resource usage check that captures the `top` command output from inside
2929
the container to the inspect report. CNJR-4618
30+
- Capture the `/etc/hosts` file from the container and the host. CNJR-7305
3031

3132
## [0.4.2] - 2025-03-25
3233

pkg/checks/container_etc_hosts.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
// Package checks defines all of the possible Conjur Inspect checks that can
2+
// be run.
3+
package checks
4+
5+
import (
6+
"bytes"
7+
"fmt"
8+
"io"
9+
"strings"
10+
11+
"github.com/cyberark/conjur-inspect/pkg/check"
12+
"github.com/cyberark/conjur-inspect/pkg/container"
13+
"github.com/cyberark/conjur-inspect/pkg/log"
14+
)
15+
16+
// ContainerEtcHosts collects the contents of /etc/hosts from inside a container
17+
type ContainerEtcHosts struct {
18+
Provider container.ContainerProvider
19+
}
20+
21+
// Describe provides a textual description of what this check gathers info on
22+
func (ceh *ContainerEtcHosts) Describe() string {
23+
return fmt.Sprintf("%s /etc/hosts", ceh.Provider.Name())
24+
}
25+
26+
// Run performs the container /etc/hosts collection
27+
func (ceh *ContainerEtcHosts) Run(runContext *check.RunContext) []check.Result {
28+
// If there is no container ID, return
29+
if strings.TrimSpace(runContext.ContainerID) == "" {
30+
return []check.Result{}
31+
}
32+
33+
// Check if the container runtime is available
34+
runtimeKey := strings.ToLower(ceh.Provider.Name())
35+
if !IsRuntimeAvailable(runContext, runtimeKey) {
36+
if runContext.VerboseErrors {
37+
return check.ErrorResult(
38+
ceh,
39+
fmt.Errorf("container runtime not available"),
40+
)
41+
}
42+
return []check.Result{}
43+
}
44+
45+
container := ceh.Provider.Container(runContext.ContainerID)
46+
47+
// Execute cat /etc/hosts inside the container
48+
stdout, stderr, err := container.Exec("cat", "/etc/hosts")
49+
if err != nil {
50+
if runContext.VerboseErrors {
51+
stderrBytes, _ := io.ReadAll(stderr)
52+
return check.ErrorResult(
53+
ceh,
54+
fmt.Errorf("failed to read /etc/hosts: %w (stderr: %s)", err, string(stderrBytes)),
55+
)
56+
}
57+
log.Warn("failed to read /etc/hosts from container: %s", err)
58+
return []check.Result{}
59+
}
60+
61+
// Read the stdout content
62+
fileBytes, err := io.ReadAll(stdout)
63+
if err != nil {
64+
if runContext.VerboseErrors {
65+
return check.ErrorResult(
66+
ceh,
67+
fmt.Errorf("failed to read command output: %w", err),
68+
)
69+
}
70+
log.Warn("failed to read /etc/hosts output: %s", err)
71+
return []check.Result{}
72+
}
73+
74+
// Save the file contents to output store with runtime-specific filename
75+
outputFilename := fmt.Sprintf(
76+
"%s-etc-hosts.txt",
77+
strings.ToLower(ceh.Provider.Name()),
78+
)
79+
_, err = runContext.OutputStore.Save(outputFilename, bytes.NewReader(fileBytes))
80+
if err != nil {
81+
log.Warn("failed to save /etc/hosts output: %s", err)
82+
}
83+
84+
// Return empty results on success (output is saved)
85+
return []check.Result{}
86+
}
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
package checks
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
"testing"
7+
8+
"github.com/cyberark/conjur-inspect/pkg/check"
9+
"github.com/cyberark/conjur-inspect/pkg/test"
10+
"github.com/stretchr/testify/assert"
11+
"github.com/stretchr/testify/require"
12+
)
13+
14+
func TestContainerEtcHostsDescribe(t *testing.T) {
15+
provider := &test.ContainerProvider{}
16+
ceh := &ContainerEtcHosts{Provider: provider}
17+
assert.Equal(t, "Test Container Provider /etc/hosts", ceh.Describe())
18+
}
19+
20+
func TestContainerEtcHostsRunSuccessful(t *testing.T) {
21+
hostsContent := "127.0.0.1\tlocalhost\n::1\tlocalhost\n172.17.0.2\tconjur\n"
22+
23+
provider := &test.ContainerProvider{
24+
ExecResponses: map[string]test.ExecResponse{
25+
"cat /etc/hosts": {
26+
Stdout: strings.NewReader(hostsContent),
27+
Stderr: strings.NewReader(""),
28+
Error: nil,
29+
},
30+
},
31+
}
32+
33+
ceh := &ContainerEtcHosts{Provider: provider}
34+
runContext := test.NewRunContext("container123")
35+
36+
results := ceh.Run(&runContext)
37+
38+
// Should return empty results on success
39+
assert.Empty(t, results)
40+
41+
// Verify the file was saved to the output store
42+
items, err := runContext.OutputStore.Items()
43+
require.NoError(t, err)
44+
require.Len(t, items, 1)
45+
info, err := items[0].Info()
46+
require.NoError(t, err)
47+
// Test provider name is "Test Container Provider" -> lowercase -> "test container provider"
48+
assert.Equal(t, "test container provider-etc-hosts.txt", info.Name())
49+
}
50+
51+
func TestContainerEtcHostsEmptyContainerID(t *testing.T) {
52+
provider := &test.ContainerProvider{}
53+
54+
ceh := &ContainerEtcHosts{Provider: provider}
55+
runContext := test.NewRunContext("")
56+
results := ceh.Run(&runContext)
57+
58+
// Should return empty results when no container ID
59+
assert.Empty(t, results)
60+
61+
// Verify no output was saved
62+
items, err := runContext.OutputStore.Items()
63+
require.NoError(t, err)
64+
require.Len(t, items, 0)
65+
}
66+
67+
func TestContainerEtcHostsRuntimeNotAvailable(t *testing.T) {
68+
provider := &test.ContainerProvider{}
69+
70+
ceh := &ContainerEtcHosts{Provider: provider}
71+
runContext := test.NewRunContext("container123")
72+
73+
// Set runtime as not available
74+
runContext.ContainerRuntimeAvailability = map[string]check.RuntimeAvailability{
75+
"test container provider": {
76+
Available: false,
77+
Error: fmt.Errorf("runtime not found"),
78+
},
79+
}
80+
81+
results := ceh.Run(&runContext)
82+
83+
// Should return empty results when runtime not available
84+
assert.Empty(t, results)
85+
86+
// Verify no output was saved
87+
items, err := runContext.OutputStore.Items()
88+
require.NoError(t, err)
89+
require.Len(t, items, 0)
90+
}
91+
92+
func TestContainerEtcHostsRuntimeNotAvailableWithVerboseErrors(t *testing.T) {
93+
provider := &test.ContainerProvider{}
94+
95+
ceh := &ContainerEtcHosts{Provider: provider}
96+
runContext := test.NewRunContext("container123")
97+
runContext.VerboseErrors = true
98+
99+
// Set runtime as not available
100+
runContext.ContainerRuntimeAvailability = map[string]check.RuntimeAvailability{
101+
"test container provider": {
102+
Available: false,
103+
Error: fmt.Errorf("runtime not found"),
104+
},
105+
}
106+
107+
results := ceh.Run(&runContext)
108+
109+
// Should return error result with verbose errors enabled
110+
require.Len(t, results, 1)
111+
assert.Equal(t, check.StatusError, results[0].Status)
112+
assert.Contains(t, results[0].Message, "container runtime not available")
113+
}
114+
115+
func TestContainerEtcHostsExecError(t *testing.T) {
116+
provider := &test.ContainerProvider{
117+
ExecResponses: map[string]test.ExecResponse{
118+
"cat /etc/hosts": {
119+
Stdout: strings.NewReader(""),
120+
Stderr: strings.NewReader("cat: /etc/hosts: Permission denied"),
121+
Error: fmt.Errorf("exit status 1"),
122+
},
123+
},
124+
}
125+
126+
ceh := &ContainerEtcHosts{Provider: provider}
127+
runContext := test.NewRunContext("container123")
128+
129+
results := ceh.Run(&runContext)
130+
131+
// Should return empty results without verbose errors
132+
assert.Empty(t, results)
133+
134+
// Verify no output was saved
135+
items, err := runContext.OutputStore.Items()
136+
require.NoError(t, err)
137+
require.Len(t, items, 0)
138+
}
139+
140+
func TestContainerEtcHostsExecErrorWithVerboseErrors(t *testing.T) {
141+
provider := &test.ContainerProvider{
142+
ExecResponses: map[string]test.ExecResponse{
143+
"cat /etc/hosts": {
144+
Stdout: strings.NewReader(""),
145+
Stderr: strings.NewReader("cat: /etc/hosts: Permission denied"),
146+
Error: fmt.Errorf("exit status 1"),
147+
},
148+
},
149+
}
150+
151+
ceh := &ContainerEtcHosts{Provider: provider}
152+
runContext := test.NewRunContext("container123")
153+
runContext.VerboseErrors = true
154+
155+
results := ceh.Run(&runContext)
156+
157+
// Should return error result with verbose errors enabled
158+
require.Len(t, results, 1)
159+
assert.Equal(t, check.StatusError, results[0].Status)
160+
assert.Contains(t, results[0].Message, "failed to read /etc/hosts")
161+
assert.Contains(t, results[0].Message, "Permission denied")
162+
}

pkg/checks/host_etc_hosts.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Package checks defines all of the possible Conjur Inspect checks that can
2+
// be run.
3+
package checks
4+
5+
import (
6+
"bytes"
7+
"os"
8+
9+
"github.com/cyberark/conjur-inspect/pkg/check"
10+
"github.com/cyberark/conjur-inspect/pkg/log"
11+
)
12+
13+
// HostEtcHosts collects the contents of /etc/hosts from the host machine
14+
type HostEtcHosts struct{}
15+
16+
// Describe provides a textual description of what this check gathers info on
17+
func (*HostEtcHosts) Describe() string {
18+
return "Host /etc/hosts"
19+
}
20+
21+
// Run performs the host /etc/hosts collection
22+
func (h *HostEtcHosts) Run(runContext *check.RunContext) []check.Result {
23+
fileBytes, err := os.ReadFile("/etc/hosts")
24+
if err != nil {
25+
if runContext.VerboseErrors {
26+
return check.ErrorResult(h, err)
27+
}
28+
log.Warn("failed to read /etc/hosts: %s", err)
29+
return []check.Result{}
30+
}
31+
32+
// Save the file contents to output store
33+
_, err = runContext.OutputStore.Save(
34+
"host-etc-hosts.txt",
35+
bytes.NewReader(fileBytes),
36+
)
37+
if err != nil {
38+
log.Warn("failed to save /etc/hosts output: %s", err)
39+
}
40+
41+
// Return empty results on success (output is saved)
42+
return []check.Result{}
43+
}

pkg/checks/host_etc_hosts_test.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package checks
2+
3+
import (
4+
"testing"
5+
6+
"github.com/cyberark/conjur-inspect/pkg/test"
7+
"github.com/stretchr/testify/assert"
8+
"github.com/stretchr/testify/require"
9+
)
10+
11+
func TestHostEtcHostsDescribe(t *testing.T) {
12+
h := &HostEtcHosts{}
13+
assert.Equal(t, "Host /etc/hosts", h.Describe())
14+
}
15+
16+
func TestHostEtcHostsRunSuccessful(t *testing.T) {
17+
// This test reads the actual /etc/hosts file on the system
18+
h := &HostEtcHosts{}
19+
runContext := test.NewRunContext("")
20+
results := h.Run(&runContext)
21+
22+
// Should return empty results on success
23+
assert.Empty(t, results)
24+
25+
// Verify the file was saved to the output store
26+
items, err := runContext.OutputStore.Items()
27+
require.NoError(t, err)
28+
require.Len(t, items, 1)
29+
info, err := items[0].Info()
30+
require.NoError(t, err)
31+
assert.Equal(t, "host-etc-hosts.txt", info.Name())
32+
}
33+
34+
func TestHostEtcHostsRunWithVerboseErrors(t *testing.T) {
35+
// This test verifies that we return empty results even with verbose errors
36+
// when the file is readable (which /etc/hosts typically is)
37+
h := &HostEtcHosts{}
38+
runContext := test.NewRunContext("")
39+
runContext.VerboseErrors = true
40+
results := h.Run(&runContext)
41+
42+
// Should still return empty results when file is readable
43+
assert.Empty(t, results)
44+
45+
// Verify the file was saved to the output store
46+
items, err := runContext.OutputStore.Items()
47+
require.NoError(t, err)
48+
require.Len(t, items, 1)
49+
}

pkg/cmd/default_report.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ func defaultReportSections() []report.Section {
7373
Checks: []check.Check{
7474
&checks.Host{},
7575
&checks.CommandHistory{},
76+
&checks.HostEtcHosts{},
7677
},
7778
},
7879
{
@@ -158,6 +159,14 @@ func defaultReportSections() []report.Section {
158159
&checks.RunItServices{
159160
Provider: &container.PodmanProvider{},
160161
},
162+
163+
// Container /etc/hosts
164+
&checks.ContainerEtcHosts{
165+
Provider: &container.DockerProvider{},
166+
},
167+
&checks.ContainerEtcHosts{
168+
Provider: &container.PodmanProvider{},
169+
},
161170
},
162171
},
163172
{

0 commit comments

Comments
 (0)