Cybersecurity-Projects/PROJECTS/intermediate/docker-security-audit/internal/analyzer/image.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
}