Skip to content

Commit be1cf87

Browse files
authored
feat(docker): Add docker-compose parser with security queries (#419)
## Summary Adds docker-compose.yml parsing support with security-focused query methods for detecting misconfigurations. **Stacked on:** docker/03-instruction-converters (#418) ## Changes - YAML parser infrastructure (YAMLGraph, YAMLNode) - ComposeGraph wrapper with service/volume/network indexing - Security query methods: GetPrivilegedServices, ServicesWithDockerSocket, ServiceHasSecurityOpt, ServiceHasCapability, etc. - 20 comprehensive test cases with 100% coverage - Support for both array and map environment variable formats ## Checklist - [x] Tests passing (`gradle testGo`) - [x] Lint passing (`gradle lintGo`)
1 parent 280acea commit be1cf87

4 files changed

Lines changed: 1350 additions & 0 deletions

File tree

Lines changed: 294 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,294 @@
1+
package graph
2+
3+
import (
4+
"strconv"
5+
"strings"
6+
)
7+
8+
// ComposeGraph wraps a YAMLGraph with docker-compose specific indexing.
9+
type ComposeGraph struct {
10+
// Embedded YAML graph
11+
YAMLGraph *YAMLGraph
12+
13+
// Compose-specific indexes
14+
Services map[string]*YAMLNode
15+
Volumes map[string]*YAMLNode
16+
Networks map[string]*YAMLNode
17+
18+
// Metadata
19+
Version string
20+
FilePath string
21+
}
22+
23+
// ParseDockerCompose parses a docker-compose.yml file.
24+
func ParseDockerCompose(filePath string) (*ComposeGraph, error) {
25+
yamlGraph, err := ParseYAML(filePath)
26+
if err != nil {
27+
return nil, err
28+
}
29+
30+
return NewComposeGraph(yamlGraph, filePath), nil
31+
}
32+
33+
// NewComposeGraph creates a ComposeGraph from a YAMLGraph.
34+
func NewComposeGraph(yamlGraph *YAMLGraph, filePath string) *ComposeGraph {
35+
compose := &ComposeGraph{
36+
YAMLGraph: yamlGraph,
37+
Services: make(map[string]*YAMLNode),
38+
Volumes: make(map[string]*YAMLNode),
39+
Networks: make(map[string]*YAMLNode),
40+
FilePath: filePath,
41+
}
42+
43+
// Index services
44+
servicesNode := yamlGraph.Query("services")
45+
if servicesNode != nil && servicesNode.Children != nil {
46+
for name, node := range servicesNode.Children {
47+
compose.Services[name] = node
48+
}
49+
}
50+
51+
// Index volumes
52+
volumesNode := yamlGraph.Query("volumes")
53+
if volumesNode != nil && volumesNode.Children != nil {
54+
for name, node := range volumesNode.Children {
55+
compose.Volumes[name] = node
56+
}
57+
}
58+
59+
// Index networks
60+
networksNode := yamlGraph.Query("networks")
61+
if networksNode != nil && networksNode.Children != nil {
62+
for name, node := range networksNode.Children {
63+
compose.Networks[name] = node
64+
}
65+
}
66+
67+
// Get version
68+
versionNode := yamlGraph.Query("version")
69+
if versionNode != nil {
70+
compose.Version = versionNode.StringValue()
71+
}
72+
73+
return compose
74+
}
75+
76+
// --- Service Query Methods ---
77+
78+
// GetServices returns all service names.
79+
func (c *ComposeGraph) GetServices() []string {
80+
names := make([]string, 0, len(c.Services))
81+
for name := range c.Services {
82+
names = append(names, name)
83+
}
84+
return names
85+
}
86+
87+
// ServiceHas checks if a service has a property with specified value.
88+
func (c *ComposeGraph) ServiceHas(serviceName, key string, value interface{}) bool {
89+
service, exists := c.Services[serviceName]
90+
if !exists {
91+
return false
92+
}
93+
return c.nodeHasValue(service, key, value)
94+
}
95+
96+
// ServiceHasKey checks if a service has a property defined.
97+
func (c *ComposeGraph) ServiceHasKey(serviceName, key string) bool {
98+
service, exists := c.Services[serviceName]
99+
if !exists {
100+
return false
101+
}
102+
return service.HasChild(key)
103+
}
104+
105+
// ServiceGet retrieves a service property value.
106+
func (c *ComposeGraph) ServiceGet(serviceName, key string) interface{} {
107+
service, exists := c.Services[serviceName]
108+
if !exists {
109+
return nil
110+
}
111+
child := service.GetChild(key)
112+
if child == nil {
113+
return nil
114+
}
115+
return child.Value
116+
}
117+
118+
// --- Security Query Methods ---
119+
120+
// GetPrivilegedServices returns services with privileged: true.
121+
func (c *ComposeGraph) GetPrivilegedServices() []string {
122+
privileged := make([]string, 0)
123+
for name, service := range c.Services {
124+
if c.nodeHasValue(service, "privileged", true) {
125+
privileged = append(privileged, name)
126+
}
127+
}
128+
return privileged
129+
}
130+
131+
// ServicesWithDockerSocket returns services that mount Docker socket.
132+
func (c *ComposeGraph) ServicesWithDockerSocket() []string {
133+
exposed := make([]string, 0)
134+
135+
for name, service := range c.Services {
136+
volumesNode := service.GetChild("volumes")
137+
if volumesNode == nil {
138+
continue
139+
}
140+
141+
for _, vol := range volumesNode.ListValues() {
142+
volStr, ok := vol.(string)
143+
if !ok {
144+
continue
145+
}
146+
if strings.Contains(volStr, "/var/run/docker.sock") ||
147+
strings.Contains(volStr, "/run/docker.sock") {
148+
exposed = append(exposed, name)
149+
break
150+
}
151+
}
152+
}
153+
154+
return exposed
155+
}
156+
157+
// ServiceHasSecurityOpt checks for specific security_opt value.
158+
func (c *ComposeGraph) ServiceHasSecurityOpt(serviceName, optValue string) bool {
159+
service, exists := c.Services[serviceName]
160+
if !exists {
161+
return false
162+
}
163+
164+
secOptNode := service.GetChild("security_opt")
165+
if secOptNode == nil {
166+
return false
167+
}
168+
169+
for _, opt := range secOptNode.ListValues() {
170+
if optStr, ok := opt.(string); ok && optStr == optValue {
171+
return true
172+
}
173+
}
174+
175+
return false
176+
}
177+
178+
// ServiceHasCapability checks for capability in cap_add or cap_drop.
179+
func (c *ComposeGraph) ServiceHasCapability(serviceName, capability, capType string) bool {
180+
service, exists := c.Services[serviceName]
181+
if !exists {
182+
return false
183+
}
184+
185+
capNode := service.GetChild(capType) // "cap_add" or "cap_drop"
186+
if capNode == nil {
187+
return false
188+
}
189+
190+
for _, cap := range capNode.ListValues() {
191+
if capStr, ok := cap.(string); ok && capStr == capability {
192+
return true
193+
}
194+
}
195+
196+
return false
197+
}
198+
199+
// ServicesWithHostNetwork returns services using network_mode: host.
200+
func (c *ComposeGraph) ServicesWithHostNetwork() []string {
201+
hostMode := make([]string, 0)
202+
for name, service := range c.Services {
203+
if c.nodeHasValue(service, "network_mode", "host") {
204+
hostMode = append(hostMode, name)
205+
}
206+
}
207+
return hostMode
208+
}
209+
210+
// ServicesWithoutReadOnly returns services without read_only: true.
211+
func (c *ComposeGraph) ServicesWithoutReadOnly() []string {
212+
writable := make([]string, 0)
213+
for name, service := range c.Services {
214+
if !service.HasChild("read_only") {
215+
writable = append(writable, name)
216+
} else if !c.nodeHasValue(service, "read_only", true) {
217+
writable = append(writable, name)
218+
}
219+
}
220+
return writable
221+
}
222+
223+
// ServiceExposesPort checks if a service exposes a specific port.
224+
func (c *ComposeGraph) ServiceExposesPort(serviceName string, port int) bool {
225+
service, exists := c.Services[serviceName]
226+
if !exists {
227+
return false
228+
}
229+
230+
portsNode := service.GetChild("ports")
231+
if portsNode == nil {
232+
return false
233+
}
234+
235+
for _, portMapping := range portsNode.ListValues() {
236+
portStr, ok := portMapping.(string)
237+
if !ok {
238+
continue
239+
}
240+
// Parse formats: "8080:80", "8080", "8080:80/tcp"
241+
parts := strings.Split(strings.Split(portStr, "/")[0], ":")
242+
for _, p := range parts {
243+
if portNum, err := strconv.Atoi(p); err == nil && portNum == port {
244+
return true
245+
}
246+
}
247+
}
248+
249+
return false
250+
}
251+
252+
// ServiceHasEnvVar checks if service has environment variable.
253+
func (c *ComposeGraph) ServiceHasEnvVar(serviceName, varName string) bool {
254+
service, exists := c.Services[serviceName]
255+
if !exists {
256+
return false
257+
}
258+
259+
envNode := service.GetChild("environment")
260+
if envNode == nil {
261+
return false
262+
}
263+
264+
// Handle map format: {VAR: value}
265+
if envNode.Children != nil {
266+
if _, exists := envNode.Children[varName]; exists {
267+
return true
268+
}
269+
}
270+
271+
// Handle array format: ["VAR=value"]
272+
for _, env := range envNode.ListValues() {
273+
if envStr, ok := env.(string); ok {
274+
if strings.HasPrefix(envStr, varName+"=") || envStr == varName {
275+
return true
276+
}
277+
}
278+
}
279+
280+
return false
281+
}
282+
283+
// --- Helper Methods ---
284+
285+
func (c *ComposeGraph) nodeHasValue(node *YAMLNode, key string, expected interface{}) bool {
286+
if node == nil {
287+
return false
288+
}
289+
child := node.GetChild(key)
290+
if child == nil {
291+
return false
292+
}
293+
return child.Value == expected
294+
}

0 commit comments

Comments
 (0)