Skip to content

Commit a8377be

Browse files
authored
fix: add check during image discovery to make sure images are valid (#3234)
Signed-off-by: Allen Conlon <[email protected]>
1 parent 1048c43 commit a8377be

File tree

9 files changed

+227
-6
lines changed

9 files changed

+227
-6
lines changed

src/pkg/packager/prepare.go

+12-5
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,15 @@ import (
88
"context"
99
"errors"
1010
"fmt"
11-
"github.com/zarf-dev/zarf/src/pkg/logger"
1211
"os"
1312
"path/filepath"
1413
"regexp"
1514
"sort"
1615
"strings"
1716
"time"
1817

18+
"github.com/zarf-dev/zarf/src/pkg/logger"
19+
1920
"github.com/defenseunicorns/pkg/helpers/v2"
2021
"github.com/goccy/go-yaml"
2122
"github.com/google/go-containerregistry/pkg/crane"
@@ -37,7 +38,7 @@ import (
3738
"github.com/zarf-dev/zarf/src/types"
3839
)
3940

40-
var imageCheck = regexp.MustCompile(`(?mi)"image":"([^"]+)"`)
41+
var imageCheck = regexp.MustCompile(`(?mi)"image":"((([a-z0-9._-]+)/)?([a-z0-9._-]+)(:([a-z0-9._-]+))?)"`)
4142
var imageFuzzyCheck = regexp.MustCompile(`(?mi)["|=]([a-z0-9\-.\/:]+:[\w.\-]*[a-z\.\-][\w.\-]*)"`)
4243

4344
// FindImages iterates over a Zarf.yaml and attempts to parse any images.
@@ -454,13 +455,19 @@ func findWhyResources(resources []*unstructured.Unstructured, whyImage, componen
454455

455456
func appendToImageMap(imgMap map[string]bool, pod corev1.PodSpec) map[string]bool {
456457
for _, container := range pod.InitContainers {
457-
imgMap[container.Image] = true
458+
if ReferenceRegexp.MatchString(container.Image) {
459+
imgMap[container.Image] = true
460+
}
458461
}
459462
for _, container := range pod.Containers {
460-
imgMap[container.Image] = true
463+
if ReferenceRegexp.MatchString(container.Image) {
464+
imgMap[container.Image] = true
465+
}
461466
}
462467
for _, container := range pod.EphemeralContainers {
463-
imgMap[container.Image] = true
468+
if ReferenceRegexp.MatchString(container.Image) {
469+
imgMap[container.Image] = true
470+
}
464471
}
465472
return imgMap
466473
}

src/pkg/packager/prepare_test.go

+22
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,28 @@ func TestFindImages(t *testing.T) {
6969
},
7070
},
7171
},
72+
{
73+
name: "valid-image-uri",
74+
cfg: &types.PackagerConfig{
75+
CreateOpts: types.ZarfCreateOptions{
76+
BaseDir: "./testdata/find-images/valid-image-uri",
77+
},
78+
FindImagesOpts: types.ZarfFindImagesOptions{
79+
SkipCosign: true,
80+
},
81+
},
82+
expectedImages: map[string][]string{
83+
"baseline": {
84+
"ghcr.io/zarf-dev/zarf/agent:v0.38.1",
85+
"10.0.0.1:443/zarf-dev/zarf/agent:v0.38.1",
86+
"alpine",
87+
"xn--7o8h.com/myimage:9.8.7",
88+
"registry.io/foo/project--id.module--name.ver---sion--name",
89+
"foo_bar:latest",
90+
"foo.com:8080/bar:1.2.3",
91+
},
92+
},
93+
},
7294
{
7395
name: "image not found",
7496
cfg: &types.PackagerConfig{

src/pkg/packager/regex.go

+101
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package packager
2+
3+
// Borrow from "github.com/containers/image" with love <3
4+
// https://github.com/containers/image/blob/aa915b75e867d14f6cb486a4fcc7d7c91cf4ca0a/docker/reference/regexp.go
5+
6+
import (
7+
"regexp"
8+
"strings"
9+
)
10+
11+
const (
12+
// alphaNumeric defines the alpha numeric atom, typically a
13+
// component of names. This only allows lower case characters and digits.
14+
alphaNumeric = `[a-z0-9]+`
15+
16+
// separator defines the separators allowed to be embedded in name
17+
// components. This allow one period, one or two underscore and multiple
18+
// dashes. Repeated dashes and underscores are intentionally treated
19+
// differently. In order to support valid hostnames as name components,
20+
// supporting repeated dash was added. Additionally double underscore is
21+
// now allowed as a separator to loosen the restriction for previously
22+
// supported names.
23+
separator = `(?:[._]|__|[-]*)`
24+
25+
// repository name to start with a component as defined by DomainRegexp
26+
// and followed by an optional port.
27+
domainComponent = `(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])`
28+
29+
// The string counterpart for TagRegexp.
30+
tag = `[\w][\w.-]{0,127}`
31+
32+
// The string counterpart for DigestRegexp.
33+
digestPat = `[A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][[:xdigit:]]{32,}`
34+
)
35+
36+
var (
37+
// nameComponent restricts registry path component names to start
38+
// with at least one letter or number, with following parts able to be
39+
// separated by one period, one or two underscore and multiple dashes.
40+
nameComponent = expression(
41+
alphaNumeric,
42+
optional(repeated(separator, alphaNumeric)))
43+
44+
domain = expression(
45+
domainComponent,
46+
optional(repeated(literal(`.`), domainComponent)),
47+
optional(literal(`:`), `[0-9]+`))
48+
49+
namePat = expression(
50+
optional(domain, literal(`/`)),
51+
nameComponent,
52+
optional(repeated(literal(`/`), nameComponent)))
53+
54+
referencePat = anchored(capture(namePat),
55+
optional(literal(":"), capture(tag)),
56+
optional(literal("@"), capture(digestPat)))
57+
58+
ReferenceRegexp = re(referencePat)
59+
)
60+
61+
// re compiles the string to a regular expression.
62+
var re = regexp.MustCompile
63+
64+
// literal compiles s into a literal regular expression, escaping any regexp
65+
// reserved characters.
66+
func literal(s string) string {
67+
return regexp.QuoteMeta(s)
68+
}
69+
70+
// expression defines a full expression, where each regular expression must
71+
// follow the previous.
72+
func expression(res ...string) string {
73+
return strings.Join(res, "")
74+
}
75+
76+
// optional wraps the expression in a non-capturing group and makes the
77+
// production optional.
78+
func optional(res ...string) string {
79+
return group(expression(res...)) + `?`
80+
}
81+
82+
// repeated wraps the regexp in a non-capturing group to get one or more
83+
// matches.
84+
func repeated(res ...string) string {
85+
return group(expression(res...)) + `+`
86+
}
87+
88+
// group wraps the regexp in a non-capturing group.
89+
func group(res ...string) string {
90+
return `(?:` + expression(res...) + `)`
91+
}
92+
93+
// capture wraps the expression in a capturing group.
94+
func capture(res ...string) string {
95+
return `(` + expression(res...) + `)`
96+
}
97+
98+
// anchored anchors the regular expression by adding start and end delimiters.
99+
func anchored(res ...string) string {
100+
return `^` + expression(res...) + `$`
101+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
image:
2+
repository: docker.io*

src/pkg/packager/testdata/find-images/helm-chart/zarf.yaml

+7-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ metadata:
44
version: 1.0.0
55
components:
66
- name: baseline
7-
required: true
7+
required: true
88
charts:
99
- name: with-values
1010
version: 0.1.0
@@ -16,3 +16,9 @@ components:
1616
version: 0.1.0
1717
namespace: without-values
1818
localPath: chart
19+
- name: invalid-image
20+
version: 0.1.0
21+
namespace: invalid-image
22+
localPath: chart
23+
valuesFiles:
24+
- values-invalid-image.yaml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
apiVersion: apps/v1
2+
kind: Deployment
3+
metadata:
4+
name: agent
5+
spec:
6+
selector:
7+
matchLabels:
8+
app: agent
9+
template:
10+
metadata:
11+
labels:
12+
app: agent
13+
spec:
14+
containers:
15+
# these should be detected
16+
- name: agent
17+
image: ghcr.io/zarf-dev/zarf/agent:v0.38.1
18+
- name: port
19+
image: 10.0.0.1:443/zarf-dev/zarf/agent:v0.38.1
20+
- name: alpine
21+
image: alpine
22+
- name: punycode
23+
image: xn--7o8h.com/myimage:9.8.7
24+
- name: project
25+
image: registry.io/foo/project--id.module--name.ver---sion--name
26+
- name: seperate
27+
image: foo_bar:latest
28+
- name: domain-port
29+
image: foo.com:8080/bar:1.2.3
30+
# these should NOT be detected
31+
- name: under
32+
image: _docker/_docker
33+
- name: quad-under
34+
image: ____/____
35+
- name: dash-namespace
36+
image: foo/-bar
37+
- name: slash-tag
38+
image: foo.com:http/bar
39+
- name: bad-image
40+
image: registry1.dso.mil*
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
apiVersion: kustomize.config.k8s.io/v1beta1
3+
kind: Kustomization
4+
resources:
5+
- deployment.yaml
6+
- policy.yaml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
---
2+
apiVersion: kyverno.io/v1
3+
kind: ClusterPolicy
4+
metadata:
5+
name: restrict-image-registries
6+
spec:
7+
background: true
8+
failurePolicy: Ignore
9+
rules:
10+
- exclude:
11+
any:
12+
- resources:
13+
namespaces:
14+
- kube-system
15+
match:
16+
all:
17+
- resources:
18+
kinds:
19+
- Pod
20+
name: validate-registries
21+
validate:
22+
foreach:
23+
- list: request.object.spec.[ephemeralContainers, initContainers, containers][]
24+
pattern:
25+
image: docker.io*
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
kind: ZarfPackageConfig
2+
metadata:
3+
name: valid-image-uri
4+
version: 1.0.0
5+
components:
6+
- name: baseline
7+
required: true
8+
manifests:
9+
- name: valid-image-uri
10+
namespace: default
11+
kustomizations:
12+
- ./.

0 commit comments

Comments
 (0)