Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package arkade for openSUSE:Factory checked 
in at 2026-06-23 17:40:42
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/arkade (Old)
 and      /work/SRC/openSUSE:Factory/.arkade.new.1956 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "arkade"

Tue Jun 23 17:40:42 2026 rev:82 rq:1361250 version:0.11.102

Changes:
--------
--- /work/SRC/openSUSE:Factory/arkade/arkade.changes    2026-06-18 
18:44:53.439723590 +0200
+++ /work/SRC/openSUSE:Factory/.arkade.new.1956/arkade.changes  2026-06-23 
17:43:06.142972855 +0200
@@ -1,0 +2,14 @@
+Tue Jun 23 04:59:59 UTC 2026 - Johannes Kastl 
<[email protected]>
+
+- Update to version 0.11.102:
+  * Add pluto CLI to find deprecated K8s APIs
+
+-------------------------------------------------------------------
+Tue Jun 23 04:56:18 UTC 2026 - Johannes Kastl 
<[email protected]>
+
+- Update to version 0.11.101:
+  * Make symlink extraction in UntarNested conditional
+  * Fix symlink path-traversal escape in UntarNested
+  * Remove stray file
+
+-------------------------------------------------------------------

Old:
----
  arkade-0.11.100.obscpio

New:
----
  arkade-0.11.102.obscpio

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ arkade.spec ++++++
--- /var/tmp/diff_new_pack.mq8tIL/_old  2026-06-23 17:43:08.995072833 +0200
+++ /var/tmp/diff_new_pack.mq8tIL/_new  2026-06-23 17:43:09.007073254 +0200
@@ -17,7 +17,7 @@
 
 
 Name:           arkade
-Version:        0.11.100
+Version:        0.11.102
 Release:        0
 Summary:        Open Source Kubernetes Marketplace
 License:        Apache-2.0

++++++ _service ++++++
--- /var/tmp/diff_new_pack.mq8tIL/_old  2026-06-23 17:43:09.495090361 +0200
+++ /var/tmp/diff_new_pack.mq8tIL/_new  2026-06-23 17:43:09.535091763 +0200
@@ -3,7 +3,7 @@
     <param name="url">https://github.com/alexellis/arkade</param>
     <param name="scm">git</param>
     <param name="exclude">.git</param>
-    <param name="revision">0.11.100</param>
+    <param name="revision">0.11.102</param>
     <param name="versionformat">@PARENT_TAG@</param>
     <param name="versionrewrite-pattern">v(.*)</param>
     <param name="changesgenerate">enable</param>

++++++ _servicedata ++++++
--- /var/tmp/diff_new_pack.mq8tIL/_old  2026-06-23 17:43:09.735098774 +0200
+++ /var/tmp/diff_new_pack.mq8tIL/_new  2026-06-23 17:43:09.755099475 +0200
@@ -1,6 +1,6 @@
 <servicedata>
 <service name="tar_scm">
                 <param name="url">https://github.com/alexellis/arkade</param>
-              <param 
name="changesrevision">db87cbea02bb931f4277719629597a627c4d4b5e</param></service></servicedata>
+              <param 
name="changesrevision">37af31c7b58de8f16a10067051e3bbe7ecb6aa79</param></service></servicedata>
 (No newline at EOF)
 

++++++ arkade-0.11.100.obscpio -> arkade-0.11.102.obscpio ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/arkade-0.11.100/README.md 
new/arkade-0.11.102/README.md
--- old/arkade-0.11.100/README.md       2026-06-16 19:06:19.000000000 +0200
+++ new/arkade-0.11.102/README.md       2026-06-22 16:31:10.000000000 +0200
@@ -918,6 +918,7 @@
 | [osm](https://github.com/openservicemesh/osm)                                
| Open Service Mesh uniformly manages, secures, and gets out-of-the-box 
observability features.                                                         
            |
 | [pack](https://github.com/buildpacks/pack)                                   
| Build apps using Cloud Native Buildpacks.                                     
                                                                                
    |
 | [packer](https://github.com/hashicorp/packer)                                
| Build identical machine images for multiple platforms from a single source 
configuration.                                                                  
       |
+| [pluto](https://github.com/FairwindsOps/pluto)                               
| Find deprecated Kubernetes apiVersions in code repositories and helm 
releases.                                                                       
             |
 | [polaris](https://github.com/FairwindsOps/polaris)                           
| Run checks to ensure Kubernetes pods and controllers are configured using 
best practices.                                                                 
        |
 | [popeye](https://github.com/derailed/popeye)                                 
| Scans live Kubernetes cluster and reports potential issues with deployed 
resources and configurations.                                                   
         |
 | [porter](https://github.com/getporter/porter)                                
| With Porter you can package your application artifact, tools, etc. as a 
bundle that can distribute and install.                                         
          |
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/arkade-0.11.100/cmd/oci/install.go 
new/arkade-0.11.102/cmd/oci/install.go
--- old/arkade-0.11.100/cmd/oci/install.go      2026-06-16 19:06:19.000000000 
+0200
+++ new/arkade-0.11.102/cmd/oci/install.go      2026-06-22 16:31:10.000000000 
+0200
@@ -56,6 +56,7 @@
 
        command.Flags().BoolP("gzipped", "g", false, "Is this a gzipped 
tarball?")
        command.Flags().Bool("quiet", false, "Suppress progress output")
+       command.Flags().Bool("symlink", false, "Write symlinks when unpacking 
OCI image, only use with trusted sources")
 
        // Hide the deprecated --path flag
        command.Flags().MarkHidden("path")
@@ -68,6 +69,7 @@
                version, _ := cmd.Flags().GetString("version")
                gzipped, _ := cmd.Flags().GetBool("gzipped")
                quiet, _ := cmd.Flags().GetBool("quiet")
+               allowSymlinks, _ := cmd.Flags().GetBool("symlink")
                showProgress, _ := cmd.Flags().GetBool("progress")
 
                if len(args) < 1 {
@@ -260,7 +262,7 @@
                                // When the alt-screen is active, suppress 
UntarNested's
                                // per-file logging so it doesn't corrupt the 
live frame.
                                untarQuiet := quiet || (tty && renderLive)
-                               if uErr := archive.UntarNested(tarFile, 
installPath, gzipped, untarQuiet); uErr != nil {
+                               if uErr := archive.UntarNested(tarFile, 
installPath, gzipped, untarQuiet, allowSymlinks); uErr != nil {
                                        workErr = fmt.Errorf("failed to untar 
%s: %w", tempFile.Name(), uErr)
                                }
                        }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/arkade-0.11.100/cmd/system/actions_runner.go 
new/arkade-0.11.102/cmd/system/actions_runner.go
--- old/arkade-0.11.100/cmd/system/actions_runner.go    2026-06-16 
19:06:19.000000000 +0200
+++ new/arkade-0.11.102/cmd/system/actions_runner.go    2026-06-22 
16:31:10.000000000 +0200
@@ -100,7 +100,7 @@
                fmt.Printf("Unpacking Actions Runner to: %s\n", 
path.Join(installPath, "actions-runner"))
 
                if err := spinWhile("Unpacking Actions Runner", func() error {
-                       return archive.UntarNested(f, installPath, true, true)
+                       return archive.UntarNested(f, installPath, true, true, 
true)
                }); err != nil {
                        return err
                }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/arkade-0.11.100/cmd/system/containerd.go 
new/arkade-0.11.102/cmd/system/containerd.go
--- old/arkade-0.11.100/cmd/system/containerd.go        2026-06-16 
19:06:19.000000000 +0200
+++ new/arkade-0.11.102/cmd/system/containerd.go        2026-06-22 
16:31:10.000000000 +0200
@@ -120,7 +120,7 @@
                tempDirName := os.TempDir() + "/containerd"
 
                if err := spinWhile("Unpacking containerd", func() error {
-                       return archive.UntarNested(f, tempDirName, true, true)
+                       return archive.UntarNested(f, tempDirName, true, true, 
true)
                }); err != nil {
                        return err
                }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/arkade-0.11.100/cmd/system/go.go 
new/arkade-0.11.102/cmd/system/go.go
--- old/arkade-0.11.100/cmd/system/go.go        2026-06-16 19:06:19.000000000 
+0200
+++ new/arkade-0.11.102/cmd/system/go.go        2026-06-22 16:31:10.000000000 
+0200
@@ -96,7 +96,7 @@
                fmt.Printf("Unpacking Go to: %s\n", path.Join(installPath, 
"go"))
 
                if err := spinWhile("Unpacking Go", func() error {
-                       return archive.UntarNested(f, installPath, true, true)
+                       return archive.UntarNested(f, installPath, true, true, 
true)
                }); err != nil {
                        return err
                }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/arkade-0.11.100/cmd/system/node.go 
new/arkade-0.11.102/cmd/system/node.go
--- old/arkade-0.11.100/cmd/system/node.go      2026-06-16 19:06:19.000000000 
+0200
+++ new/arkade-0.11.102/cmd/system/node.go      2026-06-22 16:31:10.000000000 
+0200
@@ -149,7 +149,7 @@
                        fmt.Printf("Unpacking binaries to: %s\n", 
tempUnpackPath)
                }
                if err = spinWhile("Unpacking Node.js", func() error {
-                       return archive.UntarNested(f, tempUnpackPath, true, 
true)
+                       return archive.UntarNested(f, tempUnpackPath, true, 
true, true)
                }); err != nil {
                        return err
                }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/arkade-0.11.100/cmd/system/registry.go 
new/arkade-0.11.102/cmd/system/registry.go
--- old/arkade-0.11.100/cmd/system/registry.go  2026-06-16 19:06:19.000000000 
+0200
+++ new/arkade-0.11.102/cmd/system/registry.go  2026-06-22 16:31:10.000000000 
+0200
@@ -135,7 +135,7 @@
                        tempDirName := fmt.Sprintf("%s/%s", os.TempDir(), 
toolName)
                        defer os.RemoveAll(tempDirName)
                        if err := spinWhile("Unpacking "+toolName, func() error 
{
-                               return archive.UntarNested(f, tempDirName, 
true, true)
+                               return archive.UntarNested(f, tempDirName, 
true, true, true)
                        }); err != nil {
                                return err
                        }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/arkade-0.11.100/pkg/archive/untar.go 
new/arkade-0.11.102/pkg/archive/untar.go
--- old/arkade-0.11.100/pkg/archive/untar.go    2026-06-16 19:06:19.000000000 
+0200
+++ new/arkade-0.11.102/pkg/archive/untar.go    2026-06-22 16:31:10.000000000 
+0200
@@ -118,8 +118,13 @@
 }
 
 func validRelPath(p string) bool {
-       if p == "" || strings.Contains(p, `\`) || strings.HasPrefix(p, "/") || 
strings.Contains(p, "../") {
+       if p == "" || strings.Contains(p, `\`) || strings.HasPrefix(p, "/") {
                return false
        }
+       for _, part := range strings.Split(p, "/") {
+               if part == ".." {
+                       return false
+               }
+       }
        return true
 }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/arkade-0.11.100/pkg/archive/untar_nested.go 
new/arkade-0.11.102/pkg/archive/untar_nested.go
--- old/arkade-0.11.100/pkg/archive/untar_nested.go     2026-06-16 
19:06:19.000000000 +0200
+++ new/arkade-0.11.102/pkg/archive/untar_nested.go     2026-06-22 
16:31:10.000000000 +0200
@@ -8,18 +8,21 @@
        "log"
        "os"
        "path/filepath"
+       "strings"
        "time"
 )
 
 // UntarNested reads the gzip-compressed tar file from r and writes it into 
dir.
+// When allowSymlinks is false, any symlink entry in the archive causes an
+// error; when true, symlinks are extracted subject to containment checks.
 // Copyright 2017 The Go Authors. All rights reserved.
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
-func UntarNested(r io.Reader, dir string, gzipped, quiet bool) error {
-       return untarNested(r, dir, gzipped, quiet)
+func UntarNested(r io.Reader, dir string, gzipped, quiet, allowSymlinks bool) 
error {
+       return untarNested(r, dir, gzipped, quiet, allowSymlinks)
 }
 
-func untarNested(r io.Reader, dir string, gzipped, quiet bool) (err error) {
+func untarNested(r io.Reader, dir string, gzipped, quiet, allowSymlinks bool) 
(err error) {
        t0 := time.Now()
        nFiles := 0
        madeDir := map[string]bool{}
@@ -34,17 +37,28 @@
                }
        }()
 
-       reader := r
-
        if gzipped {
                zr, err := gzip.NewReader(r)
                if err != nil {
                        return fmt.Errorf("requires gzip-compressed body: %v", 
err)
                }
-               reader = zr
+               r = zr
+       }
+
+       if err := os.MkdirAll(dir, 0755); err != nil {
+               return err
+       }
+
+       // Resolve dir to its real path so containment checks are not confused 
by a
+       // symlinked install directory (e.g. /usr/local/bin on some systems).
+       resolvedDir, err := filepath.EvalSymlinks(dir)
+       if err != nil {
+               return err
        }
+       dir = resolvedDir
+       cleanDir := filepath.Clean(dir)
 
-       tr := tar.NewReader(reader)
+       tr := tar.NewReader(r)
        loggedChtimesError := false
        for {
                f, err := tr.Next()
@@ -68,16 +82,29 @@
                }
                switch {
                case mode.IsRegular():
-                       // Make the directory. This is redundant because it 
should
-                       // already be made by a directory entry in the tar
-                       // beforehand. Thus, don't check for errors; the next
-                       // write will fail with the same error.
-                       dir := filepath.Dir(abs)
-                       if !madeDir[dir] {
-                               if err := os.MkdirAll(filepath.Dir(abs), 0755); 
err != nil {
+                       parent := filepath.Dir(abs)
+                       if !madeDir[parent] {
+                               // Guard before MkdirAll: it follows a 
pre-existing symlink and
+                               // would otherwise create directories outside 
root.
+                               if err := 
assertExistingPrefixWithinRoot(cleanDir, parent); err != nil {
                                        return err
                                }
-                               madeDir[dir] = true
+                               if err := os.MkdirAll(parent, 0755); err != nil 
{
+                                       return err
+                               }
+                               madeDir[parent] = true
+                       }
+                       // Resolve the physical parent (containment already 
guaranteed above)
+                       // to locate the write, allowing write-through of 
internal symlinks.
+                       resolvedParent, err := filepath.EvalSymlinks(parent)
+                       if err != nil {
+                               return fmt.Errorf("cannot resolve parent of %s: 
%v", abs, err)
+                       }
+                       abs = filepath.Join(resolvedParent, filepath.Base(abs))
+                       // Don't write through a pre-existing symlink at the 
leaf; O_CREATE
+                       // would follow it outside root.
+                       if fi, err := os.Lstat(abs); err == nil && 
fi.Mode()&os.ModeSymlink != 0 {
+                               return fmt.Errorf("refusing to write through 
symlink %q", abs)
                        }
                        wf, err := os.OpenFile(abs, 
os.O_RDWR|os.O_CREATE|os.O_TRUNC, mode.Perm())
                        if err != nil {
@@ -114,13 +141,48 @@
                        }
                        nFiles++
                case mode.IsDir():
+                       // Guard before MkdirAll, as with regular files.
+                       if err := assertExistingPrefixWithinRoot(cleanDir, 
abs); err != nil {
+                               return err
+                       }
                        if err := os.MkdirAll(abs, 0755); err != nil {
                                return err
                        }
                        madeDir[abs] = true
-                       // Introduced via
-                       // https://github.com/alexellis/arkade/pull/675/files
-               case os.ModeSymlink != 0:
+               case mode.Type() == os.ModeSymlink:
+                       if !allowSymlinks {
+                               return fmt.Errorf("tar file entry %s is a 
symlink, but symlink extraction is disabled", f.Name)
+                       }
+                       parent := filepath.Dir(abs)
+                       if !madeDir[parent] {
+                               if err := 
assertExistingPrefixWithinRoot(cleanDir, parent); err != nil {
+                                       return err
+                               }
+                               if err := os.MkdirAll(parent, 0755); err != nil 
{
+                                       return err
+                               }
+                               madeDir[parent] = true
+                       }
+                       // Resolve the physical parent (containment already 
guaranteed above)
+                       // to locate where the symlink is created.
+                       resolvedParent, err := filepath.EvalSymlinks(parent)
+                       if err != nil {
+                               return fmt.Errorf("cannot resolve parent of %s: 
%v", abs, err)
+                       }
+                       abs = filepath.Join(resolvedParent, filepath.Base(abs))
+                       // Validate the link target stays within root. 
resolvedParent is
+                       // symlink-free, so this lexical check matches the 
physical location.
+                       target := f.Linkname
+                       if !filepath.IsAbs(target) {
+                               target = filepath.Join(resolvedParent, target)
+                       }
+                       if !inDir(filepath.Clean(target), cleanDir) {
+                               return fmt.Errorf("refusing symlink %q -> %q 
(escapes %q)", abs, f.Linkname, dir)
+                       }
+                       // ...and physically (pre-existing symlink in the 
target path).
+                       if err := assertExistingPrefixWithinRoot(cleanDir, 
target); err != nil {
+                               return err
+                       }
                        if err := os.Symlink(f.Linkname, abs); err != nil {
                                return err
                        }
@@ -130,3 +192,34 @@
        }
        return nil
 }
+
+// assertExistingPrefixWithinRoot resolves the longest existing ancestor of p 
and
+// returns an error if it does not stay within root.
+func assertExistingPrefixWithinRoot(root, p string) error {
+       cur := filepath.Clean(p)
+       for {
+               if _, err := os.Lstat(cur); err == nil {
+                       break
+               }
+               parent := filepath.Dir(cur)
+               if parent == cur {
+                       // Reached the filesystem root without an existing 
component.
+                       return nil
+               }
+               cur = parent
+       }
+       resolved, err := filepath.EvalSymlinks(cur)
+       if err != nil {
+               return err
+       }
+       if !inDir(filepath.Clean(resolved), root) {
+               return fmt.Errorf("refusing to create %q: existing path %q 
resolves to %q outside %q", p, cur, resolved, root)
+       }
+       return nil
+}
+
+// inDir reports whether path is equal to root or a direct descendant.
+// Both arguments must be clean paths (output of filepath.Clean or 
filepath.EvalSymlinks).
+func inDir(path, root string) bool {
+       return path == root || strings.HasPrefix(path, 
root+string(os.PathSeparator))
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/arkade-0.11.100/pkg/archive/untar_nested_test.go 
new/arkade-0.11.102/pkg/archive/untar_nested_test.go
--- old/arkade-0.11.100/pkg/archive/untar_nested_test.go        1970-01-01 
01:00:00.000000000 +0100
+++ new/arkade-0.11.102/pkg/archive/untar_nested_test.go        2026-06-22 
16:31:10.000000000 +0200
@@ -0,0 +1,417 @@
+package archive
+
+import (
+       "archive/tar"
+       "bytes"
+       "os"
+       "path/filepath"
+       "testing"
+)
+
+type tarEntry struct {
+       hdr  tar.Header
+       body []byte
+}
+
+func buildTar(t *testing.T, entries []tarEntry) []byte {
+       t.Helper()
+       var b bytes.Buffer
+       tw := tar.NewWriter(&b)
+       for _, e := range entries {
+               h := e.hdr
+               if len(e.body) > 0 {
+                       h.Size = int64(len(e.body))
+               }
+               if err := tw.WriteHeader(&h); err != nil {
+                       t.Fatalf("write header %q: %v", h.Name, err)
+               }
+               if len(e.body) > 0 {
+                       if _, err := tw.Write(e.body); err != nil {
+                               t.Fatalf("write body %q: %v", h.Name, err)
+                       }
+               }
+       }
+       if err := tw.Close(); err != nil {
+               t.Fatalf("close tar: %v", err)
+       }
+       return b.Bytes()
+}
+
+// A symlink whose target is an absolute path outside the install dir must be 
rejected,
+// even when a subsequent entry attempts to write through it.
+func Test_UntarNested_RejectsAbsoluteSymlinkWriteThrough(t *testing.T) {
+       baseDir, err := os.MkdirTemp("", "arkade-untar-*")
+       if err != nil {
+               t.Fatal(err)
+       }
+       defer os.RemoveAll(baseDir)
+
+       installDir := filepath.Join(baseDir, "install")
+       if err := os.MkdirAll(installDir, 0755); err != nil {
+               t.Fatal(err)
+       }
+       outsideDir := filepath.Join(baseDir, "outside")
+       if err := os.MkdirAll(outsideDir, 0755); err != nil {
+               t.Fatal(err)
+       }
+
+       data := buildTar(t, []tarEntry{
+               {hdr: tar.Header{Name: "escape-link", Typeflag: 
tar.TypeSymlink, Linkname: outsideDir, Mode: 0777}},
+               {hdr: tar.Header{Name: "escape-link/escape.txt", Typeflag: 
tar.TypeReg, Mode: 0644}, body: []byte("escaped\n")},
+       })
+
+       if err := UntarNested(bytes.NewReader(data), installDir, false, true, 
true); err == nil {
+               t.Fatal("want error, got nil")
+       }
+
+       if content, err := os.ReadFile(filepath.Join(outsideDir, 
"escape.txt")); err == nil {
+               t.Fatalf("file written outside install dir: content=%q", 
string(content))
+       }
+}
+
+// A symlink whose target is a relative path that escapes the install dir must 
be rejected,
+// even when a subsequent entry attempts to write through it.
+func Test_UntarNested_RejectsRelativeSymlinkWriteThrough(t *testing.T) {
+       baseDir, err := os.MkdirTemp("", "arkade-untar-*")
+       if err != nil {
+               t.Fatal(err)
+       }
+       defer os.RemoveAll(baseDir)
+
+       installDir := filepath.Join(baseDir, "install")
+       if err := os.MkdirAll(installDir, 0755); err != nil {
+               t.Fatal(err)
+       }
+       outsideDir := filepath.Join(baseDir, "outside")
+       if err := os.MkdirAll(outsideDir, 0755); err != nil {
+               t.Fatal(err)
+       }
+
+       data := buildTar(t, []tarEntry{
+               {hdr: tar.Header{Name: "escape-link", Typeflag: 
tar.TypeSymlink, Linkname: "../outside", Mode: 0777}},
+               {hdr: tar.Header{Name: "escape-link/escape.txt", Typeflag: 
tar.TypeReg, Mode: 0644}, body: []byte("escaped\n")},
+       })
+
+       if err := UntarNested(bytes.NewReader(data), installDir, false, true, 
true); err == nil {
+               t.Fatal("want error, got nil")
+       }
+
+       if content, err := os.ReadFile(filepath.Join(outsideDir, 
"escape.txt")); err == nil {
+               t.Fatalf("file written outside install dir: content=%q", 
string(content))
+       }
+}
+
+// A chain of symlinks that appears valid lexically but escapes the install dir
+// at runtime must be rejected; hop1 -> "." (resolves to install), hop1/hop2 
-> ".." (escapes to base).
+func Test_UntarNested_RejectsChainedSymlinkEscape(t *testing.T) {
+       baseDir, err := os.MkdirTemp("", "arkade-untar-*")
+       if err != nil {
+               t.Fatal(err)
+       }
+       defer os.RemoveAll(baseDir)
+
+       installDir := filepath.Join(baseDir, "install")
+       if err := os.MkdirAll(installDir, 0755); err != nil {
+               t.Fatal(err)
+       }
+
+       data := buildTar(t, []tarEntry{
+               {hdr: tar.Header{Name: "hop1", Typeflag: tar.TypeSymlink, 
Linkname: ".", Mode: 0777}},
+               {hdr: tar.Header{Name: "hop1/hop2", Typeflag: tar.TypeSymlink, 
Linkname: "..", Mode: 0777}},
+               {hdr: tar.Header{Name: "hop1/hop2/outside/escape.txt", 
Typeflag: tar.TypeReg, Mode: 0644}, body: []byte("escaped\n")},
+       })
+
+       if err := UntarNested(bytes.NewReader(data), installDir, false, true, 
true); err == nil {
+               t.Fatal("want error, got nil")
+       }
+
+       if content, err := os.ReadFile(filepath.Join(baseDir, "outside", 
"escape.txt")); err == nil {
+               t.Fatalf("file written outside install dir: content=%q", 
string(content))
+       }
+}
+
+// A symlink that resolves outside the install dir must not be left on disk,
+// even without a subsequent write; hop1 -> "." (resolves to install), 
hop1/hop2 -> ".." (escapes to base).
+func Test_UntarNested_RejectsPlantedEscapingSymlink(t *testing.T) {
+       baseDir, err := os.MkdirTemp("", "arkade-untar-*")
+       if err != nil {
+               t.Fatal(err)
+       }
+       defer os.RemoveAll(baseDir)
+
+       installDir := filepath.Join(baseDir, "install")
+       if err := os.MkdirAll(installDir, 0755); err != nil {
+               t.Fatal(err)
+       }
+
+       data := buildTar(t, []tarEntry{
+               {hdr: tar.Header{Name: "hop1", Typeflag: tar.TypeSymlink, 
Linkname: ".", Mode: 0777}},
+               {hdr: tar.Header{Name: "hop1/hop2", Typeflag: tar.TypeSymlink, 
Linkname: "..", Mode: 0777}},
+       })
+
+       if err := UntarNested(bytes.NewReader(data), installDir, false, true, 
true); err == nil {
+               t.Fatal("want error, got nil")
+       }
+
+       planted := filepath.Join(installDir, "hop2")
+       if fi, err := os.Lstat(planted); err == nil && fi.Mode()&os.ModeSymlink 
!= 0 {
+               target, _ := os.Readlink(planted)
+               t.Fatalf("escaping symlink left on disk: %s -> %q", planted, 
target)
+       }
+}
+
+// A symlink whose target traverses a pre-existing symlink (inside the install 
dir
+// but pointing outside) must be rejected and not left on disk. The lexical 
target
+// check alone passes here, so this exercises the physical-prefix resolution.
+func Test_UntarNested_RejectsSymlinkTargetViaPreExistingSymlink(t *testing.T) {
+       baseDir, err := os.MkdirTemp("", "arkade-untar-*")
+       if err != nil {
+               t.Fatal(err)
+       }
+       defer os.RemoveAll(baseDir)
+
+       installDir := filepath.Join(baseDir, "install")
+       if err := os.MkdirAll(installDir, 0755); err != nil {
+               t.Fatal(err)
+       }
+       outsideDir := filepath.Join(baseDir, "outside")
+       if err := os.MkdirAll(outsideDir, 0755); err != nil {
+               t.Fatal(err)
+       }
+       // Pre-existing symlink inside the install dir that points outside.
+       if err := os.Symlink(outsideDir, filepath.Join(installDir, "safe")); 
err != nil {
+               t.Fatal(err)
+       }
+
+       // "safe/file" is lexically under installDir, but "safe" resolves 
outside.
+       data := buildTar(t, []tarEntry{
+               {hdr: tar.Header{Name: "planted", Typeflag: tar.TypeSymlink, 
Linkname: "safe/file", Mode: 0777}},
+       })
+
+       if err := UntarNested(bytes.NewReader(data), installDir, false, true, 
true); err == nil {
+               t.Fatalf("expected extraction to be rejected, got nil error")
+       }
+
+       planted := filepath.Join(installDir, "planted")
+       if fi, err := os.Lstat(planted); err == nil && fi.Mode()&os.ModeSymlink 
!= 0 {
+               target, _ := os.Readlink(planted)
+               t.Fatalf("escaping symlink left on disk: %s -> %q", planted, 
target)
+       }
+}
+
+// Ordinary nested directories and files must extract correctly.
+func Test_UntarNested_AllowsValidNestedFiles(t *testing.T) {
+       installDir, err := os.MkdirTemp("", "arkade-untar-*")
+       if err != nil {
+               t.Fatal(err)
+       }
+       defer os.RemoveAll(installDir)
+
+       data := buildTar(t, []tarEntry{
+               {hdr: tar.Header{Name: "bin", Typeflag: tar.TypeDir, Mode: 
0755}},
+               {hdr: tar.Header{Name: "bin/tool", Typeflag: tar.TypeReg, Mode: 
0755}, body: []byte("#!/bin/sh\n")},
+               {hdr: tar.Header{Name: "README.md", Typeflag: tar.TypeReg, 
Mode: 0644}, body: []byte("hello\n")},
+       })
+
+       if err := UntarNested(bytes.NewReader(data), installDir, false, true, 
true); err != nil {
+               t.Fatalf("expected clean extraction, got: %v", err)
+       }
+       for _, rel := range []string{"bin/tool", "README.md"} {
+               if _, err := os.Stat(filepath.Join(installDir, rel)); err != 
nil {
+                       t.Fatalf("expected %q to exist: %v", rel, err)
+               }
+       }
+}
+
+// A file written through an internal symlink (one whose target stays within 
the install dir)
+// must land at the symlink's target location.
+func Test_UntarNested_AllowsWriteThroughInternalSymlink(t *testing.T) {
+       installDir, err := os.MkdirTemp("", "arkade-untar-*")
+       if err != nil {
+               t.Fatal(err)
+       }
+       defer os.RemoveAll(installDir)
+
+       data := buildTar(t, []tarEntry{
+               {hdr: tar.Header{Name: "subdir", Typeflag: tar.TypeDir, Mode: 
0755}},
+               {hdr: tar.Header{Name: "link", Typeflag: tar.TypeSymlink, 
Linkname: "subdir", Mode: 0777}},
+               {hdr: tar.Header{Name: "link/file.txt", Typeflag: tar.TypeReg, 
Mode: 0644}, body: []byte("hello\n")},
+       })
+
+       if err := UntarNested(bytes.NewReader(data), installDir, false, true, 
true); err != nil {
+               t.Fatalf("expected clean extraction with write through internal 
symlink, got: %v", err)
+       }
+       if _, err := os.Stat(filepath.Join(installDir, "subdir", "file.txt")); 
err != nil {
+               t.Fatalf("expected file to exist at symlink target: %v", err)
+       }
+}
+
+// A directory created through an internal symlink must land at the symlink's 
target location.
+func Test_UntarNested_AllowsWriteThroughInternalSymlinkDir(t *testing.T) {
+       installDir, err := os.MkdirTemp("", "arkade-untar-*")
+       if err != nil {
+               t.Fatal(err)
+       }
+       defer os.RemoveAll(installDir)
+
+       data := buildTar(t, []tarEntry{
+               {hdr: tar.Header{Name: "subdir", Typeflag: tar.TypeDir, Mode: 
0755}},
+               {hdr: tar.Header{Name: "link", Typeflag: tar.TypeSymlink, 
Linkname: "subdir", Mode: 0777}},
+               {hdr: tar.Header{Name: "link/newdir", Typeflag: tar.TypeDir, 
Mode: 0755}},
+       })
+
+       if err := UntarNested(bytes.NewReader(data), installDir, false, true, 
true); err != nil {
+               t.Fatalf("expected clean extraction with dir write-through 
internal symlink, got: %v", err)
+       }
+       if _, err := os.Stat(filepath.Join(installDir, "subdir", "newdir")); 
err != nil {
+               t.Fatalf("expected directory to exist at symlink target: %v", 
err)
+       }
+}
+
+// A pre-existing symlink inside the extraction root that points outside must 
not cause
+// MkdirAll to create directories outside the root, even though no file is 
written there.
+func Test_UntarNested_PreExistingSymlinkDoesNotCreateDirOutsideRoot(t 
*testing.T) {
+       baseDir, err := os.MkdirTemp("", "arkade-untar-*")
+       if err != nil {
+               t.Fatal(err)
+       }
+       defer os.RemoveAll(baseDir)
+
+       installDir := filepath.Join(baseDir, "install")
+       if err := os.MkdirAll(installDir, 0755); err != nil {
+               t.Fatal(err)
+       }
+       outsideDir := filepath.Join(baseDir, "outside")
+       if err := os.MkdirAll(outsideDir, 0755); err != nil {
+               t.Fatal(err)
+       }
+
+       // Plant a symlink inside the extraction root pointing outside — 
pre-existing, not from tar.
+       if err := os.Symlink(outsideDir, filepath.Join(installDir, "link")); 
err != nil {
+               t.Fatal(err)
+       }
+
+       // Tar only contains a regular file whose parent traverses the 
pre-existing symlink.
+       data := buildTar(t, []tarEntry{
+               {hdr: tar.Header{Name: "link/subdir/escape.txt", Typeflag: 
tar.TypeReg, Mode: 0644}, body: []byte("escaped\n")},
+       })
+
+       if err := UntarNested(bytes.NewReader(data), installDir, false, true, 
true); err == nil {
+               t.Fatal("want error, got nil")
+       }
+
+       // MkdirAll must not follow the symlink and create outsideDir/subdir 
before EvalSymlinks catches the escape.
+       if _, err := os.Stat(filepath.Join(outsideDir, "subdir")); err == nil {
+               t.Fatal("directory created outside extraction root via 
pre-existing symlink")
+       }
+}
+
+// A pre-existing symlink at the final path component must not be written 
through;
+// a regular-file entry of the same name must not redirect the write outside 
root.
+func Test_UntarNested_RejectsLeafSymlinkWriteThrough(t *testing.T) {
+       baseDir, err := os.MkdirTemp("", "arkade-untar-*")
+       if err != nil {
+               t.Fatal(err)
+       }
+       defer os.RemoveAll(baseDir)
+
+       installDir := filepath.Join(baseDir, "install")
+       if err := os.MkdirAll(installDir, 0755); err != nil {
+               t.Fatal(err)
+       }
+       outsideTarget := filepath.Join(baseDir, "target.txt")
+       if err := os.WriteFile(outsideTarget, []byte("ORIGINAL"), 0644); err != 
nil {
+               t.Fatal(err)
+       }
+       // Plant a leaf symlink inside the root pointing at a file outside the 
root.
+       if err := os.Symlink(outsideTarget, filepath.Join(installDir, "evil")); 
err != nil {
+               t.Fatal(err)
+       }
+
+       data := buildTar(t, []tarEntry{
+               {hdr: tar.Header{Name: "evil", Typeflag: tar.TypeReg, Mode: 
0644}, body: []byte("HACKED")},
+       })
+
+       if err := UntarNested(bytes.NewReader(data), installDir, false, true, 
true); err == nil {
+               t.Fatal("want error, got nil")
+       }
+
+       if b, _ := os.ReadFile(outsideTarget); string(b) != "ORIGINAL" {
+               t.Fatalf("file outside root overwritten through leaf symlink: 
now %q", string(b))
+       }
+}
+
+// A symlink whose target stays within the install dir must be created.
+func Test_UntarNested_AllowsValidInternalSymlink(t *testing.T) {
+       installDir, err := os.MkdirTemp("", "arkade-untar-*")
+       if err != nil {
+               t.Fatal(err)
+       }
+       defer os.RemoveAll(installDir)
+
+       data := buildTar(t, []tarEntry{
+               {hdr: tar.Header{Name: "tool-v1", Typeflag: tar.TypeReg, Mode: 
0755}, body: []byte("bin\n")},
+               {hdr: tar.Header{Name: "tool", Typeflag: tar.TypeSymlink, 
Linkname: "tool-v1", Mode: 0777}},
+       })
+
+       if err := UntarNested(bytes.NewReader(data), installDir, false, true, 
true); err != nil {
+               t.Fatalf("expected clean extraction with internal symlink, got: 
%v", err)
+       }
+       linkPath := filepath.Join(installDir, "tool")
+       fi, err := os.Lstat(linkPath)
+       if err != nil {
+               t.Fatalf("expected symlink %q to exist: %v", linkPath, err)
+       }
+       if fi.Mode()&os.ModeSymlink == 0 {
+               t.Fatalf("expected %q to be a symlink", linkPath)
+       }
+}
+
+// A symlink entry whose parent directory is not listed as its own entry in 
the tar
+// must still be created; the parent directory is made on demand.
+func Test_UntarNested_CreatesParentDirForSymlink(t *testing.T) {
+       installDir, err := os.MkdirTemp("", "arkade-untar-*")
+       if err != nil {
+               t.Fatal(err)
+       }
+       defer os.RemoveAll(installDir)
+
+       // No "nested" directory entry precedes the symlink; the target stays 
within root.
+       data := buildTar(t, []tarEntry{
+               {hdr: tar.Header{Name: "nested/link", Typeflag: 
tar.TypeSymlink, Linkname: "tool-v1", Mode: 0777}},
+       })
+
+       if err := UntarNested(bytes.NewReader(data), installDir, false, true, 
true); err != nil {
+               t.Fatalf("expected clean extraction with on-demand parent dir 
for symlink, got: %v", err)
+       }
+       linkPath := filepath.Join(installDir, "nested", "link")
+       fi, err := os.Lstat(linkPath)
+       if err != nil {
+               t.Fatalf("expected symlink %q to exist: %v", linkPath, err)
+       }
+       if fi.Mode()&os.ModeSymlink == 0 {
+               t.Fatalf("expected %q to be a symlink", linkPath)
+       }
+}
+
+// When allowSymlinks is false, any symlink entry in the tar must be rejected,
+// even when the link target stays safely within root.
+func Test_UntarNested_RejectsSymlinkWhenDisabled(t *testing.T) {
+       installDir, err := os.MkdirTemp("", "arkade-untar-*")
+       if err != nil {
+               t.Fatal(err)
+       }
+       defer os.RemoveAll(installDir)
+
+       data := buildTar(t, []tarEntry{
+               {hdr: tar.Header{Name: "tool-v1", Typeflag: tar.TypeReg, Mode: 
0755}, body: []byte("bin\n")},
+               {hdr: tar.Header{Name: "tool", Typeflag: tar.TypeSymlink, 
Linkname: "tool-v1", Mode: 0777}},
+       })
+
+       if err := UntarNested(bytes.NewReader(data), installDir, false, true, 
false); err == nil {
+               t.Fatalf("expected error when extracting symlink with symlinks 
disabled, got nil")
+       }
+       if _, err := os.Lstat(filepath.Join(installDir, "tool")); err == nil {
+               t.Fatalf("expected symlink not to be created when symlinks 
disabled")
+       }
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/arkade-0.11.100/pkg/get/get_test.go 
new/arkade-0.11.102/pkg/get/get_test.go
--- old/arkade-0.11.100/pkg/get/get_test.go     2026-06-16 19:06:19.000000000 
+0200
+++ new/arkade-0.11.102/pkg/get/get_test.go     2026-06-22 16:31:10.000000000 
+0200
@@ -3426,6 +3426,60 @@
 
 }
 
+func Test_DownloadPlutoCli(t *testing.T) {
+       tools := MakeTools()
+       name := "pluto"
+
+       tool := getTool(name, tools)
+
+       tests := []test{
+               {
+                       os:      "darwin",
+                       arch:    arch64bit,
+                       version: "3.12.0",
+                       url:     
`https://github.com/FairwindsOps/pluto/releases/download/3.12.0/pluto_3.12.0_darwin_amd64.tar.gz`,
+               },
+               {
+                       os:      "linux",
+                       arch:    arch64bit,
+                       version: "3.12.0",
+                       url:     
`https://github.com/FairwindsOps/pluto/releases/download/3.12.0/pluto_3.12.0_linux_amd64.tar.gz`,
+               },
+               {
+                       os:      "linux",
+                       arch:    archARM64,
+                       version: "3.12.0",
+                       url:     
`https://github.com/FairwindsOps/pluto/releases/download/3.12.0/pluto_3.12.0_linux_arm64.tar.gz`,
+               },
+               {
+                       os:      "darwin",
+                       arch:    archDarwinARM64,
+                       version: "3.12.0",
+                       url:     
`https://github.com/FairwindsOps/pluto/releases/download/3.12.0/pluto_3.12.0_darwin_arm64.tar.gz`,
+               },
+               {
+                       os:      "linux",
+                       arch:    archARM7,
+                       version: "3.12.0",
+                       url:     
`https://github.com/FairwindsOps/pluto/releases/download/3.12.0/pluto_3.12.0_linux_armv7.tar.gz`,
+               },
+       }
+
+       for _, tc := range tests {
+               t.Run(tc.os+" "+tc.arch+" "+tc.version, func(r *testing.T) {
+
+                       got, _, err := tool.GetURL(tc.os, tc.arch, tc.version, 
false)
+                       if err != nil {
+                               t.Fatal(err)
+                       }
+                       if got != tc.url {
+                               t.Errorf("want: %s, got: %s", tc.url, got)
+                       }
+               })
+       }
+
+}
+
 func Test_DownloadKubetailCli(t *testing.T) {
        tools := MakeTools()
        name := "kubetail"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/arkade-0.11.100/pkg/get/tools.go 
new/arkade-0.11.102/pkg/get/tools.go
--- old/arkade-0.11.100/pkg/get/tools.go        2026-06-16 19:06:19.000000000 
+0200
+++ new/arkade-0.11.102/pkg/get/tools.go        2026-06-22 16:31:10.000000000 
+0200
@@ -2200,6 +2200,34 @@
 
        tools = append(tools,
                Tool{
+                       Owner:       "FairwindsOps",
+                       Repo:        "pluto",
+                       Name:        "pluto",
+                       Description: "Find deprecated Kubernetes apiVersions in 
code repositories and helm releases.",
+                       BinaryTemplate: `
+                               {{$arch := "amd64"}}
+                               {{if eq .Arch "armv7l" -}}
+                               {{$arch = "armv7"}}
+                               {{- else if eq .Arch "aarch64" -}}
+                               {{$arch = "arm64"}}
+                               {{- else if eq .Arch "arm64" -}}
+                               {{$arch = "arm64"}}
+                               {{- end -}}
+
+                               {{$osString:= .OS}}
+                               {{ if HasPrefix .OS "darwin" -}}
+                               {{$osString = "darwin"}}
+                               {{- else if eq .OS "linux" -}}
+                               {{$osString = "linux"}}
+                               {{- end -}}
+                               {{$ext := ".tar.gz"}}
+
+                               
{{.Version}}/{{.Name}}_{{.VersionNumber}}_{{$osString}}_{{$arch}}{{$ext}}
+                               `,
+               })
+
+       tools = append(tools,
+               Tool{
                        Owner:       "johanhaleby",
                        Repo:        "kubetail",
                        Name:        "kubetail",

++++++ arkade.obsinfo ++++++
--- /var/tmp/diff_new_pack.mq8tIL/_old  2026-06-23 17:43:12.039179541 +0200
+++ /var/tmp/diff_new_pack.mq8tIL/_new  2026-06-23 17:43:12.043179682 +0200
@@ -1,5 +1,5 @@
 name: arkade
-version: 0.11.100
-mtime: 1781629579
-commit: db87cbea02bb931f4277719629597a627c4d4b5e
+version: 0.11.102
+mtime: 1782138670
+commit: 37af31c7b58de8f16a10067051e3bbe7ecb6aa79
 

++++++ vendor.tar.gz ++++++

Reply via email to