diff --git a/recommend/recommend.go b/recommend/recommend.go index 9ab1ed66..01972824 100644 --- a/recommend/recommend.go +++ b/recommend/recommend.go @@ -145,13 +145,12 @@ func Recommend(c *k8s.Client, o common.Options, policyGenerators ...engines.Engi labelMap := labelArrayToLabelMap(o.Labels) if len(o.Images) == 0 { - // recommendation based on k8s manifest + // Recommendation based on K8s manifest dps, err := c.K8sClientset.AppsV1().Deployments(o.Namespace).List(context.TODO(), v1.ListOptions{}) if err != nil { return err } for _, dp := range dps.Items { - if !matchLabels(labelMap, dp.Spec.Template.Labels) { continue } @@ -183,7 +182,6 @@ func Recommend(c *k8s.Client, o common.Options, policyGenerators ...engines.Engi o.Tags = unique(o.Tags) options = o - reg := registry.New(o.Config) if err = createOutDir(o.OutDir); err != nil { return err @@ -205,10 +203,23 @@ func Recommend(c *k8s.Client, o common.Options, policyGenerators ...engines.Engi Image: i, Deployment: deployment.Name, } - reg.Analyze(&img) + + // Update: Pull the image using the OCI registry and get file and directory lists + reg := registry.New(i, []string{}, "", "") // Use the actual image name here + files, directories, err := reg.Pull(o.OutDir) + if err != nil { + log.WithError(err).Error("failed to pull the image from registry") + return err + } + + log.Infof("Pulled files: %v", files) + log.Infof("Pulled directories: %v", directories) + if policyMap, msMap, err = gen.Scan(&img, o); err != nil { log.WithError(err).Error("policy generator scan failed") } + + // Process and write the policies based on the file and directory information writePolicyFile(policyMap, msMap) if err := report.SectEnd(); err != nil { log.WithError(err).Error("report section end failed") @@ -221,3 +232,4 @@ func Recommend(c *k8s.Client, o common.Options, policyGenerators ...engines.Engi return nil } + diff --git a/recommend/registry/const.go b/recommend/registry/const.go new file mode 100644 index 00000000..a867dbf3 --- /dev/null +++ b/recommend/registry/const.go @@ -0,0 +1,20 @@ +package registry + +import "errors" + +const ( + DefaultTempDirPrefix = "tmp" + DefaultRegistry = "docker.io" + DefaultTag = "latest" + + artifactType = "application/vnd.cncf.kubearmor.config.v1+json" + mediaType = "application/vnd.cncf.kubearmor.policy.layer.v1.yaml" + + // Connect to remote repository via HTTP instead of HTTPS when + // set to "true". + EnvOCIInsecure = "KARMOR_OCI_TLS_INSECURE" +) + +var ( + ErrInvalidImage = errors.New("invalid image path") +) \ No newline at end of file diff --git a/recommend/registry/registry.go b/recommend/registry/registry.go index 6fd9b475..97b202b8 100644 --- a/recommend/registry/registry.go +++ b/recommend/registry/registry.go @@ -5,320 +5,223 @@ package registry import ( - "archive/tar" - "bufio" "context" _ "embed" // need for embedding - "encoding/base64" + "encoding/json" "fmt" - "io" - "math/rand" + "os" "path/filepath" - "strings" - - "github.com/docker/docker/client" - "github.com/docker/docker/pkg/jsonmessage" - image "github.com/kubearmor/kubearmor-client/recommend/image" - "github.com/moby/term" - dockerTypes "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/registry" - kg "github.com/kubearmor/KubeArmor/KubeArmor/log" - "github.com/kubearmor/kubearmor-client/hacks" - log "github.com/sirupsen/logrus" + // image "github.com/kubearmor/kubearmor-client/recommend/image" + // kg "github.com/kubearmor/KubeArmor/KubeArmor/log" + // log "github.com/sirupsen/logrus" + v1 "github.com/opencontainers/image-spec/specs-go/v1" + "oras.land/oras-go/v2" + "oras.land/oras-go/v2/content" + "oras.land/oras-go/v2/content/file" + "oras.land/oras-go/v2/registry/remote" + "oras.land/oras-go/v2/registry/remote/auth" + credentials "oras.land/oras-go/v2/registry/remote/credentials" + "oras.land/oras-go/v2/registry/remote/retry" ) const karmorTempDirPattern = "karmor" // Scanner represents a utility for scanning Docker registries -type Scanner struct { - authConfiguration authConfigurations - cli *client.Client // docker client - cache map[string]image.Info -} -// authConfigurations contains the configuration information's -type authConfigurations struct { - configPath string // stores path of docker config.json - authCreds []string -} +type OCIRegistry struct { + // Image is OCI image. Must follow the OCI image standard. + Image string -func getAuthStr(u, p string) string { - if u == "" || p == "" { - return "" - } + // Files are absolute artifact paths. + Files []string - encodedJSON, err := json.Marshal(registry.AuthConfig{ - Username: u, - Password: p, - }) - if err != nil { - log.WithError(err).Fatal("failed to marshal credentials") - } + // Credentials encapsulates registry authentication details. + Credentials struct { + // Username for registry. + Username string - return base64.URLEncoding.EncodeToString(encodedJSON) + // Password for registry. + Password string + } } -func (r *Scanner) loadDockerAuthConfigs() { - r.authConfiguration.authCreds = append(r.authConfiguration.authCreds, fmt.Sprintf("%s:%s", os.Getenv("DOCKER_USERNAME"), os.Getenv("DOCKER_PASSWORD"))) - if r.authConfiguration.configPath != "" { - data, err := os.ReadFile(filepath.Clean(r.authConfiguration.configPath)) - if err != nil { - return - } - - confsWrapper := struct { - Auths map[string]registry.AuthConfig `json:"auths"` - }{} - err = json.Unmarshal(data, &confsWrapper) - if err != nil { - return - } - - for _, conf := range confsWrapper.Auths { - data, _ := base64.StdEncoding.DecodeString(conf.Auth) - userPass := strings.SplitN(string(data), ":", 2) - r.authConfiguration.authCreds = append(r.authConfiguration.authCreds, getAuthStr(userPass[0], userPass[1])) - } +func New(img string, files []string, user string, passwd string) *OCIRegistry { + return &OCIRegistry{ + Image: img, + Files: files, + Credentials: struct { + Username string + Password string + }{ + Username: user, + Password: passwd, + }, } } // New creates and initializes a new instance of the Scanner -func New(dockerConfigPath string) *Scanner { - var err error - scanner := Scanner{ - authConfiguration: authConfigurations{ - configPath: dockerConfigPath, - }, - cache: make(map[string]image.Info), +func (o *OCIRegistry) Pull(output string) (files []string, directories []string, err error) { + ctx := context.Background() + tempDir, err, rmdir := MakeTemporaryDir("") + fmt.Printf("tmpdir: %v", tempDir) + if err != nil { + return nil, nil, err } + defer rmdir() + // 0. Create a file store + store, err := file.New(tempDir) if err != nil { - log.WithError(err).Error("could not create temp dir") + return nil, nil, err } + defer store.Close() - scanner.cli, err = client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) + reg, repoPath, tag, err := getRegRepoTag(o.Image) + repoPath = "docker.io/library/ubuntu" + fmt.Printf("This is the repoPath %v \n",repoPath) + fmt.Println(reg) if err != nil { - log.WithError(err).Fatal("could not create new docker client") + return nil, nil, err } - scanner.loadDockerAuthConfigs() - - return &scanner -} -// Analyze performs analysis and caching of image information using the Scanner -func (r *Scanner) Analyze(img *image.Info) { - if val, ok := r.cache[img.Name]; ok { - log.WithFields(log.Fields{ - "image": img.Name, - }).Infof("Image already scanned in this session, using cached informations for image") - img.Arch = val.Arch - img.DirList = val.DirList - img.FileList = val.FileList - img.Distro = val.Distro - img.Labels = val.Labels - img.OS = val.OS - img.RepoTags = val.RepoTags - return - } - tmpDir, err := os.MkdirTemp("", karmorTempDirPattern) + // 1. Connect to a remote repository + repo, err := remote.NewRepository(repoPath) if err != nil { - log.WithError(err).Error("could not create temp dir") - } - defer func() { - err = os.RemoveAll(tmpDir) - if err != nil { - log.WithError(err).Error("failed to remove cache files") + return nil, nil, err + } + if v := os.Getenv(EnvOCIInsecure); v == "true" { + repo.PlainHTTP = true + } + if o.Credentials.Username != "" { + fmt.Printf("Using static credentials: %s\n", o.Credentials.Username) + repo.Client = &auth.Client{ + Client: retry.DefaultClient, + Cache: auth.DefaultCache, + Credential: auth.StaticCredential(reg, auth.Credential{ + Username: o.Credentials.Username, + Password: o.Credentials.Password, + }), } - }() - img.TempDir = tmpDir - err = r.pullImage(img.Name) - if err != nil { - log.Warn("Failed to pull image. Dumping generic policies.") - img.OS = "linux" - img.RepoTags = append(img.RepoTags, img.Name) } else { - tarname := saveImageToTar(img.Name, r.cli, tmpDir) - img.FileList, img.DirList = extractTar(tarname, tmpDir) - img.GetImageInfo() - } - - r.cache[img.Name] = *img -} - -// The randomizer used in this function is not used for any cryptographic -// operation and hence safe to use. -func randString(n int) string { - var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") - b := make([]rune, n) - for i := range b { - b[i] = letterRunes[rand.Intn(len(letterRunes))] // #nosec - } - return string(b) -} + // Get credentials from the Docker credential store + fmt.Println("hello") + storeOpts := credentials.StoreOptions{} // Adjust as per the deprecation notice + credStore, err := credentials.NewStoreFromDocker(storeOpts) + if err != nil { + return nil, nil, fmt.Errorf("failed to create credential store: %w", err) + } -func (r *Scanner) pullImage(imageName string) (err error) { - log.WithFields(log.Fields{ - "image": imageName, - }).Info("pulling image") + // Retrieve the credentials and print them to debug + creds, err := credStore.Get(ctx, reg) + if err != nil { + return nil, nil, fmt.Errorf("failed to get credentials for %s: %w", reg, err) + } - var out io.ReadCloser + fmt.Printf("Retrieved credentials from Docker store: %v\n", creds) - for _, cred := range r.authConfiguration.authCreds { - out, err = r.cli.ImagePull(context.Background(), imageName, - dockerTypes.ImagePullOptions{ - RegistryAuth: cred, - }) - if err == nil { - break + repo.Client = &auth.Client{ + Client: retry.DefaultClient, + Cache: auth.DefaultCache, + Credential: credentials.Credential(credStore), } } + + // 2. Copy from the remote repository to the file store + fmt.Println("reached second") + fmt.Printf("the tag is %v and the another target is %v \n", tag ,tag ) + fmt.Printf("the repo is %v \n", *repo) + fmt.Printf("the store is %v \n", store) + fmt.Printf("the options is %v \n", oras.DefaultCopyOptions) + copyOptions := oras.DefaultCopyOptions + + + fmt.Printf("max data bytes :%v", copyOptions.MaxMetadataBytes ) + manifestDescriptor, err := oras.Copy(ctx, repo, tag, store, tag, copyOptions) if err != nil { - return err + fmt.Println("reached second error") + return nil, nil, err } - defer func() { - if err := out.Close(); err != nil { - kg.Warnf("Error closing io stream %s\n", err) - } - }() - termFd, isTerm := term.GetFdInfo(os.Stderr) - err = jsonmessage.DisplayJSONMessagesStream(out, os.Stderr, termFd, isTerm, nil) + fmt.Println("reached third") + // 3. Fetch from OCI layout store + fetched, err := content.FetchAll(ctx, store, manifestDescriptor) if err != nil { - log.WithError(err).Error("could not display json") - } - - return -} - -// Sanitize archive file pathing from "G305: Zip Slip vulnerability" -func sanitizeArchivePath(d, t string) (v string, err error) { - v = filepath.Join(d, t) - if strings.HasPrefix(v, filepath.Clean(d)) { - return v, nil + return nil, nil, err } - return "", fmt.Errorf("%s: %s", "content filepath is tainted", t) -} - -func extractTar(tarname string, tempDir string) ([]string, []string) { - var fl []string - var dl []string - - f, err := os.Open(filepath.Clean(tarname)) + manifest := &v1.Manifest{} + err = json.Unmarshal(fetched, manifest) if err != nil { - log.WithError(err).WithFields(log.Fields{ - "tar": tarname, - }).Fatal("os create failed") - } - defer hacks.CloseCheckErr(f, tarname) - if isTarFile(f) { - _, err := f.Seek(0, 0) - if err != nil { - log.WithError(err).WithFields(log.Fields{ - "tar": tarname, - }).Fatal("Failed to seek to the beginning of the file") + return nil, nil, err + } + fmt.Println("reached four") + // 4. Iterate over layers and extract files + var layerFiles []string + for _, layer := range manifest.Layers { + if layer.MediaType != mediaType { + continue } - tr := tar.NewReader(bufio.NewReader(f)) - for { - hdr, err := tr.Next() - if err == io.EOF { - break // End of archive - } - if err != nil { - log.WithError(err).Error("tar next failed") - return nil, nil - } - - tgt, err := sanitizeArchivePath(tempDir, hdr.Name) - if err != nil { - log.WithError(err).WithFields(log.Fields{ - "file": hdr.Name, - }).Error("ignoring file since it could not be sanitized") - continue - } - - switch hdr.Typeflag { - case tar.TypeDir: - if _, err := os.Stat(tgt); err != nil { - if err := os.MkdirAll(tgt, 0750); err != nil { - log.WithError(err).WithFields(log.Fields{ - "target": tgt, - }).Fatal("tar mkdirall") - } - } - dl = append(dl, tgt) - case tar.TypeReg: - f, err := os.OpenFile(filepath.Clean(tgt), os.O_CREATE|os.O_RDWR, os.FileMode(hdr.Mode)) - if err != nil { - log.WithError(err).WithFields(log.Fields{ - "target": tgt, - }).Error("tar open file") - } else { - - // copy over contents - if _, err := io.CopyN(f, tr, 2e+9 /*2GB*/); err != io.EOF { - log.WithError(err).WithFields(log.Fields{ - "target": tgt, - }).Fatal("tar io.Copy()") - } - } - hacks.CloseCheckErr(f, tgt) - if strings.HasSuffix(tgt, "layer.tar") { - ifl, idl := extractTar(tgt, tempDir) - fl = append(fl, ifl...) - dl = append(dl, idl...) - } else if strings.HasPrefix(hdr.Name, "blobs/") { - ifl, idl := extractTar(tgt, tempDir) - fl = append(fl, ifl...) - dl = append(dl, idl...) - - } else { - fl = append(fl, tgt) - } - } + if title, ok := layer.Annotations[v1.AnnotationTitle]; ok { + layerFiles = append(layerFiles, filepath.Join(tempDir, title)) } - } else { - log.WithFields(log.Fields{ - "file": tarname, - }).Error("Not a valid tar file") } - return fl, dl -} -func isTarFile(f *os.File) bool { - tr := tar.NewReader(bufio.NewReader(f)) - _, err := tr.Next() - return err == nil -} + if output == "" { + output, err = os.Getwd() + if err != nil { + return nil, nil, err + } + } + outputStat, err := os.Stat(output) + if err != nil { + return nil, nil, err + } + if !outputStat.IsDir() { + return nil, nil, fmt.Errorf("%s is not a directory", output) + } -func saveImageToTar(imageName string, cli *client.Client, tempDir string) string { - imgdata, err := cli.ImageSave(context.Background(), []string{imageName}) + // 5. Copy files to the output directory + dsts, err := CopyFiles(layerFiles, output) if err != nil { - log.WithError(err).Fatal("could not save image") + return nil, nil, err } - defer func() { - if err := imgdata.Close(); err != nil { - kg.Warnf("Error closing io stream %s\n", err) - } - }() + o.Files = dsts - tarname := filepath.Join(tempDir, randString(8)+".tar") + // 6. Walk through the directory to categorize files and directories + err = filepath.Walk(output, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() { + directories = append(directories, path) + } else { + files = append(files, path) + } + return nil + }) - f, err := os.Create(filepath.Clean(tarname)) if err != nil { - log.WithError(err).Fatal("os create failed") + return nil, nil, err } - if _, err := io.CopyN(bufio.NewWriter(f), imgdata, 5e+9 /*5GB*/); err != io.EOF { - log.WithError(err).WithFields(log.Fields{ - "tar": tarname, - }).Fatal("io.CopyN() failed") - } - hacks.CloseCheckErr(f, tarname) - log.WithFields(log.Fields{ - "tar": tarname, - }).Info("dumped image to tar") - return tarname + return files, directories, nil } + +// // Analyze performs analysis and caching of image information using the Scanner +// func (r *Scanner) Analyze(img *image.Info) { +// if val, ok := r.cache[img.Name]; ok { +// log.WithFields(log.Fields{ +// "image": img.Name, +// }).Infof("Image already scanned in this session, using cached informations for image") +// img.Arch = val.Arch +// img.DirList = val.DirList +// img.FileList = val.FileList +// img.Distro = val.Distro +// img.Labels = val.Labels +// img.OS = val.OS +// img.RepoTags = val.RepoTags +// return +// } +// tmpDir, err := os.MkdirTemp("", karmorTempDirPattern) diff --git a/recommend/registry/util.go b/recommend/registry/util.go new file mode 100644 index 00000000..20a7898f --- /dev/null +++ b/recommend/registry/util.go @@ -0,0 +1,95 @@ +package registry + +import ( + "fmt" + "io" + "os" + "path/filepath" + "strings" + + "github.com/distribution/distribution/v3/reference" + "github.com/rs/zerolog/log" +) + +// getRegRepoTag return registry, repository path and tag out of image +// string. It returns default registry and tag if not found. +func getRegRepoTag(image string) (string, string, string, error) { + var reg, repo, tag string + matches := reference.ReferenceRegexp.FindStringSubmatch(image) + if matches == nil { + return "", "", "", ErrInvalidImage + } + tag = matches[2] + repo = matches[1] + // TODO(akshay): Improve: Fetch reg from repo + repoSplit := strings.SplitN(repo, "/", 2) + if strings.ContainsAny(repoSplit[0], ".:") { + reg = repoSplit[0] + } + if reg == "" { + reg = DefaultRegistry + repo = strings.Join([]string{reg, repo}, "/") + } + if tag == "" { + tag = DefaultTag + } + return reg, repo, tag, nil +} + +// MakeTemporaryDir creates a temporary directory and returns the path +// of the new directory. +func MakeTemporaryDir(pfx string) (string, error, func()) { + if pfx == "" { + pfx = DefaultTempDirPrefix + } + + dir, err := os.MkdirTemp(os.TempDir(), pfx) + if err != nil { + return "", err, nil + } + return dir, nil, func() { + err := os.RemoveAll(dir) + if err != nil { + log.Warn().Msgf("unable to clean temp directory: %s", err) + } + } +} + +// CopyFiles copies srcFiles to dst directory. It returns path of +// copied files. +func CopyFiles(srcFiles []string, dst string) ([]string, error) { + copied := make([]string, 0, len(srcFiles)) + for _, file := range srcFiles { + file, err := filepath.Abs(file) + if err != nil { + return nil, err + } + stat, err := os.Stat(file) + if err != nil { + return nil, err + } + if !stat.Mode().IsRegular() { + return nil, fmt.Errorf("%s is not a regular file", file) + } + + sp, err := os.Open(file) + if err != nil { + return nil, err + } + defer sp.Close() + + new := filepath.Join(dst, filepath.Base(file)) + dp, err := os.Create(new) + if err != nil { + return nil, err + } + defer dp.Close() + + _, err = io.Copy(dp, sp) + if err != nil { + return nil, err + } + copied = append(copied, new) + } + return copied, nil +} \ No newline at end of file