Skip to content

Commit 0a16bd0

Browse files
authored
feat: add basic zstd compression support (#842)
Malcontent was not properly scanning zstd compressed files e.g. kernel modules on modern Ubuntu systems. As an example, without this change: ``` $ mal --format=simple --verbose analyze /lib/modules/6.11.0-19-generic/kernel/fs/smb/server/ksmbd.ko.zst time=2025-03-24T20:51:36.262-07:00 level=DEBUG source=$HOME/git/chainguard-dev/malcontent/pkg/action/scan.go:71 msg="skipping /usr/lib/modules/6.11.0-19-generic/kernel/fs/smb/server/ksmbd.ko.zst [<unknown>]: data file or empty" path=/usr/lib/modules/6.11.0-19-generic/kernel/fs/smb/server/ksmbd.ko.zst ``` With this patch applied: ``` $ ./mal --format=simple --verbose analyze /lib/modules/6.11.0-19-generic/kernel/fs/smb/server/ksmbd.ko.zst time=2025-03-24T20:53:47.375-07:00 level=DEBUG source=$HOME/git/chainguard-dev/malcontent/pkg/archive/archive.go:110 msg="creating temp dir" path=/usr/lib/modules/6.11.0-19-generic/kernel/fs/smb/server/ksmbd.ko.zst time=2025-03-24T20:53:47.375-07:00 level=DEBUG source=$HOME/git/chainguard-dev/malcontent/pkg/archive/zstd.go:18 msg="extracting zstd" dir=$HOME/tmp/ksmbd.ko.zst439390431 file=/usr/lib/modules/6.11.0-19-generic/kernel/fs/smb/server/ksmbd.ko.zst c2/addr/ip: medium crypto/aes: low crypto/cipher: medium fs/attributes/remove: medium fs/attributes/set: medium fs/directory/create: low fs/directory/remove: low fs/file/delete: low fs/file/open: low fs/lock_update: low impact/remote_access/heartbeat: medium net/ip/send_unicast: low net/rpc/ntlm: medium net/socket/listen: medium net/socket/peer_address: low net/socket/receive: low net/socket/send: low os/kernel/netlink: low persist/daemon: medium persist/kernel_module/module: medium persist/kernel_module/name: medium sus/exclamation: medium ``` This patch was mostly copy-wasting from the bz2 archive implementation and cherry-picking bits and bobs from the zstd support in the rpm.go implementation. Signed-off-by: Steve Beattie <steve.beattie@chainguard.dev>
1 parent 5254e43 commit 0a16bd0

File tree

3 files changed

+78
-0
lines changed

3 files changed

+78
-0
lines changed

pkg/archive/archive.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,8 @@ func ExtractionMethod(ext string) func(context.Context, string, string) error {
212212
return ExtractZip
213213
case ".bz2", ".bzip2":
214214
return ExtractBz2
215+
case ".zst", ".zstd":
216+
return ExtractZstd
215217
case ".rpm":
216218
return ExtractRPM
217219
case ".deb":

pkg/archive/zstd.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package archive
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"io"
7+
"os"
8+
"path/filepath"
9+
"strings"
10+
11+
"github.com/chainguard-dev/clog"
12+
"github.com/klauspost/compress/zstd"
13+
)
14+
15+
// ExtractZstd extracts .zst and .zstd archives.
16+
func ExtractZstd(ctx context.Context, d string, f string) error {
17+
logger := clog.FromContext(ctx).With("dir", d, "file", f)
18+
logger.Debug("extracting zstd")
19+
20+
buf, ok := bufferPool.Get().(*[]byte)
21+
if !ok {
22+
return fmt.Errorf("failed to retrieve buffer for zstd")
23+
}
24+
defer bufferPool.Put(buf)
25+
26+
fi, err := os.Stat(f)
27+
if err != nil {
28+
return fmt.Errorf("failed to stat zstd file %s: %w", f, err)
29+
}
30+
if fi.Size() == 0 {
31+
return fmt.Errorf("empty zstd file: %s", f)
32+
}
33+
34+
uncompressed := strings.TrimSuffix(filepath.Base(f), ".zstd")
35+
uncompressed = strings.TrimSuffix(uncompressed, ".zst")
36+
target := filepath.Join(d, uncompressed)
37+
38+
if !IsValidPath(target, d) {
39+
return fmt.Errorf("invalid zstd decompression file path: %s", target)
40+
}
41+
42+
if err := os.MkdirAll(d, 0o700); err != nil {
43+
return fmt.Errorf("failed to create directory for decomrpessed zstd file: %w", err)
44+
}
45+
46+
out, err := os.OpenFile(target, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600)
47+
if err != nil {
48+
return fmt.Errorf("failed to create decompressed zstd file: %w", err)
49+
}
50+
defer out.Close()
51+
52+
zstdFile, err := os.Open(f)
53+
if err != nil {
54+
return fmt.Errorf("failed to open zstd file: %w", err)
55+
}
56+
defer zstdFile.Close()
57+
58+
zr, err := zstd.NewReader(zstdFile)
59+
if err != nil {
60+
return fmt.Errorf("failed to open zstd file %s: %w", f, err)
61+
}
62+
defer zr.Close()
63+
64+
written, err := io.CopyBuffer(out, io.LimitReader(zr, maxBytes), *buf)
65+
if err != nil {
66+
return fmt.Errorf("failed to copy zstd compressed file: %w", err)
67+
}
68+
69+
if written >= maxBytes {
70+
return fmt.Errorf("file exceeds maximum allowed size (%d bytes): %s", maxBytes, target)
71+
}
72+
73+
return nil
74+
}

pkg/programkind/programkind.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ var ArchiveMap = map[string]bool{
3535
".upx": true,
3636
".whl": true,
3737
".xz": true,
38+
".zst": true,
39+
".zstd": true,
3840
".zip": true,
3941
}
4042

0 commit comments

Comments
 (0)