Skip to content

Commit aac9e2c

Browse files
tarnowscGitHub Enterprise
authored andcommitted
Merge pull request #22 from Conjur-Enterprise/cnjr-7307-network-inspect
CNJR-7307: Add network inspect and /etc/hosts checks
2 parents a8bd413 + f00a013 commit aac9e2c

15 files changed

+861
-11
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ 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
31+
- Capture the container runtime's `network inspect` output. CNJR-7307
3032

3133
## [0.4.2] - 2025-03-25
3234

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+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// Package checks defines all of the possible Conjur Inspect checks that can
2+
// be run.
3+
package checks
4+
5+
import (
6+
"fmt"
7+
"io"
8+
"strings"
9+
10+
"github.com/cyberark/conjur-inspect/pkg/check"
11+
"github.com/cyberark/conjur-inspect/pkg/container"
12+
"github.com/cyberark/conjur-inspect/pkg/log"
13+
)
14+
15+
// ContainerNetworkInspect collects the network inspection data from the
16+
// container runtime and saves it to the output store.
17+
type ContainerNetworkInspect struct {
18+
Provider container.ContainerProvider
19+
}
20+
21+
// Describe provides a textual description of what this check gathers info on
22+
func (cni *ContainerNetworkInspect) Describe() string {
23+
return fmt.Sprintf("%s network inspect", cni.Provider.Name())
24+
}
25+
26+
// Run performs the network inspection check
27+
func (cni *ContainerNetworkInspect) Run(runContext *check.RunContext) []check.Result {
28+
// Check if the container runtime is available
29+
runtimeKey := strings.ToLower(cni.Provider.Name())
30+
if !IsRuntimeAvailable(runContext, runtimeKey) {
31+
if runContext.VerboseErrors {
32+
return check.ErrorResult(
33+
cni,
34+
fmt.Errorf("container runtime not available"),
35+
)
36+
}
37+
return []check.Result{}
38+
}
39+
40+
networkInspectOutput, err := cni.Provider.NetworkInspect()
41+
if err != nil {
42+
if runContext.VerboseErrors {
43+
return check.ErrorResult(
44+
cni,
45+
fmt.Errorf("failed to inspect networks: %w", err),
46+
)
47+
}
48+
return []check.Result{}
49+
}
50+
51+
// Read the output to save it
52+
outputBytes, err := io.ReadAll(networkInspectOutput)
53+
if err != nil {
54+
if runContext.VerboseErrors {
55+
return check.ErrorResult(
56+
cni,
57+
fmt.Errorf("failed to read network inspect output: %w", err),
58+
)
59+
}
60+
return []check.Result{}
61+
}
62+
63+
// Save raw network inspect output
64+
outputFileName := fmt.Sprintf(
65+
"%s-network-inspect.json",
66+
strings.ToLower(cni.Provider.Name()),
67+
)
68+
_, err = runContext.OutputStore.Save(outputFileName, strings.NewReader(string(outputBytes)))
69+
if err != nil {
70+
log.Warn(
71+
"Failed to save %s network inspect output: %s",
72+
cni.Provider.Name(),
73+
err,
74+
)
75+
}
76+
77+
return []check.Result{}
78+
}

0 commit comments

Comments
 (0)