Files
trufflehog/pkg/handlers/apk_content_detection_test.go
Johan NestaasandCursor 6f3c981e7b
Lint / golangci-lint (push) Waiting to run
Lint / man-page-staleness (push) Waiting to run
Lint / semgrep (push) Waiting to run
Lint / checksecretparts (push) Waiting to run
Scan for secrets / test (push) Waiting to run
Test / test (push) Waiting to run
Test / test-community (push) Waiting to run
Release / mark-latest (push) Canceled after 0s
Release / Release (push) Canceled after 0s
fix(handlers): apk handler now doesnt check for apk extension since json-enumerator and other byte stream methods wouldnt have it (#5151)
* fix(handlers): apk handler now doesnt check for apk extension since json-enumerator and other byte stream methods wouldnt have it

* fix(handlers): don't fail zip/jar processing when APK check errors

Content-based APK detection now runs isAPKFile on every zip/jar (not just
.apk-named files). Previously a zip.NewReader failure (truncated zip,
corrupted central directory, or a polyglot mimetype still reports as zip)
was returned as a fatal error from newFileReader, causing HandleFile to
skip the file entirely. Treat the APK check error as non-fatal: log at V(3)
and fall through to the archive/default handler.

Also remove the now-dead readerConfig/readerOption/withFileExtension and
getFileExtension infrastructure left unused after shouldHandleAsAPK stopped
consuming the file extension.

Co-authored-by: Cursor <[email protected]>

---------

Co-authored-by: Cursor <[email protected]>
2026-07-24 10:38:46 -07:00

117 lines
4.4 KiB
Go

package handlers
import (
"archive/zip"
"bytes"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
"github.com/trufflesecurity/trufflehog/v3/pkg/feature"
)
// makeAPKZip builds an in-memory zip that satisfies isAPKFile (contains both
// AndroidManifest.xml and classes.dex). A large stored padding entry is written
// first so the manifest/dex local headers fall outside mimetype's detection
// window, keeping mimetype's verdict "application/zip" rather than APK. This
// forces the content-based isAPKFile deep scan (rather than mimetype's own APK
// detection) to be what promotes the file to the APK handler.
func makeAPKZip(t *testing.T, extraFiles map[string][]byte) []byte {
t.Helper()
var buf bytes.Buffer
zw := zip.NewWriter(&buf)
padding, err := zw.CreateHeader(&zip.FileHeader{Name: "padding.txt", Method: zip.Store})
require.NoError(t, err)
_, err = padding.Write(bytes.Repeat([]byte("A"), 5000))
require.NoError(t, err)
for _, name := range []string{"AndroidManifest.xml", "classes.dex"} {
w, err := zw.Create(name)
require.NoError(t, err)
_, err = w.Write([]byte("placeholder"))
require.NoError(t, err)
}
for name, content := range extraFiles {
w, err := zw.Create(name)
require.NoError(t, err)
_, err = w.Write(content)
require.NoError(t, err)
}
require.NoError(t, zw.Close())
return buf.Bytes()
}
// makeNonAPKZip builds a plain in-memory zip without any APK markers.
func makeNonAPKZip(t *testing.T) []byte {
t.Helper()
var buf bytes.Buffer
zw := zip.NewWriter(&buf)
w, err := zw.Create("hello.txt")
require.NoError(t, err)
_, err = w.Write([]byte("just some content"))
require.NoError(t, err)
require.NoError(t, zw.Close())
return buf.Bytes()
}
// TestNewFileReaderAPKContentDetection verifies APK detection is driven by file
// content (the presence of AndroidManifest.xml + classes.dex) and not by any
// filename/extension: the reader is promoted to the APK mime type with no
// extension supplied, while a zip lacking those markers stays a generic zip.
func TestNewFileReaderAPKContentDetection(t *testing.T) {
feature.EnableAPKHandler.Store(true)
t.Cleanup(func() { feature.EnableAPKHandler.Store(false) })
ctx := context.Background()
rdr, err := newFileReader(ctx, bytes.NewReader(makeAPKZip(t, nil)))
require.NoError(t, err)
assert.Equal(t, string(apkMime), rdr.mime.String(), "APK content should route to the APK handler without an extension")
require.NoError(t, rdr.Close())
rdrPlain, err := newFileReader(ctx, bytes.NewReader(makeNonAPKZip(t)))
require.NoError(t, err)
assert.Equal(t, string(zipMime), rdrPlain.mime.String(), "a zip without APK markers should remain a generic zip")
require.NoError(t, rdrPlain.Close())
}
// TestNewFileReaderAPKDetectionDisabled ensures APK content detection does not
// run when the feature flag is off, leaving the file as a generic zip.
func TestNewFileReaderAPKDetectionDisabled(t *testing.T) {
feature.EnableAPKHandler.Store(false)
rdr, err := newFileReader(context.Background(), bytes.NewReader(makeAPKZip(t, nil)))
require.NoError(t, err)
assert.Equal(t, string(zipMime), rdr.mime.String())
require.NoError(t, rdr.Close())
}
// TestNewFileReaderAPKCheckErrorFallsThrough ensures that when the APK feature
// flag is on and mimetype identifies a file as a zip, but the content-based APK
// check fails to parse it (e.g. truncated/corrupted zip, or a polyglot that
// mimetype still reports as zip), newFileReader does NOT return a fatal error.
// Instead it must fall through to normal handling so the file is still processed
// rather than skipped entirely. This guards the regression where dropping the
// .apk extension guard made every unparseable zip/jar unprocessable.
func TestNewFileReaderAPKCheckErrorFallsThrough(t *testing.T) {
feature.EnableAPKHandler.Store(true)
t.Cleanup(func() { feature.EnableAPKHandler.Store(false) })
// Start from a valid zip (so mimetype detects "application/zip") then truncate
// the tail, removing the end-of-central-directory record so zip.NewReader fails.
valid := makeNonAPKZip(t)
truncated := valid[:len(valid)-10]
rdr, err := newFileReader(context.Background(), bytes.NewReader(truncated))
require.NoError(t, err, "APK check failure must not be fatal; file should fall through to normal handling")
assert.Equal(t, string(zipMime), rdr.mime.String(), "file should remain a generic zip, not be skipped")
require.NoError(t, rdr.Close())
}