194 lines
5.0 KiB
Go
194 lines
5.0 KiB
Go
/*
|
|
© AngelaMos | 2026
|
|
image.go
|
|
|
|
ImageAnalyzer inspects local Docker images for CIS Section 4 violations
|
|
|
|
Lists all local images via the Docker API, inspects each one, and
|
|
checks for root user configuration, missing or explicitly disabled
|
|
HEALTHCHECK, and privileged port exposure. Findings reference CIS
|
|
Section 4 controls.
|
|
|
|
Key exports:
|
|
ImageAnalyzer - implements Analyzer for local Docker images
|
|
NewImageAnalyzer - constructor taking a docker.Client
|
|
|
|
Connects to:
|
|
analyzer.go - implements Analyzer interface, uses CategoryImage
|
|
docker/client.go - lists and inspects images
|
|
benchmark/controls.go - fetches CIS Section 4 controls by ID
|
|
finding.go - creates findings with CISControl references
|
|
*/
|
|
|
|
package analyzer
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/CarterPerez-dev/docksec/internal/benchmark"
|
|
"github.com/CarterPerez-dev/docksec/internal/docker"
|
|
"github.com/CarterPerez-dev/docksec/internal/finding"
|
|
"github.com/docker/docker/api/types/image"
|
|
)
|
|
|
|
type ImageAnalyzer struct {
|
|
client *docker.Client
|
|
}
|
|
|
|
func NewImageAnalyzer(client *docker.Client) *ImageAnalyzer {
|
|
return &ImageAnalyzer{client: client}
|
|
}
|
|
|
|
func (a *ImageAnalyzer) Name() string {
|
|
return "image"
|
|
}
|
|
|
|
func (a *ImageAnalyzer) Analyze(
|
|
ctx context.Context,
|
|
) (finding.Collection, error) {
|
|
images, err := a.client.ListImages(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var findings finding.Collection
|
|
for _, img := range images {
|
|
info, err := a.client.InspectImage(ctx, img.ID)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
name := img.ID[:12]
|
|
if len(img.RepoTags) > 0 {
|
|
name = img.RepoTags[0]
|
|
}
|
|
|
|
target := finding.Target{
|
|
Type: finding.TargetImage,
|
|
Name: name,
|
|
ID: img.ID,
|
|
}
|
|
|
|
findings = append(findings, a.analyzeImage(target, info)...)
|
|
}
|
|
|
|
return findings, nil
|
|
}
|
|
|
|
func (a *ImageAnalyzer) analyzeImage(
|
|
target finding.Target,
|
|
info image.InspectResponse,
|
|
) finding.Collection {
|
|
var findings finding.Collection
|
|
|
|
findings = append(findings, a.checkRootUser(target, info)...)
|
|
findings = append(findings, a.checkHealthcheck(target, info)...)
|
|
findings = append(findings, a.checkExposedPorts(target, info)...)
|
|
|
|
return findings
|
|
}
|
|
|
|
func (a *ImageAnalyzer) checkRootUser(
|
|
target finding.Target,
|
|
info image.InspectResponse,
|
|
) finding.Collection {
|
|
var findings finding.Collection
|
|
|
|
if info.Config == nil {
|
|
return findings
|
|
}
|
|
|
|
user := info.Config.User
|
|
if user == "" || user == "root" || user == "0" {
|
|
control, _ := benchmark.Get("4.1")
|
|
f := finding.New("CIS-4.1", control.Title, finding.SeverityMedium, target).
|
|
WithDescription(control.Description).
|
|
WithCategory(string(CategoryImage)).
|
|
WithRemediation(control.Remediation).
|
|
WithReferences(control.References...).
|
|
WithCISControl(control.ToCISControl())
|
|
findings = append(findings, f)
|
|
}
|
|
|
|
return findings
|
|
}
|
|
|
|
func (a *ImageAnalyzer) checkHealthcheck(
|
|
target finding.Target,
|
|
info image.InspectResponse,
|
|
) finding.Collection {
|
|
var findings finding.Collection
|
|
|
|
if info.Config == nil {
|
|
return findings
|
|
}
|
|
|
|
if info.Config.Healthcheck == nil ||
|
|
len(info.Config.Healthcheck.Test) == 0 {
|
|
control, _ := benchmark.Get("4.6")
|
|
f := finding.New("CIS-4.6", control.Title, finding.SeverityLow, target).
|
|
WithDescription(control.Description).
|
|
WithCategory(string(CategoryImage)).
|
|
WithRemediation(control.Remediation).
|
|
WithReferences(control.References...).
|
|
WithCISControl(control.ToCISControl())
|
|
findings = append(findings, f)
|
|
}
|
|
|
|
if info.Config.Healthcheck != nil &&
|
|
len(info.Config.Healthcheck.Test) > 0 {
|
|
if info.Config.Healthcheck.Test[0] == "NONE" {
|
|
control, _ := benchmark.Get("4.6")
|
|
f := finding.New("CIS-4.6", "HEALTHCHECK explicitly disabled", finding.SeverityLow, target).
|
|
WithDescription("Image has HEALTHCHECK set to NONE, disabling health monitoring.").
|
|
WithCategory(string(CategoryImage)).
|
|
WithRemediation(control.Remediation).
|
|
WithReferences(control.References...).
|
|
WithCISControl(control.ToCISControl())
|
|
findings = append(findings, f)
|
|
}
|
|
}
|
|
|
|
return findings
|
|
}
|
|
|
|
func (a *ImageAnalyzer) checkExposedPorts(
|
|
target finding.Target,
|
|
info image.InspectResponse,
|
|
) finding.Collection {
|
|
var findings finding.Collection
|
|
|
|
if info.Config == nil || info.Config.ExposedPorts == nil {
|
|
return findings
|
|
}
|
|
|
|
privilegedPorts := []string{}
|
|
for port := range info.Config.ExposedPorts {
|
|
portNum := strings.Split(string(port), "/")[0]
|
|
if isPrivilegedPort(portNum) {
|
|
privilegedPorts = append(privilegedPorts, portNum)
|
|
}
|
|
}
|
|
|
|
if len(privilegedPorts) > 0 {
|
|
f := finding.New("DS-IMG-PRIVPORT", "Image exposes privileged ports: "+strings.Join(privilegedPorts, ", "), finding.SeverityInfo, target).
|
|
WithDescription("Image exposes ports below 1024 which typically require root privileges.").
|
|
WithCategory(string(CategoryImage)).
|
|
WithRemediation("Consider using non-privileged ports (>1024) and mapping them at runtime if needed.")
|
|
findings = append(findings, f)
|
|
}
|
|
|
|
return findings
|
|
}
|
|
|
|
func isPrivilegedPort(port string) bool {
|
|
var portNum int
|
|
_, err := fmt.Sscanf(port, "%d", &portNum)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return portNum > 0 && portNum < 1024
|
|
}
|