Skip to content

Commit fb7988e

Browse files
committed
publish metadata from image-get
1 parent edb7457 commit fb7988e

File tree

8 files changed

+875
-4
lines changed

8 files changed

+875
-4
lines changed

components/targets/image-get/component.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,9 @@ steps:
1818
args:
1919
- -c
2020
- /entrypoint.sh "{{ .parameters.image }}" "{{ sourceCodeWorkspace }}/image.tar" "{{ .parameters.username }}" "{{.parameters.password}}"
21+
- name: write-metadata
22+
image: components/targets/image-get/metadata-writer
23+
executable: /bin/app
24+
env_vars:
25+
IMAGE_GET_TARGET_METADATA_PATH: "{{ targetMetadataWorkspace }}"
26+
IMAGE_GET_IMAGE: "{{ .parameters.image }}"
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"log"
6+
"os"
7+
"path"
8+
"strings"
9+
"time"
10+
11+
"github.com/go-errors/errors"
12+
"github.com/google/go-containerregistry/pkg/name"
13+
"github.com/package-url/packageurl-go"
14+
"google.golang.org/protobuf/encoding/protojson"
15+
16+
"github.com/smithy-security/pkg/env"
17+
"github.com/smithy-security/smithy/sdk/component"
18+
ocsffindinginfo "github.com/smithy-security/smithy/sdk/gen/ocsf_ext/finding_info/v1"
19+
)
20+
21+
type (
22+
// Conf wraps the component configuration.
23+
Conf struct {
24+
TargetMetadataPath string
25+
Image string
26+
}
27+
imageMetadataWriterTarget struct {
28+
conf *Conf
29+
}
30+
)
31+
32+
// NewConf returns a new configuration build from environment lookup.
33+
func NewConf(envLoader env.Loader) (*Conf, error) {
34+
var envOpts = make([]env.ParseOption, 0)
35+
targetMetadataPath, err := env.GetOrDefault(
36+
"IMAGE_GET_TARGET_METADATA_PATH",
37+
"",
38+
append(envOpts, env.WithDefaultOnError(true))...,
39+
)
40+
if err != nil {
41+
return nil, errors.Errorf("could not get IMAGE_GET_TARGET_METADATA_PATH: %w", err)
42+
}
43+
44+
image, err := env.GetOrDefault(
45+
"IMAGE_GET_IMAGE",
46+
"",
47+
append(envOpts, env.WithDefaultOnError(true))...,
48+
)
49+
if err != nil {
50+
return nil, errors.Errorf("could not get IMAGE_GET_IMAGE: %w", err)
51+
}
52+
53+
if targetMetadataPath != "" && !strings.HasSuffix(targetMetadataPath, "target.json") {
54+
targetMetadataPath = path.Join(targetMetadataPath, "target.json")
55+
}
56+
return &Conf{
57+
TargetMetadataPath: targetMetadataPath,
58+
Image: image,
59+
}, nil
60+
}
61+
62+
func WriteTargetMetadata(conf *Conf) error {
63+
purl, err := packageUrlFromImage(conf.Image)
64+
if err != nil {
65+
return errors.Errorf("could not get package url from image: %w", err)
66+
}
67+
68+
dataSource := &ocsffindinginfo.DataSource{
69+
TargetType: ocsffindinginfo.DataSource_TARGET_TYPE_CONTAINER_IMAGE,
70+
OciPackageMetadata: &ocsffindinginfo.DataSource_OCIPackageMetadata{
71+
PackageUrl: purl.ToString(),
72+
Tag: purl.Version,
73+
},
74+
}
75+
76+
marshaledDataSource, err := protojson.Marshal(dataSource)
77+
if err != nil {
78+
return errors.Errorf("could not marshal data source into JSON: %w", err)
79+
}
80+
81+
// Write content to the file
82+
err = os.WriteFile(conf.TargetMetadataPath, marshaledDataSource, 0644)
83+
if err != nil {
84+
return errors.Errorf("Error writing file: %w", err)
85+
}
86+
return nil
87+
}
88+
89+
func packageUrlFromImage(image string) (*packageurl.PackageURL, error) {
90+
var (
91+
namespace = ""
92+
path = ""
93+
tag = ""
94+
digest = ""
95+
)
96+
// Parse the reference
97+
ref, err := name.ParseReference(image)
98+
if err != nil {
99+
return nil, err
100+
}
101+
102+
// Add registry if not docker.io
103+
registry := ref.Context().Registry.Name()
104+
if registry != "index.docker.io" && registry != "docker.io" {
105+
namespace = registry
106+
}
107+
108+
// Add repository path
109+
path = ref.Context().RepositoryStr()
110+
111+
// Add tag or digest
112+
if tagged, ok := ref.(name.Tag); ok {
113+
tag = tagged.TagStr()
114+
} else if digested, ok := ref.(name.Digest); ok {
115+
digest = digested.DigestStr()
116+
}
117+
118+
var qualifiers packageurl.Qualifiers
119+
if digest != "" {
120+
qualifiers = packageurl.QualifiersFromMap(map[string]string{"digest": digest})
121+
122+
// special edge case: always prefer the digest over tag
123+
tag = ""
124+
}
125+
126+
if tag == "" && digest == "" {
127+
// If no tag or digest is provided, default to "latest"
128+
tag = "latest"
129+
}
130+
131+
return packageurl.NewPackageURL(
132+
packageurl.TypeDocker,
133+
namespace,
134+
path,
135+
tag,
136+
qualifiers,
137+
"",
138+
), nil
139+
}
140+
141+
func main() {
142+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
143+
defer cancel()
144+
145+
if err := Main(ctx); err != nil {
146+
log.Fatalf("unexpected error: %v", err)
147+
}
148+
}
149+
150+
func Main(ctx context.Context) error {
151+
conf, err := NewConf(nil)
152+
if err != nil {
153+
return errors.Errorf("could not create new configuration: %w", err)
154+
}
155+
156+
metadataTarget, err := NewTarget(conf)
157+
if err != nil {
158+
return errors.Errorf("could not create git clone target: %w", err)
159+
}
160+
161+
opts := append(make([]component.RunnerOption, 0), component.RunnerWithComponentName("image-get"))
162+
163+
if err := component.RunTarget(
164+
ctx,
165+
metadataTarget,
166+
opts...,
167+
); err != nil {
168+
return errors.Errorf("could not run target: %w", err)
169+
}
170+
171+
return nil
172+
}
173+
174+
func NewTarget(conf *Conf) (*imageMetadataWriterTarget, error) {
175+
if conf == nil {
176+
return nil, errors.New("conf cannot be nil")
177+
}
178+
179+
return &imageMetadataWriterTarget{
180+
conf: conf,
181+
}, nil
182+
}
183+
184+
func (t *imageMetadataWriterTarget) Prepare(ctx context.Context) error {
185+
if t.conf == nil {
186+
return errors.New("conf cannot be nil")
187+
}
188+
189+
if err := WriteTargetMetadata(t.conf); err != nil {
190+
return errors.Errorf("could not write target metadata: %w", err)
191+
}
192+
193+
return nil
194+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package main
2+
3+
import (
4+
"os"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
)
9+
10+
func TestWriteTargetMetadata(t *testing.T) {
11+
tests := []struct {
12+
name string
13+
conf Conf
14+
expectedError bool
15+
expectedOutput string
16+
}{
17+
{
18+
name: "Valid image with full URL and version tag",
19+
conf: Conf{
20+
TargetMetadataPath: "",
21+
Image: "repo.example.com/project/image:1.0.0",
22+
},
23+
expectedError: false,
24+
expectedOutput: `{"targetType":"TARGET_TYPE_CONTAINER_IMAGE","ociPackageMetadata":{"packageUrl":"pkg:docker/repo.example.com/project%[email protected]","tag":"1.0.0"}}`,
25+
},
26+
{
27+
name: "Valid image with no tag (latest assumed)",
28+
conf: Conf{
29+
TargetMetadataPath: "",
30+
Image: "repo.example.com/project/image",
31+
},
32+
expectedOutput: `{"targetType":"TARGET_TYPE_CONTAINER_IMAGE","ociPackageMetadata":{"packageUrl":"pkg:docker/repo.example.com/project%2Fimage@latest","tag":"latest"}}`,
33+
},
34+
{
35+
name: "Valid image with latest tag",
36+
conf: Conf{
37+
TargetMetadataPath: "",
38+
Image: "repo.example.com/project/image:latest",
39+
},
40+
expectedError: false,
41+
expectedOutput: `{"targetType":"TARGET_TYPE_CONTAINER_IMAGE","ociPackageMetadata":{"packageUrl":"pkg:docker/repo.example.com/project%2Fimage@latest","tag":"latest"}}`,
42+
},
43+
{
44+
name: "Valid image with SHA tag",
45+
conf: Conf{
46+
TargetMetadataPath: "",
47+
Image: "repo.example.com/project/ubuntu@sha256:98706f0f213dbd440021993a82d2f70451a73698315370ae8615cc468ac06624",
48+
},
49+
expectedError: false,
50+
expectedOutput: `{"targetType":"TARGET_TYPE_CONTAINER_IMAGE","ociPackageMetadata":{"packageUrl":"pkg:docker/repo.example.com/project%2Fubuntu?digest=sha256%3A98706f0f213dbd440021993a82d2f70451a73698315370ae8615cc468ac06624"}}`,
51+
},
52+
{
53+
name: "Valid image with SHA AND tag",
54+
conf: Conf{
55+
TargetMetadataPath: "",
56+
Image: "repo.example.com/project/ubuntu:18.04@sha256:98706f0f213dbd440021993a82d2f70451a73698315370ae8615cc468ac06624",
57+
},
58+
expectedError: false,
59+
expectedOutput: `{"targetType":"TARGET_TYPE_CONTAINER_IMAGE","ociPackageMetadata":{"packageUrl":"pkg:docker/repo.example.com/project%2Fubuntu?digest=sha256%3A98706f0f213dbd440021993a82d2f70451a73698315370ae8615cc468ac06624"}}`,
60+
},
61+
{
62+
name: "Invalid image with empty string",
63+
conf: Conf{
64+
TargetMetadataPath: "",
65+
Image: "",
66+
},
67+
expectedError: true,
68+
},
69+
{
70+
name: "Invalid image with malformed tag",
71+
conf: Conf{
72+
TargetMetadataPath: "",
73+
Image: "repo.example.com/project/image:tag:extra",
74+
},
75+
expectedError: true,
76+
},
77+
}
78+
79+
for _, tt := range tests {
80+
t.Run(tt.name, func(t *testing.T) {
81+
// Create a temporary file for testing
82+
tempFile, err := os.CreateTemp("", "target.json")
83+
if err != nil {
84+
t.Fatalf("failed to create temp file: %v", err)
85+
}
86+
defer os.Remove(tempFile.Name())
87+
88+
// Update the conf to use the temp file path
89+
tt.conf.TargetMetadataPath = tempFile.Name()
90+
91+
err = WriteTargetMetadata(tt.conf)
92+
if tt.expectedError {
93+
assert.Error(t, err)
94+
} else {
95+
assert.NoError(t, err)
96+
97+
// Read the file and verify its contents
98+
data, readErr := os.ReadFile(tempFile.Name())
99+
assert.NoError(t, readErr)
100+
assert.JSONEq(t, tt.expectedOutput, string(data))
101+
}
102+
})
103+
}
104+
}

components/targets/image-get/metadata-writer/go.mod

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,55 @@ require (
1414
)
1515

1616
require (
17+
ariga.io/atlas v0.29.0 // indirect
18+
github.com/Masterminds/goutils v1.1.1 // indirect
19+
github.com/Masterminds/semver/v3 v3.2.0 // indirect
20+
github.com/Masterminds/sprig/v3 v3.2.3 // indirect
21+
github.com/abice/go-enum v0.6.0 // indirect
22+
github.com/agext/levenshtein v1.2.3 // indirect
23+
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
24+
github.com/bmatcuk/doublestar v1.3.4 // indirect
25+
github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect
1726
github.com/davecgh/go-spew v1.1.1 // indirect
27+
github.com/go-openapi/inflect v0.19.0 // indirect
28+
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
29+
github.com/golang/mock v1.6.0 // indirect
30+
github.com/google/go-cmp v0.6.0 // indirect
31+
github.com/google/uuid v1.6.0 // indirect
32+
github.com/hashicorp/hcl/v2 v2.18.1 // indirect
33+
github.com/huandu/xstrings v1.3.3 // indirect
34+
github.com/imdario/mergo v0.3.13 // indirect
35+
github.com/jackc/pgpassfile v1.0.0 // indirect
36+
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
37+
github.com/jackc/pgx/v5 v5.6.0 // indirect
38+
github.com/jonboulle/clockwork v0.4.0 // indirect
39+
github.com/labstack/gommon v0.4.1 // indirect
40+
github.com/mattn/go-colorable v0.1.13 // indirect
41+
github.com/mattn/go-isatty v0.0.20 // indirect
42+
github.com/mattn/go-sqlite3 v1.14.24 // indirect
43+
github.com/mattn/goveralls v0.0.12 // indirect
44+
github.com/mitchellh/copystructure v1.2.0 // indirect
45+
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
46+
github.com/mitchellh/reflectwalk v1.0.2 // indirect
1847
github.com/opencontainers/go-digest v1.0.0 // indirect
1948
github.com/pmezard/go-difflib v1.0.0 // indirect
49+
github.com/russross/blackfriday/v2 v2.1.0 // indirect
50+
github.com/shopspring/decimal v1.2.0 // indirect
51+
github.com/spf13/cast v1.3.1 // indirect
52+
github.com/sqlc-dev/sqlc v1.27.0 // indirect
53+
github.com/urfave/cli/v2 v2.26.0 // indirect
54+
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
55+
github.com/zclconf/go-cty v1.14.4 // indirect
56+
go.uber.org/mock v0.5.0 // indirect
57+
golang.org/x/crypto v0.32.0 // indirect
58+
golang.org/x/mod v0.22.0 // indirect
59+
golang.org/x/net v0.34.0 // indirect
60+
golang.org/x/sync v0.10.0 // indirect
61+
golang.org/x/sys v0.29.0 // indirect
62+
golang.org/x/text v0.21.0 // indirect
63+
golang.org/x/tools v0.29.0 // indirect
64+
golang.org/x/tools/cmd/cover v0.1.0-deprecated // indirect
65+
google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 // indirect
66+
google.golang.org/grpc v1.65.0 // indirect
2067
gopkg.in/yaml.v3 v3.0.1 // indirect
2168
)

0 commit comments

Comments
 (0)